Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb793a3f13 | ||
|
|
c3da9d6101 | ||
|
|
0c9f953476 | ||
|
|
658f42d4fe | ||
|
|
870a965c22 | ||
|
|
0c645812bd | ||
|
|
7b71f25496 | ||
|
|
50977d5afd | ||
|
|
ff0979663e | ||
|
|
a6ac9864af | ||
|
|
2e0331cb88 | ||
|
|
ec54e399fb |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -7,12 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.3] - 2023-10-26
|
||||||
|
|
||||||
|
- fix typos by Lenz Grimmer
|
||||||
|
- catch mqtt errors, so we can forward messages to tsun even if the mqtt broker is not reachable
|
||||||
|
- avoid resetting the daily generation counters even if the inverter sends zero values after reconnection
|
||||||
|
|
||||||
## [0.4.2] - 2023-10-21
|
## [0.4.2] - 2023-10-21
|
||||||
|
|
||||||
- count unknown data types in received messages
|
- count unknown data types in received messages
|
||||||
- count defintion errors in our internal tables
|
- count definition errors in our internal tables
|
||||||
- increase test coverage of the Infos class to 100%
|
- increase test coverage of the Infos class to 100%
|
||||||
- avoids resetting the daily generation counters even if the inverter sends zero values at sunset
|
- avoid resetting the daily generation counters even if the inverter sends zero values at sunset
|
||||||
|
|
||||||
## [0.4.1] - 2023-10-20
|
## [0.4.1] - 2023-10-20
|
||||||
|
|
||||||
@@ -111,4 +117,4 @@ This version halves the size of the Docker image and reduces the attack surface
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- First checkin, the project was born
|
- First checkin, the project was born
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ mqtt.passwd = ''
|
|||||||
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
ha.auto_conf_prefix = 'homeassistant' # MQTT prefix for subscribing for homeassistant status updates
|
||||||
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
|
ha.discovery_prefix = 'homeassistant' # MQTT prefix for discovery topic
|
||||||
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
|
ha.entity_prefix = 'tsun' # MQTT topic prefix for publishing inverter values
|
||||||
|
ha.proxy_node_id = 'proxy' # MQTT node id, for the proxy_node_id
|
||||||
|
ha.proxy_unique_id = 'P170000000000001' # MQTT unique id, to identify a proxy instance
|
||||||
|
|
||||||
|
|
||||||
# microinverters
|
# microinverters
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class AsyncStream(Message):
|
|||||||
|
|
||||||
except (ConnectionResetError,
|
except (ConnectionResetError,
|
||||||
ConnectionAbortedError,
|
ConnectionAbortedError,
|
||||||
|
BrokenPipeError,
|
||||||
RuntimeError) as error:
|
RuntimeError) as error:
|
||||||
logger.warning(f'In loop for {self.addr}: {error}')
|
logger.warning(f'In loop for {self.addr}: {error}')
|
||||||
self.close()
|
self.close()
|
||||||
|
|||||||
@@ -341,7 +341,14 @@ class Infos:
|
|||||||
dict = dict[key]
|
dict = dict[key]
|
||||||
name += key + '.'
|
name += key + '.'
|
||||||
|
|
||||||
update = keys[-1] not in dict or (not must_incr and dict[keys[-1]] != result) or (must_incr and dict[keys[-1]] < result)
|
if keys[-1] not in dict:
|
||||||
|
update = (not must_incr or result>0)
|
||||||
|
else:
|
||||||
|
if must_incr:
|
||||||
|
update = dict[keys[-1]] < result
|
||||||
|
else:
|
||||||
|
update = dict[keys[-1]] != result
|
||||||
|
|
||||||
if update: dict[keys[-1]] = result
|
if update: dict[keys[-1]] = result
|
||||||
name += keys[-1]
|
name += keys[-1]
|
||||||
yield keys[0], update
|
yield keys[0], update
|
||||||
@@ -349,7 +356,7 @@ class Infos:
|
|||||||
update = False
|
update = False
|
||||||
name = str(f'info-id.0x{info_id:x}')
|
name = str(f'info-id.0x{info_id:x}')
|
||||||
|
|
||||||
self.tracer.log(level, f'{name} : {result}{unit}')
|
self.tracer.log(level, f'{name} : {result}{unit} update: {update}')
|
||||||
|
|
||||||
i +=1
|
i +=1
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import asyncio, logging, traceback, json
|
|||||||
from config import Config
|
from config import Config
|
||||||
from async_stream import AsyncStream
|
from async_stream import AsyncStream
|
||||||
from mqtt import Mqtt
|
from mqtt import Mqtt
|
||||||
|
from aiomqtt import MqttCodeError
|
||||||
|
|
||||||
#import gc
|
#import gc
|
||||||
|
|
||||||
#logger = logging.getLogger('conn')
|
#logger = logging.getLogger('conn')
|
||||||
@@ -34,8 +36,9 @@ class Inverter(AsyncStream):
|
|||||||
if self.remoteStream:
|
if self.remoteStream:
|
||||||
logging.debug ("disconnect client connection")
|
logging.debug ("disconnect client connection")
|
||||||
self.remoteStream.disc()
|
self.remoteStream.disc()
|
||||||
|
try:
|
||||||
await self.__async_publ_mqtt_packet('proxy')
|
await self.__async_publ_mqtt_packet('proxy')
|
||||||
|
except: pass
|
||||||
|
|
||||||
async def client_loop(self, addr):
|
async def client_loop(self, addr):
|
||||||
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
@@ -74,14 +77,22 @@ class Inverter(AsyncStream):
|
|||||||
async def async_publ_mqtt(self) -> None:
|
async def async_publ_mqtt(self) -> None:
|
||||||
'''puplish data to MQTT broker'''
|
'''puplish data to MQTT broker'''
|
||||||
# check if new inverter or collector infos are available or when the home assistant has changed the status back to online
|
# check if new inverter or collector infos are available or when the home assistant has changed the status back to online
|
||||||
if (('inverter' in self.new_data and self.new_data['inverter']) or
|
try:
|
||||||
('collector' in self.new_data and self.new_data['collector']) or
|
if (('inverter' in self.new_data and self.new_data['inverter']) or
|
||||||
self.mqtt.ha_restarts != self.ha_restarts):
|
('collector' in self.new_data and self.new_data['collector']) or
|
||||||
await self.__register_home_assistant()
|
self.mqtt.ha_restarts != self.ha_restarts):
|
||||||
self.ha_restarts = self.mqtt.ha_restarts
|
await self.__register_home_assistant()
|
||||||
|
self.ha_restarts = self.mqtt.ha_restarts
|
||||||
|
|
||||||
for key in self.new_data:
|
for key in self.new_data:
|
||||||
await self.__async_publ_mqtt_packet(key)
|
await self.__async_publ_mqtt_packet(key)
|
||||||
|
except MqttCodeError as error:
|
||||||
|
logging.error(f'Mqtt except: {error}')
|
||||||
|
except Exception:
|
||||||
|
logging.error(
|
||||||
|
f"Inverter: Exception:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
|
||||||
async def __async_publ_mqtt_packet(self, key):
|
async def __async_publ_mqtt_packet(self, key):
|
||||||
db = self.db.db
|
db = self.db.db
|
||||||
@@ -101,14 +112,9 @@ class Inverter(AsyncStream):
|
|||||||
|
|
||||||
async def __register_home_assistant(self) -> None:
|
async def __register_home_assistant(self) -> None:
|
||||||
'''register all our topics at home assistant'''
|
'''register all our topics at home assistant'''
|
||||||
try:
|
for data_json, component, node_id, id in self.db.ha_confs(self.entity_prfx, self.node_id, self.unique_id, self.proxy_node_id, self.proxy_unique_id, self.sug_area):
|
||||||
for data_json, component, node_id, id in self.db.ha_confs(self.entity_prfx, self.node_id, self.unique_id, self.proxy_node_id, self.proxy_unique_id, self.sug_area):
|
logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}")
|
||||||
logger_mqtt.debug(f"MQTT Register: cmp:'{component}' node_id:'{node_id}' {data_json}")
|
await self.mqtt.publish(f"{self.discovery_prfx}{component}/{node_id}{id}/config", data_json)
|
||||||
await self.mqtt.publish(f"{self.discovery_prfx}{component}/{node_id}{id}/config", data_json)
|
|
||||||
except Exception:
|
|
||||||
logging.error(
|
|
||||||
f"Inverter: Exception:\n"
|
|
||||||
f"{traceback.format_exc()}")
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
logging.debug(f'Inverter.close() {self.addr}')
|
logging.debug(f'Inverter.close() {self.addr}')
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def InvDataSeq2_Zero(): # Data indication from the controller
|
|||||||
msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
|
msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53'
|
||||||
msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
|
msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca'
|
||||||
msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
|
msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00'
|
||||||
msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06'
|
msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x00\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x00\x00\x00\x00\x06'
|
||||||
msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x00\x00\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
|
msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x00\x00\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00'
|
||||||
msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
|
msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x00\x00\x00\x00\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4'
|
||||||
msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x00\x00\x00\x00'
|
msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x00\x00\x00\x00'
|
||||||
@@ -206,26 +206,87 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
|
|||||||
if key == 'total':
|
if key == 'total':
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
pass
|
elif key == 'env':
|
||||||
elif key == 'input':
|
|
||||||
assert update == True
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==22
|
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}})
|
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
||||||
|
tests = 0
|
||||||
|
for key, update in i.parse (InvDataSeq2):
|
||||||
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
||||||
|
|
||||||
tests = 0
|
tests = 0
|
||||||
for key, update in i.parse (InvDataSeq2_Zero):
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
if key == 'total':
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
tests +=1
|
tests +=1
|
||||||
pass
|
elif key == 'env':
|
||||||
elif key == 'input':
|
assert update == True
|
||||||
tests +=1
|
tests +=1
|
||||||
|
|
||||||
assert tests==22
|
assert tests==4
|
||||||
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0, "Daily_Generation": 0.0, "Total_Generation": 0.0}})
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0, "Rated_Power": 0})
|
||||||
|
|
||||||
|
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
|
||||||
|
i = Infos()
|
||||||
|
tests = 0
|
||||||
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == True
|
||||||
|
tests +=1
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
assert json.dumps(i.db['total']) == json.dumps({})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0, "Rated_Power": 0})
|
||||||
|
|
||||||
|
tests = 0
|
||||||
|
for key, update in i.parse (InvDataSeq2_Zero):
|
||||||
|
if key == 'total':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == False
|
||||||
|
tests +=1
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
assert json.dumps(i.db['total']) == json.dumps({})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0, "Rated_Power": 0})
|
||||||
|
|
||||||
|
tests = 0
|
||||||
|
for key, update in i.parse (InvDataSeq2):
|
||||||
|
if key == 'total':
|
||||||
|
assert update == True
|
||||||
|
tests +=1
|
||||||
|
elif key == 'env':
|
||||||
|
assert update == True
|
||||||
|
tests +=1
|
||||||
|
|
||||||
|
assert tests==4
|
||||||
|
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
|
||||||
|
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
|
||||||
|
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23, "Rated_Power": 600})
|
||||||
|
|
||||||
|
|
||||||
def test_statistic_counter():
|
def test_statistic_counter():
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ services:
|
|||||||
mqtt:
|
mqtt:
|
||||||
container_name: mqtt-broker
|
container_name: mqtt-broker
|
||||||
image: eclipse-mosquitto:2
|
image: eclipse-mosquitto:2
|
||||||
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
- 1883
|
- 1883
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user