diff --git a/README.md b/README.md index 697a6801..45066d88 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ List of approaches - [Others, C/C++](tools/nano/NRF24_SendRcv/) ## Quick Start with ESP8266 -- [Go here ✨](https://github.com/grindylow/ahoy/blob/ahoy_v0.5.16/tools/esp8266/README.md#things-needed) +- [Go here ✨](tools/esp8266/README.md#things-needed) ## Success Stories diff --git a/tools/esp8266/README.md b/tools/esp8266/README.md index 97ccdd13..18f00c17 100644 --- a/tools/esp8266/README.md +++ b/tools/esp8266/README.md @@ -33,6 +33,8 @@ This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.
Further information will help you to communicate to the compatible inverters. +You find the full [User_Manual here](User_Manual.md) + ## Compatiblity For now the following Inverters should work out of the box: @@ -232,7 +234,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t ## MQTT command to set the DTU without webinterface - [Read here](https://github.com/grindylow/ahoy/blob/main/tools/esp8266/User_Manual.md) +[Read here](tools/esp8266/User_Manual.md) ## Used Libraries @@ -258,4 +260,4 @@ We run a Discord Server that can be used to get in touch with the Developers and ## ToDo -[See this post](https://github.com/grindylow/ahoy/issues/142) +[See this post](https://github.com/lumapu/ahoy/issues/142) diff --git a/tools/esp8266/User_Manual.md b/tools/esp8266/User_Manual.md index bd273817..be023ec7 100644 --- a/tools/esp8266/User_Manual.md +++ b/tools/esp8266/User_Manual.md @@ -237,6 +237,8 @@ Gather user inverter information here to understand what differs between some in | setje | HM-600 | | 1.0.08 | 2020 | 07-10 | 104 | | | | madmartin | HM-600 | 0.1.4 | 1.0.10 | 2021 | 11-01 | 104 | | | | lumapu | HM-1200 | 0.1.0 | 1.0.12 | 2020 | 06-24 | | | | +| chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | | +| chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | | | | | | | | | | | | | | | | | | | | | | diff --git a/tools/homeassistant/README.md b/tools/homeassistant/README.md new file mode 100644 index 00000000..0cdd9d84 --- /dev/null +++ b/tools/homeassistant/README.md @@ -0,0 +1,9 @@ +# HomeAssistant Examples + +Disclaimer: these are collected examples from https://www.mikrocontroller.net/topic/525778 (Page 12) + +in manual.yaml you will find the setup for manual configuration, adapt your name (Terrasse) and the topic (inverter) to your needs and place it into configuration.yaml + +in autodiscovery.yaml you will find the setup for automatic discovery of the inverter + +Note: the config might need adaption to your system (mqtt, homeassistant etc) diff --git a/tools/homeassistant/autodiscovery.yaml b/tools/homeassistant/autodiscovery.yaml new file mode 100644 index 00000000..48e29163 --- /dev/null +++ b/tools/homeassistant/autodiscovery.yaml @@ -0,0 +1,4 @@ +mqtt: + broker: http:// + discovery: true + discovery_prefix: inverter diff --git a/tools/homeassistant/manual.yaml b/tools/homeassistant/manual.yaml new file mode 100644 index 00000000..793a30e5 --- /dev/null +++ b/tools/homeassistant/manual.yaml @@ -0,0 +1,23 @@ +sensor: + - platform: mqtt + state_topic: "inverter/Terrasse/ch0/P_AC" + name: "Aktuelle Produktion HM-600" + device_class: energy + unit_of_measurement: "Watt" + value_template: > + {{value|round(2)}} + state_class: total_increasing + unique_id: "current_hm600" + last_reset_topic: "inverter/Terrasse/ch0/P_AC" + last_reset_value_template: "1970-01-01T00:00:00+00:00" + - platform: mqtt + state_topic: "inverter/Terrasse/ch0/YieldTotal" + name: "Gesamtproduktion HM-600" + device_class: energy + unit_of_measurement: "KW/H" + value_template: > + {{value|round(2)}} + state_class: total_increasing + unique_id: "total_hm600" + last_reset_topic: "inverter/Terrasse/ch0/YieldTotal" + last_reset_value_template: "1970-01-01T00:00:00+00:00" diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index d6ca70d2..5b305e70 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -482,21 +482,24 @@ def compose_esb_packet(packet, mtu=17, **params): fragment = compose_esb_fragment(packet[i:i+mtu], **params) yield fragment -def compose_set_time_payload(timestamp=None): +def compose_send_time_payload(cmdId, alarm_id=0): """ Build set time request packet - :param timestamp: time to set (default: int(time.time()) ) - :type timestamp: int + :param cmd to request + :type cmd: uint8 :return: payload :rtype: bytes """ - if not timestamp: - timestamp = int(time.time()) + timestamp = int(time.time()) - payload = b'\x0b\x00' - payload = payload + struct.pack('>L', timestamp) # big-endian: msb at low address - payload = payload + b'\x00\x00\x00\x05\x00\x00\x00\x00' + # indices from esp8266 hmRadio.h / sendTimePacket() + payload = struct.pack('>B', cmdId) # 10 + payload = payload + b'\x00' # 11 + payload = payload + struct.pack('>L', timestamp) # 12..15 big-endian: msb at low address + payload = payload + b'\x00\x00' # 16..17 + payload = payload + struct.pack('>H', alarm_id) # 18..19 + payload = payload + b'\x00\x00\x00\x00' # 20..23 return frame_payload(payload) @@ -649,7 +652,7 @@ class InverterTransaction: except StopIteration: seq_last = max(frames, key=lambda frame:frame.seq).seq if len(frames) else 0 self.__retransmit_frame(seq_last + 1) - raise BufferError(f'Missing packet: Last packet {len(self.scratch)}') + raise BufferError(f'Missing packet: Last packet {seq_last + 1}') # Rebuild payload from unordered frames payload = b'' diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 219b4449..4fbaa8ac 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -7,6 +7,7 @@ Hoymiles micro-inverters main application import sys import struct +from enum import IntEnum import re import time from datetime import datetime @@ -16,7 +17,7 @@ from yaml.loader import SafeLoader import paho.mqtt.client import hoymiles -def main_loop(): +def main_loop(do_init): """Main loop""" inverters = [ inverter for inverter in ahoy_config.get('inverters', []) @@ -25,9 +26,29 @@ def main_loop(): for inverter in inverters: if hoymiles.HOYMILES_DEBUG_LOGGING: print(f'Poll inverter {inverter["serial"]}') - poll_inverter(inverter) - -def poll_inverter(inverter, retries=4): + poll_inverter(inverter, do_init) + +class InfoCommands(IntEnum): + InverterDevInform_Simple = 0 # 0x00 + InverterDevInform_All = 1 # 0x01 + GridOnProFilePara = 2 # 0x02 + HardWareConfig = 3 # 0x03 + SimpleCalibrationPara = 4 # 0x04 + SystemConfigPara = 5 # 0x05 + RealTimeRunData_Debug = 11 # 0x0b + RealTimeRunData_Reality = 12 # 0x0c + RealTimeRunData_A_Phase = 13 # 0x0d + RealTimeRunData_B_Phase = 14 # 0x0e + RealTimeRunData_C_Phase = 15 # 0x0f + AlarmData = 17 # 0x11, Alarm data - all unsent alarms + AlarmUpdate = 18 # 0x12, Alarm data - all pending alarms + RecordData = 19 # 0x13 + InternalData = 20 # 0x14 + GetLossRate = 21 # 0x15 + GetSelfCheckState = 30 # 0x1e + InitDataState = 0xff + +def poll_inverter(inverter, do_init, retries=4): """ Send/Receive command_queue, initiate status poll on inverter @@ -39,11 +60,15 @@ def poll_inverter(inverter, retries=4): dtu_ser = ahoy_config.get('dtu', {}).get('serial') # Queue at least status data request - command_queue[str(inverter_ser)].append(hoymiles.compose_set_time_payload()) + inv_str = str(inverter_ser) + if do_init: + command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.InverterDevInform_All)) +# command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.SystemConfigPara)) + command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.RealTimeRunData_Debug)) - # Putt all queued commands for current inverter on air - while len(command_queue[str(inverter_ser)]) > 0: - payload = command_queue[str(inverter_ser)].pop(0) + # Put all queued commands for current inverter on air + while len(command_queue[inv_str]) > 0: + payload = command_queue[inv_str].pop(0) # Send payload {ttl}-times until we get at least one reponse payload_ttl = retries @@ -95,6 +120,11 @@ def poll_inverter(inverter, retries=4): string_id = string_id + 1 print() + if 'event_count' in data: + if event_message_index[inv_str] < data['event_count']: + event_message_index[inv_str] = data['event_count'] + command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) + if mqtt_client: mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None)) @@ -219,6 +249,7 @@ if __name__ == '__main__': mqtt_client = None + event_message_index = {} command_queue = {} mqtt_command_topic_subs = [] @@ -261,7 +292,9 @@ if __name__ == '__main__': g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])] for g_inverter in ahoy_config.get('inverters', []): g_inverter_ser = g_inverter.get('serial') - command_queue[str(g_inverter_ser)] = [] + inv_str = str(g_inverter_ser) + command_queue[inv_str] = [] + event_message_index[inv_str] = 0 # # Enables and subscribe inverter to mqtt /command-Topic @@ -276,10 +309,13 @@ if __name__ == '__main__': loop_interval = ahoy_config.get('interval', 1) try: + do_init = True while True: t_loop_start = time.time() - main_loop() + main_loop(do_init) + + do_init = False print('', end='', flush=True) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 809dd796..42d1fd0f 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -294,10 +294,12 @@ class EventsResponse(UnknownResponse): crc_valid = self.validate_crc_m() if crc_valid: - print(' payload has valid modbus crc') + #print(' payload has valid modbus crc') self.response = self.response[:-2] - status = self.response[:2] + status = struct.unpack('>H', self.response[:2])[0] + a_text = self.alarm_codes.get(status, 'N/A') + print (f' Inverter status: {a_text} ({status})') chunk_size = 12 for i_chunk in range(2, len(self.response), chunk_size): @@ -314,6 +316,28 @@ class EventsResponse(UnknownResponse): print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))) print(end='', flush=True) +class HardwareInfoResponse(UnknownResponse): + def __init__(self, *args, **params): + super().__init__(*args, **params) + """ + const byteAssign_t InfoAssignment[] = { + { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, + { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, + { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, + { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } + }; + self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') + """ + fw_version, fw_build_yyyy, fw_build_mmdd, unknown, hw_id = struct.unpack('>HHHHH', self.response[0:10]) + + fw_version_maj = int((fw_version / 10000)) + fw_version_min = int((fw_version % 10000) / 100) + fw_version_pat = int((fw_version % 100)) + fw_build_mm = int(fw_build_mmdd / 100) + fw_build_dd = int(fw_build_mmdd % 100) + print() + print(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}') + class DebugDecodeAny(UnknownResponse): """Default decoder""" @@ -359,6 +383,9 @@ class DebugDecodeAny(UnknownResponse): # 1121-Series Intervers, 1 MPPT, 1 Phase +class Hm300Decode01(HardwareInfoResponse): + """ Firmware version / date """ + class Hm300Decode02(EventsResponse): """ Inverter generic events log """ @@ -407,6 +434,9 @@ class Hm300Decode0B(StatusResponse): """ Inverter temperature in °C """ return self.unpack('>H', 26)[0]/10 +class Hm300Decode0C(Hm300Decode0B): + """ 1121-series mirco-inverters status data """ + class Hm300Decode11(EventsResponse): """ Inverter generic events log """ @@ -415,6 +445,9 @@ class Hm300Decode12(EventsResponse): # 1141-Series Inverters, 2 MPPT, 1 Phase +class Hm600Decode01(HardwareInfoResponse): + """ Firmware version / date """ + class Hm600Decode02(EventsResponse): """ Inverter generic events log """ @@ -492,6 +525,9 @@ class Hm600Decode0B(StatusResponse): """ Event counter """ return self.unpack('>H', 40)[0] +class Hm600Decode0C(Hm600Decode0B): + """ 1141-series mirco-inverters status data """ + class Hm600Decode11(EventsResponse): """ Inverter generic events log """ @@ -500,6 +536,9 @@ class Hm600Decode12(EventsResponse): # 1161-Series Inverters, 2 MPPT, 1 Phase +class Hm1200Decode01(HardwareInfoResponse): + """ Firmware version / date """ + class Hm1200Decode02(EventsResponse): """ Inverter generic events log """ @@ -619,6 +658,9 @@ class Hm1200Decode0B(StatusResponse): """ Event counter """ return self.unpack('>H', 60)[0] +class Hm1200Decode0C(Hm1200Decode0B): + """ 1161-series mirco-inverters status data """ + class Hm1200Decode11(EventsResponse): """ Inverter generic events log """