@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Prepare support of inverters with 6 MTPPs
|
||||
- Clear `Daily Generation` values at midnigth
|
||||
- Read pv module details from config file and use it for the Home Assistant registration
|
||||
see: [#43](https://github.com/s-allius/tsun-gen3-proxy/issues/43)
|
||||
|
||||
## [0.6.0] - 2024-04-02
|
||||
|
||||
|
||||
@@ -128,15 +128,23 @@ inverters.allow_all = false # True: allow inverters, even if we have no invert
|
||||
[inverters."R17xxxxxxxxxxxx1"]
|
||||
node_id = 'inv1' # Optional, MQTT replacement for inverters serial number
|
||||
suggested_area = 'roof' # Optional, suggested installation area for home-assistant
|
||||
pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
[inverters."R17xxxxxxxxxxxx2"]
|
||||
node_id = 'inv2' # Optional, MQTT replacement for inverters serial number
|
||||
suggested_area = 'balcony' # Optional, suggested installation area for home-assistant
|
||||
pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
[inverters."Y17xxxxxxxxxxxx1"]
|
||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||
node_id = 'inv_3' # MQTT replacement for inverters serial number
|
||||
suggested_area = 'garage' # suggested installation place for home-assistant
|
||||
pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -31,13 +31,21 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
|
||||
[inverters."R170000000000001"]
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-395M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
#[inverters."R17xxxxxxxxxxxx2"]
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation area for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-405M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
[inverters."Y170000000000001"]
|
||||
#monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
||||
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
#pv4 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||
|
||||
|
||||
@@ -45,7 +45,31 @@ class Config():
|
||||
if len(s) > 0 and
|
||||
s[-1] != '/' else s)),
|
||||
|
||||
Optional('suggested_area', default=""): Use(str)
|
||||
Optional('suggested_area', default=""): Use(str),
|
||||
Optional('pv1'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
Optional('pv2'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
Optional('pv3'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
Optional('pv4'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
Optional('pv5'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
},
|
||||
Optional('pv6'): {
|
||||
Optional('type'): Use(str),
|
||||
Optional('manufacturer'): Use(str),
|
||||
}
|
||||
}}
|
||||
}, ignore_extra_keys=True
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ class Talent(Message):
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
|
||||
def set_serial_no(self, serial_no: str):
|
||||
def __set_serial_no(self, serial_no: str):
|
||||
|
||||
if self.unique_id == serial_no:
|
||||
logger.debug(f'SerialNo: {serial_no}')
|
||||
@@ -72,6 +72,7 @@ class Talent(Message):
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
self.db.set_pv_module_details(inv)
|
||||
else:
|
||||
self.node_id = ''
|
||||
self.sug_area = ''
|
||||
@@ -95,7 +96,7 @@ class Talent(Message):
|
||||
hex_dump_memory(logging.INFO, f'Received from {self.addr}:',
|
||||
self._recv_buffer, self.header_len+self.data_len)
|
||||
|
||||
self.set_serial_no(self.id_str.decode("utf-8"))
|
||||
self.__set_serial_no(self.id_str.decode("utf-8"))
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
|
||||
@@ -70,7 +70,7 @@ class SolarmanV5(Message):
|
||||
# deallocated by the garbage collector ==> we get a memory leak
|
||||
self.switch.clear()
|
||||
|
||||
def set_serial_no(self, snr: int):
|
||||
def __set_serial_no(self, snr: int):
|
||||
serial_no = str(snr)
|
||||
if self.unique_id == serial_no:
|
||||
logger.debug(f'SerialNo: {serial_no}')
|
||||
@@ -87,6 +87,7 @@ class SolarmanV5(Message):
|
||||
self.node_id = inv['node_id']
|
||||
self.sug_area = inv['suggested_area']
|
||||
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
|
||||
self.db.set_pv_module_details(inv)
|
||||
|
||||
if not found:
|
||||
self.node_id = ''
|
||||
@@ -112,7 +113,7 @@ class SolarmanV5(Message):
|
||||
self._recv_buffer, self.header_len+self.data_len+2)
|
||||
if self.__trailer_is_ok(self._recv_buffer, self.header_len
|
||||
+ self.data_len + 2):
|
||||
self.set_serial_no(self.snr)
|
||||
self.__set_serial_no(self.snr)
|
||||
self.__dispatch_msg()
|
||||
self.__flush_recv_msg()
|
||||
return
|
||||
@@ -352,9 +353,9 @@ class SolarmanV5(Message):
|
||||
ftype = result[0] # always 2
|
||||
valid = result[1] == 1 # status
|
||||
ts = result[2]
|
||||
repeat = result[3] # always 60
|
||||
set_hb = result[3] # always 60 or 120
|
||||
logger.info(f'ftype:{ftype} accepted:{valid}'
|
||||
f' ts:{ts:08x} repeat:{repeat}s')
|
||||
f' ts:{ts:08x} nextHeartbeat: {set_hb}s')
|
||||
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
logger.info(f'ts: {dt.strftime("%Y-%m-%d %H:%M:%S")}')
|
||||
|
||||
@@ -34,21 +34,33 @@ class Register(Enum):
|
||||
PV1_VOLTAGE = 100
|
||||
PV1_CURRENT = 101
|
||||
PV1_POWER = 102
|
||||
PV1_MANUFACTURER = 103
|
||||
PV1_MODEL = 104
|
||||
PV2_VOLTAGE = 110
|
||||
PV2_CURRENT = 111
|
||||
PV2_POWER = 112
|
||||
PV2_MANUFACTURER = 113
|
||||
PV2_MODEL = 114
|
||||
PV3_VOLTAGE = 120
|
||||
PV3_CURRENT = 121
|
||||
PV3_POWER = 122
|
||||
PV3_MANUFACTURER = 123
|
||||
PV3_MODEL = 124
|
||||
PV4_VOLTAGE = 130
|
||||
PV4_CURRENT = 131
|
||||
PV4_POWER = 132
|
||||
PV4_MANUFACTURER = 133
|
||||
PV4_MODEL = 134
|
||||
PV5_VOLTAGE = 140
|
||||
PV5_CURRENT = 141
|
||||
PV5_POWER = 142
|
||||
PV5_MANUFACTURER = 143
|
||||
PV5_MODEL = 144
|
||||
PV6_VOLTAGE = 150
|
||||
PV6_CURRENT = 151
|
||||
PV6_POWER = 152
|
||||
PV6_MANUFACTURER = 153
|
||||
PV6_MODEL = 154
|
||||
PV1_DAILY_GENERATION = 200
|
||||
PV1_TOTAL_GENERATION = 201
|
||||
PV2_DAILY_GENERATION = 210
|
||||
@@ -111,7 +123,7 @@ class ClrAtMidnight:
|
||||
dict = dict[prfx]
|
||||
|
||||
for key in keys[1:-1]:
|
||||
if key not in dict: # pragma: no cover
|
||||
if key not in dict:
|
||||
dict[key] = {}
|
||||
dict = dict[key]
|
||||
dict[keys[-1]] = 0
|
||||
@@ -153,12 +165,12 @@ class Infos:
|
||||
'proxy': {'singleton': True, 'name': 'Proxy', 'mf': 'Stefan Allius'}, # noqa: E501
|
||||
'controller': {'via': 'proxy', 'name': 'Controller', 'mdl': Register.CHIP_MODEL, 'mf': Register.CHIP_TYPE, 'sw': Register.COLLECTOR_FW_VERSION}, # noqa: E501
|
||||
'inverter': {'via': 'controller', 'name': 'Micro Inverter', 'mdl': Register.EQUIPMENT_MODEL, 'mf': Register.MANUFACTURER, 'sw': Register.VERSION}, # noqa: E501
|
||||
'input_pv1': {'via': 'inverter', 'name': 'Module PV1'},
|
||||
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
||||
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501
|
||||
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
|
||||
'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501
|
||||
'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501
|
||||
'input_pv1': {'via': 'inverter', 'name': 'Module PV1', 'mdl': Register.PV1_MODEL, 'mf': Register.PV1_MANUFACTURER}, # noqa: E501
|
||||
'input_pv2': {'via': 'inverter', 'name': 'Module PV2', 'mdl': Register.PV2_MODEL, 'mf': Register.PV2_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 2}}, # noqa: E501
|
||||
'input_pv3': {'via': 'inverter', 'name': 'Module PV3', 'mdl': Register.PV3_MODEL, 'mf': Register.PV3_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 3}}, # noqa: E501
|
||||
'input_pv4': {'via': 'inverter', 'name': 'Module PV4', 'mdl': Register.PV4_MODEL, 'mf': Register.PV4_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 4}}, # noqa: E501
|
||||
'input_pv5': {'via': 'inverter', 'name': 'Module PV5', 'mdl': Register.PV5_MODEL, 'mf': Register.PV5_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 5}}, # noqa: E501
|
||||
'input_pv6': {'via': 'inverter', 'name': 'Module PV6', 'mdl': Register.PV6_MODEL, 'mf': Register.PV6_MANUFACTURER, 'dep': {'reg': Register.NO_INPUTS, 'gte': 6}}, # noqa: E501
|
||||
}
|
||||
|
||||
__comm_type_val_tpl = "{%set com_types = ['n/a','Wi-Fi', 'G4', 'G5', 'GPRS'] %}{{com_types[value_json['Communication_Type']|int(0)]|default(value_json['Communication_Type'])}}" # noqa: E501
|
||||
@@ -181,6 +193,19 @@ class Infos:
|
||||
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'fmt': '| string + " W"', 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'fmt': '| string + " W"', 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
|
||||
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV2_MODEL: {'name': ['inverter', 'PV2_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV3_MANUFACTURER: {'name': ['inverter', 'PV3_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV3_MODEL: {'name': ['inverter', 'PV3_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV4_MANUFACTURER: {'name': ['inverter', 'PV4_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV4_MODEL: {'name': ['inverter', 'PV4_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV5_MANUFACTURER: {'name': ['inverter', 'PV5_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV5_MODEL: {'name': ['inverter', 'PV5_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MANUFACTURER: {'name': ['inverter', 'PV6_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
|
||||
|
||||
# proxy:
|
||||
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501
|
||||
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501
|
||||
@@ -518,3 +543,20 @@ class Infos:
|
||||
elif 'less_eq' in dep:
|
||||
return not value <= dep['less_eq']
|
||||
return True
|
||||
|
||||
def set_pv_module_details(self, inv: dict) -> None:
|
||||
map = {'pv1': {'manufacturer': Register.PV1_MANUFACTURER, 'model': Register.PV1_MODEL}, # noqa: E501
|
||||
'pv2': {'manufacturer': Register.PV2_MANUFACTURER, 'model': Register.PV2_MODEL}, # noqa: E501
|
||||
'pv3': {'manufacturer': Register.PV3_MANUFACTURER, 'model': Register.PV3_MODEL}, # noqa: E501
|
||||
'pv4': {'manufacturer': Register.PV4_MANUFACTURER, 'model': Register.PV4_MODEL}, # noqa: E501
|
||||
'pv5': {'manufacturer': Register.PV5_MANUFACTURER, 'model': Register.PV5_MODEL}, # noqa: E501
|
||||
'pv6': {'manufacturer': Register.PV6_MANUFACTURER, 'model': Register.PV6_MODEL} # noqa: E501
|
||||
}
|
||||
|
||||
for key, reg in map.items():
|
||||
if key in inv:
|
||||
if 'manufacturer' in inv[key]:
|
||||
self.set_db_def_value(reg['manufacturer'],
|
||||
inv[key]['manufacturer'])
|
||||
if 'type' in inv[key]:
|
||||
self.set_db_def_value(reg['model'], inv[key]['type'])
|
||||
|
||||
@@ -530,12 +530,18 @@ def test_clr_at_midnight():
|
||||
i.set_db_def_value(Register.NO_INPUTS, 2)
|
||||
val = i.dev_value(Register.NO_INPUTS) # valid addr but not initiliazed
|
||||
assert val == 2
|
||||
|
||||
i.info_defs[Register.TEST_REG1] = { # add a entry with incomplete ha definition
|
||||
'name': ['test', 'grp', 'REG_1'], 'ha': {'dev_cla': None }
|
||||
}
|
||||
i.reg_clr_at_midnight('tsun/inv_1/')
|
||||
# tsun/inv_2/input
|
||||
assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0})
|
||||
assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}})
|
||||
|
||||
i.reg_clr_at_midnight('tsun/inv_1/')
|
||||
assert json.dumps(ClrAtMidnight.db['tsun/inv_1/total']) == json.dumps({'Daily_Generation': 0})
|
||||
assert json.dumps(ClrAtMidnight.db['tsun/inv_1/input']) == json.dumps({"pv1": {"Daily_Generation": 0}, "pv2": {"Daily_Generation": 0}})
|
||||
|
||||
test = 0
|
||||
for key, data in ClrAtMidnight.elm():
|
||||
if key == 'tsun/inv_1/total':
|
||||
@@ -546,3 +552,41 @@ def test_clr_at_midnight():
|
||||
test += 1
|
||||
assert test == 2
|
||||
assert json.dumps(ClrAtMidnight.db) == json.dumps({})
|
||||
|
||||
i.reg_clr_at_midnight('tsun/inv_1/')
|
||||
|
||||
|
||||
|
||||
|
||||
def test_pv_module_config():
|
||||
i = InfosG3()
|
||||
# i.set_db_def_value(Register.NO_INPUTS, 2)
|
||||
|
||||
dt = {
|
||||
'pv1':{'manufacturer':'TSUN1','type': 'Module 100W'},
|
||||
'pv2':{'manufacturer':'TSUN2'},
|
||||
'pv3':{'manufacturer':'TSUN3','type': 'Module 300W'},
|
||||
'pv4':{'type': 'Module 400W'},
|
||||
'pv5':{},
|
||||
}
|
||||
i.set_pv_module_details(dt)
|
||||
assert 'TSUN1' == i.dev_value(Register.PV1_MANUFACTURER)
|
||||
assert 'TSUN2' == i.dev_value(Register.PV2_MANUFACTURER)
|
||||
assert 'TSUN3' == i.dev_value(Register.PV3_MANUFACTURER)
|
||||
assert None == i.dev_value(Register.PV4_MANUFACTURER)
|
||||
assert None == i.dev_value(Register.PV5_MANUFACTURER)
|
||||
assert 'Module 100W' == i.dev_value(Register.PV1_MODEL)
|
||||
assert None == i.dev_value(Register.PV2_MODEL)
|
||||
assert 'Module 300W' == i.dev_value(Register.PV3_MODEL)
|
||||
assert 'Module 400W' == i.dev_value(Register.PV4_MODEL)
|
||||
assert None == i.dev_value(Register.PV5_MODEL)
|
||||
|
||||
def test_broken_info_defs():
|
||||
i = InfosG3()
|
||||
val = i.get_db_value(Register.NO_INPUTS, 666)
|
||||
assert val == 666
|
||||
i.info_defs[Register.TEST_REG1] = 'test' # add a string instead of a dict
|
||||
val = i.get_db_value(Register.TEST_REG1, 666)
|
||||
assert val == 666
|
||||
i.set_db_def_value(Register.TEST_REG1, 2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user