Browse Source

Merge branch 'main' into asyncWeb03

pull/283/head
stefan123t 2 years ago
committed by GitHub
parent
commit
12d8ff5949
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 6
      tools/esp8266/README.md
  3. 2
      tools/esp8266/User_Manual.md
  4. 9
      tools/homeassistant/README.md
  5. 4
      tools/homeassistant/autodiscovery.yaml
  6. 23
      tools/homeassistant/manual.yaml
  7. 19
      tools/rpi/hoymiles/__init__.py
  8. 56
      tools/rpi/hoymiles/__main__.py
  9. 46
      tools/rpi/hoymiles/decoders/__init__.py

2
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

6
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.<br/>
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)

2
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 | | |
| | | | | | | | | |
| | | | | | | | | |

9
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)

4
tools/homeassistant/autodiscovery.yaml

@ -0,0 +1,4 @@
mqtt:
broker: http://<IP des Brokers>
discovery: true
discovery_prefix: inverter

23
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"

19
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())
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''

56
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)

46
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 """

Loading…
Cancel
Save