This commit is contained in:
suaveolent 2023-11-17 12:10:33 +01:00
commit cb3b4dae38
8 changed files with 369 additions and 0 deletions

188
.gitignore vendored Normal file
View File

@ -0,0 +1,188 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

0
README.md Normal file
View File

View File

22
hoymiles-wifi/__main__.py Normal file
View File

@ -0,0 +1,22 @@
from inverter import Inverter
import time
def main():
# Replace 'your_inverter_ip' with the actual IP address or hostname of your inverter
inverter = Inverter('192.168.1.190')
try:
while True:
response = inverter.update_state()
if response:
print(f"Inverter State: {response}")
else:
print("Unable to retrieve inverter state")
# Sleep for a while before the next update
time.sleep(60)
except KeyboardInterrupt:
print("Exiting.")
if __name__ == "__main__":
main()

72
hoymiles-wifi/inverter.py Normal file
View File

@ -0,0 +1,72 @@
import socket
import struct
from protos.RealData_pb2 import RealDataResDTO, HMSStateResponse
import crcmod
import logging
from datetime import datetime
import time
logging.basicConfig(level=logging.INFO)
INVERTER_PORT = 10081
class NetworkState:
Unknown = 0
Online = 1
Offline = 2
class Inverter:
def __init__(self, host):
self.host = host
self.state = NetworkState.Unknown
self.sequence = 0
def set_state(self, new_state):
if self.state != new_state:
self.state = new_state
logging.info(f"Inverter is {new_state}")
def update_state(self):
self.sequence = (self.sequence + 1) & 0xFFFF
request = RealDataResDTO()
# date = datetime.now()
# time_string = date.strftime("%Y-%m-%d %H:%M:%S")
# request.ymd_hms = time_string
# request.cp = 23 + self.sequence
# request.offset = 0
# request.time = int(time.time())
header = b"\x48\x4d\xa3\x03"
request_as_bytes = request.SerializeToString()
crc16_func = crcmod.predefined.Crc('modbus')
crc16_func.update(request_as_bytes)
crc16 = crc16_func.crcValue & 0xFFFF
len_bytes = struct.pack('>H', len(request_as_bytes) + 10)
message = header + struct.pack('>HHH', self.sequence, crc16, len(request_as_bytes)) + request_as_bytes
ip = socket.gethostbyname(self.host)
address = (ip, INVERTER_PORT)
try:
with socket.create_connection(address, timeout=0.5) as stream:
stream.sendall(message)
buf = stream.recv(1024)
except socket.error as e:
logging.debug(str(e))
self.set_state(NetworkState.Offline)
return None
read_length = struct.unpack('>H', buf[6:8])[0]
parsed = HMSStateResponse.FromString(buf[10:10 + read_length])
if parsed is None:
logging.debug("Error parsing response")
self.set_state(NetworkState.Offline)
return None
self.set_state(NetworkState.Online)
return parsed

View File

@ -0,0 +1,46 @@
syntax = "proto3";
message RealDataResDTO {
string ymd_hms = 1;
int32 cp = 2;
int32 err_code = 3;
int32 offset = 4;
int32 time = 5;
}
message InverterState {
int64 inv_id = 1;
int32 port_id = 2;
int32 grid_voltage = 3; // [V], factor 0.1
int32 grid_freq = 4; // [Hz], factor 0.1
int32 pv_current_power = 5; // [W], factor 0.1
int32 unknown1 = 7;
int32 unknown2 = 8; // power limit? [%], factor 0.1
int32 temperature = 9; // [C], factor 0.1
int32 unknown3 = 10;
int32 unknown4 = 12;
int32 bit_field = 20; // a guess
}
message PortState {
int64 pv_sn = 1; // serial number
int32 pv_port = 2; // id of the port
int32 pv_vol = 3; // [V], factor 0.1
int32 pv_cur = 4; // [A], factor 0.1
int32 pv_power = 5; // [W], factor 0.1
int32 pv_energy_total = 6; // [Wh]
int32 pv_daily_yield = 7; // [Wh]
int32 bitfield = 8; // bitfield is a guess
}
message HMSStateResponse {
bytes dtu_sn = 1; // serial
int32 time = 2; // epoch
int32 device_nub = 3;
int32 pv_nub = 4; // repeats cp field from request
int32 package_nub = 5;
repeated InverterState inverter_state = 9;
repeated PortState port_state = 11;
int32 pv_current_power = 12; // [W], factor 0.1
int32 pv_daily_yield = 13; // [Wh]
}

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: RealData.proto
# Protobuf Python Version: 4.25.0
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eRealData.proto\"]\n\x0eRealDataResDTO\x12\x0f\n\x07ymd_hms\x18\x01 \x01(\t\x12\n\n\x02\x63p\x18\x02 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\"\xe3\x01\n\rInverterState\x12\x0e\n\x06inv_id\x18\x01 \x01(\x03\x12\x0f\n\x07port_id\x18\x02 \x01(\x05\x12\x14\n\x0cgrid_voltage\x18\x03 \x01(\x05\x12\x11\n\tgrid_freq\x18\x04 \x01(\x05\x12\x18\n\x10pv_current_power\x18\x05 \x01(\x05\x12\x10\n\x08unknown1\x18\x07 \x01(\x05\x12\x10\n\x08unknown2\x18\x08 \x01(\x05\x12\x13\n\x0btemperature\x18\t \x01(\x05\x12\x10\n\x08unknown3\x18\n \x01(\x05\x12\x10\n\x08unknown4\x18\x0c \x01(\x05\x12\x11\n\tbit_field\x18\x14 \x01(\x05\"\xa0\x01\n\tPortState\x12\r\n\x05pv_sn\x18\x01 \x01(\x03\x12\x0f\n\x07pv_port\x18\x02 \x01(\x05\x12\x0e\n\x06pv_vol\x18\x03 \x01(\x05\x12\x0e\n\x06pv_cur\x18\x04 \x01(\x05\x12\x10\n\x08pv_power\x18\x05 \x01(\x05\x12\x17\n\x0fpv_energy_total\x18\x06 \x01(\x05\x12\x16\n\x0epv_daily_yield\x18\x07 \x01(\x05\x12\x10\n\x08\x62itfield\x18\x08 \x01(\x05\"\xe3\x01\n\x10HMSStateResponse\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\x0c\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x12\n\ndevice_nub\x18\x03 \x01(\x05\x12\x0e\n\x06pv_nub\x18\x04 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x05 \x01(\x05\x12&\n\x0einverter_state\x18\t \x03(\x0b\x32\x0e.InverterState\x12\x1e\n\nport_state\x18\x0b \x03(\x0b\x32\n.PortState\x12\x18\n\x10pv_current_power\x18\x0c \x01(\x05\x12\x16\n\x0epv_daily_yield\x18\r \x01(\x05\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'RealData_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_REALDATARESDTO']._serialized_start=18
_globals['_REALDATARESDTO']._serialized_end=111
_globals['_INVERTERSTATE']._serialized_start=114
_globals['_INVERTERSTATE']._serialized_end=341
_globals['_PORTSTATE']._serialized_start=344
_globals['_PORTSTATE']._serialized_end=504
_globals['_HMSSTATERESPONSE']._serialized_start=507
_globals['_HMSSTATERESPONSE']._serialized_end=734
# @@protoc_insertion_point(module_scope)

9
setup.py Normal file
View File

@ -0,0 +1,9 @@
from setuptools import find_packages, setup
setup(
name='hoymiles-wifi',
packages=find_packages(),
version='0.0.1',
description='A python library for interfacing with Hoymiles HMS-XXXXW-T2 series of micro-inverters.',
author='suaveolent',
)