mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-21 12:56:11 +02:00
Merge branch 'main' into asyncWeb03
This commit is contained in:
commit
12d8ff5949
9 changed files with 144 additions and 23 deletions
|
@ -13,7 +13,7 @@ List of approaches
|
||||||
- [Others, C/C++](tools/nano/NRF24_SendRcv/)
|
- [Others, C/C++](tools/nano/NRF24_SendRcv/)
|
||||||
|
|
||||||
## Quick Start with ESP8266
|
## 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
|
## Success Stories
|
||||||
|
|
|
@ -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/>
|
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.
|
Further information will help you to communicate to the compatible inverters.
|
||||||
|
|
||||||
|
You find the full [User_Manual here](User_Manual.md)
|
||||||
|
|
||||||
## Compatiblity
|
## Compatiblity
|
||||||
|
|
||||||
For now the following Inverters should work out of the box:
|
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
|
## 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
|
## Used Libraries
|
||||||
|
|
||||||
|
@ -258,4 +260,4 @@ We run a Discord Server that can be used to get in touch with the Developers and
|
||||||
|
|
||||||
## ToDo
|
## ToDo
|
||||||
|
|
||||||
[See this post](https://github.com/grindylow/ahoy/issues/142)
|
[See this post](https://github.com/lumapu/ahoy/issues/142)
|
||||||
|
|
|
@ -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 | | |
|
| setje | HM-600 | | 1.0.08 | 2020 | 07-10 | 104 | | |
|
||||||
| madmartin | HM-600 | 0.1.4 | 1.0.10 | 2021 | 11-01 | 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 | | | |
|
| 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
Normal file
9
tools/homeassistant/README.md
Normal file
|
@ -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
Normal file
4
tools/homeassistant/autodiscovery.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
mqtt:
|
||||||
|
broker: http://<IP des Brokers>
|
||||||
|
discovery: true
|
||||||
|
discovery_prefix: inverter
|
23
tools/homeassistant/manual.yaml
Normal file
23
tools/homeassistant/manual.yaml
Normal file
|
@ -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"
|
|
@ -482,21 +482,24 @@ def compose_esb_packet(packet, mtu=17, **params):
|
||||||
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
fragment = compose_esb_fragment(packet[i:i+mtu], **params)
|
||||||
yield fragment
|
yield fragment
|
||||||
|
|
||||||
def compose_set_time_payload(timestamp=None):
|
def compose_send_time_payload(cmdId, alarm_id=0):
|
||||||
"""
|
"""
|
||||||
Build set time request packet
|
Build set time request packet
|
||||||
|
|
||||||
:param timestamp: time to set (default: int(time.time()) )
|
:param cmd to request
|
||||||
:type timestamp: int
|
:type cmd: uint8
|
||||||
:return: payload
|
:return: payload
|
||||||
:rtype: bytes
|
:rtype: bytes
|
||||||
"""
|
"""
|
||||||
if not timestamp:
|
timestamp = int(time.time())
|
||||||
timestamp = int(time.time())
|
|
||||||
|
|
||||||
payload = b'\x0b\x00'
|
# indices from esp8266 hmRadio.h / sendTimePacket()
|
||||||
payload = payload + struct.pack('>L', timestamp) # big-endian: msb at low address
|
payload = struct.pack('>B', cmdId) # 10
|
||||||
payload = payload + b'\x00\x00\x00\x05\x00\x00\x00\x00'
|
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)
|
return frame_payload(payload)
|
||||||
|
|
||||||
|
@ -649,7 +652,7 @@ class InverterTransaction:
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
seq_last = max(frames, key=lambda frame:frame.seq).seq if len(frames) else 0
|
seq_last = max(frames, key=lambda frame:frame.seq).seq if len(frames) else 0
|
||||||
self.__retransmit_frame(seq_last + 1)
|
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
|
# Rebuild payload from unordered frames
|
||||||
payload = b''
|
payload = b''
|
||||||
|
|
|
@ -7,6 +7,7 @@ Hoymiles micro-inverters main application
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import struct
|
import struct
|
||||||
|
from enum import IntEnum
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -16,7 +17,7 @@ from yaml.loader import SafeLoader
|
||||||
import paho.mqtt.client
|
import paho.mqtt.client
|
||||||
import hoymiles
|
import hoymiles
|
||||||
|
|
||||||
def main_loop():
|
def main_loop(do_init):
|
||||||
"""Main loop"""
|
"""Main loop"""
|
||||||
inverters = [
|
inverters = [
|
||||||
inverter for inverter in ahoy_config.get('inverters', [])
|
inverter for inverter in ahoy_config.get('inverters', [])
|
||||||
|
@ -25,9 +26,29 @@ def main_loop():
|
||||||
for inverter in inverters:
|
for inverter in inverters:
|
||||||
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
print(f'Poll inverter {inverter["serial"]}')
|
print(f'Poll inverter {inverter["serial"]}')
|
||||||
poll_inverter(inverter)
|
poll_inverter(inverter, do_init)
|
||||||
|
|
||||||
def poll_inverter(inverter, retries=4):
|
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
|
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')
|
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
|
||||||
|
|
||||||
# Queue at least status data request
|
# 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
|
# Put all queued commands for current inverter on air
|
||||||
while len(command_queue[str(inverter_ser)]) > 0:
|
while len(command_queue[inv_str]) > 0:
|
||||||
payload = command_queue[str(inverter_ser)].pop(0)
|
payload = command_queue[inv_str].pop(0)
|
||||||
|
|
||||||
# Send payload {ttl}-times until we get at least one reponse
|
# Send payload {ttl}-times until we get at least one reponse
|
||||||
payload_ttl = retries
|
payload_ttl = retries
|
||||||
|
@ -95,6 +120,11 @@ def poll_inverter(inverter, retries=4):
|
||||||
string_id = string_id + 1
|
string_id = string_id + 1
|
||||||
print()
|
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:
|
if mqtt_client:
|
||||||
mqtt_send_status(mqtt_client, inverter_ser, data,
|
mqtt_send_status(mqtt_client, inverter_ser, data,
|
||||||
topic=inverter.get('mqtt', {}).get('topic', None))
|
topic=inverter.get('mqtt', {}).get('topic', None))
|
||||||
|
@ -219,6 +249,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
mqtt_client = None
|
mqtt_client = None
|
||||||
|
|
||||||
|
event_message_index = {}
|
||||||
command_queue = {}
|
command_queue = {}
|
||||||
mqtt_command_topic_subs = []
|
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', [])]
|
g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])]
|
||||||
for g_inverter in ahoy_config.get('inverters', []):
|
for g_inverter in ahoy_config.get('inverters', []):
|
||||||
g_inverter_ser = g_inverter.get('serial')
|
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
|
# Enables and subscribe inverter to mqtt /command-Topic
|
||||||
|
@ -276,10 +309,13 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
loop_interval = ahoy_config.get('interval', 1)
|
loop_interval = ahoy_config.get('interval', 1)
|
||||||
try:
|
try:
|
||||||
|
do_init = True
|
||||||
while True:
|
while True:
|
||||||
t_loop_start = time.time()
|
t_loop_start = time.time()
|
||||||
|
|
||||||
main_loop()
|
main_loop(do_init)
|
||||||
|
|
||||||
|
do_init = False
|
||||||
|
|
||||||
print('', end='', flush=True)
|
print('', end='', flush=True)
|
||||||
|
|
||||||
|
|
|
@ -294,10 +294,12 @@ class EventsResponse(UnknownResponse):
|
||||||
|
|
||||||
crc_valid = self.validate_crc_m()
|
crc_valid = self.validate_crc_m()
|
||||||
if crc_valid:
|
if crc_valid:
|
||||||
print(' payload has valid modbus crc')
|
#print(' payload has valid modbus crc')
|
||||||
self.response = self.response[:-2]
|
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
|
chunk_size = 12
|
||||||
for i_chunk in range(2, len(self.response), chunk_size):
|
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(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
|
||||||
print(end='', flush=True)
|
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):
|
class DebugDecodeAny(UnknownResponse):
|
||||||
"""Default decoder"""
|
"""Default decoder"""
|
||||||
|
|
||||||
|
@ -359,6 +383,9 @@ class DebugDecodeAny(UnknownResponse):
|
||||||
|
|
||||||
|
|
||||||
# 1121-Series Intervers, 1 MPPT, 1 Phase
|
# 1121-Series Intervers, 1 MPPT, 1 Phase
|
||||||
|
class Hm300Decode01(HardwareInfoResponse):
|
||||||
|
""" Firmware version / date """
|
||||||
|
|
||||||
class Hm300Decode02(EventsResponse):
|
class Hm300Decode02(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
@ -407,6 +434,9 @@ class Hm300Decode0B(StatusResponse):
|
||||||
""" Inverter temperature in °C """
|
""" Inverter temperature in °C """
|
||||||
return self.unpack('>H', 26)[0]/10
|
return self.unpack('>H', 26)[0]/10
|
||||||
|
|
||||||
|
class Hm300Decode0C(Hm300Decode0B):
|
||||||
|
""" 1121-series mirco-inverters status data """
|
||||||
|
|
||||||
class Hm300Decode11(EventsResponse):
|
class Hm300Decode11(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
@ -415,6 +445,9 @@ class Hm300Decode12(EventsResponse):
|
||||||
|
|
||||||
|
|
||||||
# 1141-Series Inverters, 2 MPPT, 1 Phase
|
# 1141-Series Inverters, 2 MPPT, 1 Phase
|
||||||
|
class Hm600Decode01(HardwareInfoResponse):
|
||||||
|
""" Firmware version / date """
|
||||||
|
|
||||||
class Hm600Decode02(EventsResponse):
|
class Hm600Decode02(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
@ -492,6 +525,9 @@ class Hm600Decode0B(StatusResponse):
|
||||||
""" Event counter """
|
""" Event counter """
|
||||||
return self.unpack('>H', 40)[0]
|
return self.unpack('>H', 40)[0]
|
||||||
|
|
||||||
|
class Hm600Decode0C(Hm600Decode0B):
|
||||||
|
""" 1141-series mirco-inverters status data """
|
||||||
|
|
||||||
class Hm600Decode11(EventsResponse):
|
class Hm600Decode11(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
@ -500,6 +536,9 @@ class Hm600Decode12(EventsResponse):
|
||||||
|
|
||||||
|
|
||||||
# 1161-Series Inverters, 2 MPPT, 1 Phase
|
# 1161-Series Inverters, 2 MPPT, 1 Phase
|
||||||
|
class Hm1200Decode01(HardwareInfoResponse):
|
||||||
|
""" Firmware version / date """
|
||||||
|
|
||||||
class Hm1200Decode02(EventsResponse):
|
class Hm1200Decode02(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
@ -619,6 +658,9 @@ class Hm1200Decode0B(StatusResponse):
|
||||||
""" Event counter """
|
""" Event counter """
|
||||||
return self.unpack('>H', 60)[0]
|
return self.unpack('>H', 60)[0]
|
||||||
|
|
||||||
|
class Hm1200Decode0C(Hm1200Decode0B):
|
||||||
|
""" 1161-series mirco-inverters status data """
|
||||||
|
|
||||||
class Hm1200Decode11(EventsResponse):
|
class Hm1200Decode11(EventsResponse):
|
||||||
""" Inverter generic events log """
|
""" Inverter generic events log """
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue