From 6c6109d421d8cbf88848f24c43be6ffa3456e7ec Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 18 Oct 2024 23:49:23 +0200 Subject: [PATCH 1/8] update class diagramms --- app/proxy_2.svg | 657 +++++++++++++++++++++-------------------------- app/proxy_2.yuml | 34 +-- app/proxy_3.svg | 364 ++++++++++++++++++++++++++ app/proxy_3.yuml | 42 +++ 4 files changed, 717 insertions(+), 380 deletions(-) create mode 100644 app/proxy_3.svg create mode 100644 app/proxy_3.yuml diff --git a/app/proxy_2.svg b/app/proxy_2.svg index 6a6fb51..232983f 100644 --- a/app/proxy_2.svg +++ b/app/proxy_2.svg @@ -4,429 +4,368 @@ - - + + G - + A0 - - - -You can stick notes -on diagrams too! + + + +Example of +instantiation for a +GEN3 inverter! A1 - -<<AbstractIterMeta>> - - -__iter__() + +<<AbstractIterMeta>> + + +__iter__() A14 - -<<ProtocolIfc>> - -_registry - -close() + +<<ProtocolIfc>> + +_registry + +close() - + A1->A14 - - + + A2 - -InverterG3 - -addr -remote:StreamPtr -local:StreamPtr - -create_remote() -close() - - - -A7 - -AsyncStreamServer - -create_remote - -<async>server_loop() -<async>_async_forward() -<async>publish_outstanding_mqtt() -close() - - - -A2->A7 - - - -local - - - -A8 - -AsyncStreamClient - - -<async>client_loop() -<async>_async_forward()) - - - -A2->A8 - - -remote + +InverterG3 + +addr +remote:StreamPtr +local:StreamPtr + +create_remote() +close() A3 - -InverterG3P - -addr -remote:StreamPtr -local:StreamPtr - -create_remote() -close() + +local:StreamPtr - - -A3->A7 - - - -local - - - -A3->A8 - - -remote + + +A2->A3 + + + A4 - -<<AsyncIfc>> - - -set_node_id() -get_conn_no() -tx_add() -tx_flush() -tx_get() -tx_peek() -tx_log() -tx_clear() -tx_len() -fwd_add() -fwd_log() -rx_get() -rx_peek() -rx_log() -rx_clear() -rx_len() -rx_set_cb() -prot_set_timeout_cb() + +remote:StreamPtr - - -A5 - -AsyncIfcImpl - -fwd_fifo:ByteFifo -tx_fifo:ByteFifo -rx_fifo:ByteFifo -conn_no:Count -node_id -timeout_cb - - - -A4->A5 - - - - - -A6 - -AsyncStream - -reader -writer -addr -r_addr -l_addr - -<async>loop -disc() -close() -healthy() -__async_read() -__async_write() -__async_forward() - - + -A5->A6 - - +A2->A4 + + + - - -A6->A7 - - + + +A8 + +AsyncStreamServer + +create_remote + +<async>server_loop() +<async>_async_forward() +<async>publish_outstanding_mqtt() +close() - - -A6->A8 - - + + +A3->A8 + + + A9 - -Talent - -ifc:AsyncIfc -conn_no -addr -await_conn_resp_cnt -id_str -contact_name -contact_mail -db:InfosG3 -mb:Modbus -switch - -msg_contact_info() -msg_ota_update() -msg_get_time() -msg_collector_data() -msg_inverter_data() -msg_unknown() -healthy() -close() + +AsyncStreamClient + + +<async>client_loop() +<async>_async_forward()) - + + +A4->A9 + + +0..1 + + + +A5 + +<<AsyncIfc>> + + +set_node_id() +get_conn_no() +tx_add() +tx_flush() +tx_get() +tx_peek() +tx_log() +tx_clear() +tx_len() +fwd_add() +fwd_log() +rx_get() +rx_peek() +rx_log() +rx_clear() +rx_len() +rx_set_cb() +prot_set_timeout_cb() + + + +A6 + +AsyncIfcImpl + +fwd_fifo:ByteFifo +tx_fifo:ByteFifo +rx_fifo:ByteFifo +conn_no:Count +node_id +timeout_cb + + + +A5->A6 + + + + + +A7 + +AsyncStream + +reader +writer +addr +r_addr +l_addr + +<async>loop +disc() +close() +healthy() +__async_read() +__async_write() +__async_forward() + + + +A6->A7 + + + + -A9->A2 - - -remote +A7->A8 + + - - -A9->A2 - - - -local - - - -A9->A4 - - -use - - - -A12 - -InfosG3 - - -ha_confs() -parse() - - - -A9->A12 - - + + +A7->A9 + + A10 - -SolarmanV5 - -ifc:AsyncIfc -conn_no -addr -control -serial -snr -db:InfosG3P -mb:Modbus -switch - -msg_unknown() -healthy() -close() + +Talent + +conn_no +addr +await_conn_resp_cnt +id_str +contact_name +contact_mail +db:InfosG3 +mb:Modbus +switch + +msg_contact_info() +msg_ota_update() +msg_get_time() +msg_collector_data() +msg_inverter_data() +msg_unknown() +healthy() +close() - + A10->A3 - - -remote - - - -A10->A3 - - - -local + + + - + A10->A4 - - -use + + +0..1 - - -A13 - -InfosG3P - - -ha_confs() -parse() + + +A12 + +InfosG3 + + +ha_confs() +parse() - - -A10->A13 - - + + +A10->A12 + + A11 - -Infos - -stat -new_stat_data -info_dev - -static_init() -dev_value() -inc_counter() -dec_counter() -ha_proxy_conf -ha_conf -ha_remove -update_db -set_db_def_value -get_db_value -ignore_this_device + +Infos + +stat +new_stat_data +info_dev + +static_init() +dev_value() +inc_counter() +dec_counter() +ha_proxy_conf +ha_conf +ha_remove +update_db +set_db_def_value +get_db_value +ignore_this_device - + A11->A12 - - + + - - -A11->A13 - - + + +A13 + +Message + +server_side:bool +mb:Modbus +ifc:AsyncIfc +node_id +header_valid:bool +header_len +data_len +unique_id +sug_area:str +new_data:dict +state:State +shutdown_started:bool +modbus_elms +mb_timer:Timer +mb_timeout +mb_first_timeout +modbus_polling:bool + +_set_mqtt_timestamp() +_timeout() +_send_modbus_cmd() +<async> end_modbus_cmd() +close() +inc_counter() +dec_counter() + + + +A13->A5 + + +use + + + +A13->A10 + + + + + +A14->A13 + + A15 - -Message - -node_id - -inc_counter() -dec_counter() + +Modbus + +que +snd_handler +rsp_handler +timeout +max_retires +last_xxx +err +retry_cnt +req_pend +tim + +build_msg() +recv_req() +recv_resp() +close() - - -A14->A15 - - - - - -A15->A9 - - - - - -A15->A10 - - - - - -A16 - -Modbus - -que -snd_handler -rsp_handler -timeout -max_retires -last_xxx -err -retry_cnt -req_pend -tim - -build_msg() -recv_req() -recv_resp() -close() - - - -A16->A9 - - -has -1 - - - -A16->A10 - - -has -1 + + +A15->A13 + + +has +0..1 diff --git a/app/proxy_2.yuml b/app/proxy_2.yuml index 39a399e..5138428 100644 --- a/app/proxy_2.yuml +++ b/app/proxy_2.yuml @@ -2,11 +2,12 @@ // {direction:topDown} // {generate:true} -[note: You can stick notes on diagrams too!{bg:cornsilk}] +[note: Example of instantiation for a GEN3 inverter!{bg:cornsilk}] [<>||__iter__()] [InverterG3|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()] -[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()] +[InverterG3]++->[local:StreamPtr] +[InverterG3]++->[remote:StreamPtr] [<>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()] [AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb] @@ -19,33 +20,24 @@ [AsyncStream]^[AsyncStreamClient] -[Talent|ifc:AsyncIfc;conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()] -[Talent][AsyncStreamClient] -[Talent]<-local++[InverterG3] -[InverterG3]++local->[AsyncStreamServer] - -[SolarmanV5|ifc:AsyncIfc;conn_no;addr;;control;serial;snr;db:InfosG3P;mb:Modbus;switch|msg_unknown();;healthy();close()] -[SolarmanV5][AsyncStreamClient] -[SolarmanV5]<-local++[InverterG3P] -[InverterG3P]++local->[AsyncStreamServer] +[Talent|conn_no;addr;;await_conn_resp_cnt;id_str;contact_name;contact_mail;db:InfosG3;mb:Modbus;switch|msg_contact_info();msg_ota_update();msg_get_time();msg_collector_data();msg_inverter_data();msg_unknown();;healthy();close()] +[Talent]<-++[local:StreamPtr] +[local:StreamPtr]++->[AsyncStreamServer] +[Talent]<-0..1[remote:StreamPtr] +[remote:StreamPtr]0..1->[AsyncStreamClient] [Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device] [Infos]^[InfosG3||ha_confs();parse()] -[Infos]^[InfosG3P||ha_confs();parse()] -[Talent]use->[<>] [Talent]->[InfosG3] -[SolarmanV5]use->[<>] -[SolarmanV5]->[InfosG3P] + +[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd(); end_modbus_cmd();close();inc_counter();dec_counter()] +[Message]use->[<>] [<>|_registry|close()] [<>]^-.-[<>] -[<>]^-.-[Message|node_id|inc_counter();dec_counter()] +[<>]^-.-[Message] [Message]^[Talent] -[Message]^[SolarmanV5] [Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()] -[Modbus]<1-has[SolarmanV5] -[Modbus]<1-has[Talent] +[Modbus]<0..1-has[Message] diff --git a/app/proxy_3.svg b/app/proxy_3.svg new file mode 100644 index 0000000..37fc587 --- /dev/null +++ b/app/proxy_3.svg @@ -0,0 +1,364 @@ + + + + + + +G + + + +A0 + + + +Example of +instantiation for a +GEN3PLUS inverter! + + + +A1 + +<<AbstractIterMeta>> + + +__iter__() + + + +A14 + +<<ProtocolIfc>> + +_registry + +close() + + + +A1->A14 + + + + + +A2 + +InverterG3P + +addr +remote:StreamPtr +local:StreamPtr + +create_remote() +close() + + + +A3 + +local:StreamPtr + + + +A2->A3 + + + + + + +A4 + +remote:StreamPtr + + + +A2->A4 + + + + + + +A8 + +AsyncStreamServer + +create_remote + +<async>server_loop() +<async>_async_forward() +<async>publish_outstanding_mqtt() +close() + + + +A3->A8 + + + + + + +A9 + +AsyncStreamClient + + +<async>client_loop() +<async>_async_forward()) + + + +A4->A9 + + +0..1 + + + +A5 + +<<AsyncIfc>> + + +set_node_id() +get_conn_no() +tx_add() +tx_flush() +tx_get() +tx_peek() +tx_log() +tx_clear() +tx_len() +fwd_add() +fwd_log() +rx_get() +rx_peek() +rx_log() +rx_clear() +rx_len() +rx_set_cb() +prot_set_timeout_cb() + + + +A6 + +AsyncIfcImpl + +fwd_fifo:ByteFifo +tx_fifo:ByteFifo +rx_fifo:ByteFifo +conn_no:Count +node_id +timeout_cb + + + +A5->A6 + + + + + +A7 + +AsyncStream + +reader +writer +addr +r_addr +l_addr + +<async>loop +disc() +close() +healthy() +__async_read() +__async_write() +__async_forward() + + + +A6->A7 + + + + + +A7->A8 + + + + + +A7->A9 + + + + + +A10 + +SolarmanV5 + +conn_no +addr +control +serial +snr +db:InfosG3P +switch + +msg_unknown() +healthy() +close() + + + +A10->A3 + + + + + + +A10->A4 + + +0..1 + + + +A12 + +InfosG3P + + +ha_confs() +parse() + + + +A10->A12 + + + + + +A11 + +Infos + +stat +new_stat_data +info_dev + +static_init() +dev_value() +inc_counter() +dec_counter() +ha_proxy_conf +ha_conf +ha_remove +update_db +set_db_def_value +get_db_value +ignore_this_device + + + +A11->A12 + + + + + +A13 + +Message + +server_side:bool +mb:Modbus +ifc:AsyncIfc +node_id +header_valid:bool +header_len +data_len +unique_id +sug_area:str +new_data:dict +state:State +shutdown_started:bool +modbus_elms +mb_timer:Timer +mb_timeout +mb_first_timeout +modbus_polling:bool + +_set_mqtt_timestamp() +_timeout() +_send_modbus_cmd() +<async> end_modbus_cmd() +close() +inc_counter() +dec_counter() + + + +A13->A5 + + +use + + + +A13->A10 + + + + + +A14->A13 + + + + + +A15 + +Modbus + +que +snd_handler +rsp_handler +timeout +max_retires +last_xxx +err +retry_cnt +req_pend +tim + +build_msg() +recv_req() +recv_resp() +close() + + + +A15->A13 + + +has +0..1 + + + diff --git a/app/proxy_3.yuml b/app/proxy_3.yuml new file mode 100644 index 0000000..499c93f --- /dev/null +++ b/app/proxy_3.yuml @@ -0,0 +1,42 @@ +// {type:class} +// {direction:topDown} +// {generate:true} + +[note: Example of instantiation for a GEN3PLUS inverter!{bg:cornsilk}] +[<>||__iter__()] + +[InverterG3P|addr;remote:StreamPtr;local:StreamPtr|create_remote();;close()] +[InverterG3P]++->[local:StreamPtr] +[InverterG3P]++->[remote:StreamPtr] + +[<>||set_node_id();get_conn_no();;tx_add();tx_flush();tx_get();tx_peek();tx_log();tx_clear();tx_len();;fwd_add();fwd_log();rx_get();rx_peek();rx_log();rx_clear();rx_len();rx_set_cb();;prot_set_timeout_cb()] +[AsyncIfcImpl|fwd_fifo:ByteFifo;tx_fifo:ByteFifo;rx_fifo:ByteFifo;conn_no:Count;node_id;timeout_cb] +[AsyncStream|reader;writer;addr;r_addr;l_addr|;loop;disc();close();healthy();;__async_read();__async_write();__async_forward()] +[AsyncStreamServer|create_remote|server_loop();_async_forward();publish_outstanding_mqtt();close()] +[AsyncStreamClient||client_loop();_async_forward())] +[<>]^-.-[AsyncIfcImpl] +[AsyncIfcImpl]^[AsyncStream] +[AsyncStream]^[AsyncStreamServer] +[AsyncStream]^[AsyncStreamClient] + +[SolarmanV5|conn_no;addr;;control;serial;snr;db:InfosG3P;switch|msg_unknown();;healthy();close()] +[SolarmanV5]<-++[local:StreamPtr] +[local:StreamPtr]++->[AsyncStreamServer] +[SolarmanV5]<-0..1[remote:StreamPtr] +[remote:StreamPtr]0..1->[AsyncStreamClient] + +[Infos|stat;new_stat_data;info_dev|static_init();dev_value();inc_counter();dec_counter();ha_proxy_conf;ha_conf;ha_remove;update_db;set_db_def_value;get_db_value;ignore_this_device] +[Infos]^[InfosG3P||ha_confs();parse()] + +[SolarmanV5]->[InfosG3P] + +[Message|server_side:bool;mb:Modbus;ifc:AsyncIfc;node_id;header_valid:bool;header_len;data_len;unique_id;sug_area:str;new_data:dict;state:State;shutdown_started:bool;modbus_elms;mb_timer:Timer;mb_timeout;mb_first_timeout;modbus_polling:bool|_set_mqtt_timestamp();_timeout();_send_modbus_cmd(); end_modbus_cmd();close();inc_counter();dec_counter()] +[Message]use->[<>] + +[<>|_registry|close()] +[<>]^-.-[<>] +[<>]^-.-[Message] +[Message]^[SolarmanV5] + +[Modbus|que;;snd_handler;rsp_handler;timeout;max_retires;last_xxx;err;retry_cnt;req_pend;tim|build_msg();recv_req();recv_resp();close()] +[Modbus]<0..1-has[Message] From 9eb7c7fbe082dd9d49964c675dc3d5dbfd8936e2 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 19 Oct 2024 01:23:16 +0200 Subject: [PATCH 2/8] increase test coverage --- app/tests/test_async_stream.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/tests/test_async_stream.py b/app/tests/test_async_stream.py index d1d5911..3aaf35e 100644 --- a/app/tests/test_async_stream.py +++ b/app/tests/test_async_stream.py @@ -340,6 +340,7 @@ def create_remote(remote, test_type, with_close_hdr:bool = False): elif test_type == TestType.FWD_RUNTIME_ERROR_NO_STREAM: remote.stream = None raise RuntimeError("Peer closed") + return True def close(): return @@ -533,3 +534,39 @@ async def test_forward_runtime_error3(): await ifc.server_loop() assert cnt == 1 del ifc + +@pytest.mark.asyncio +async def test_forward_resp(): + assert asyncio.get_running_loop() + remote = StreamPtr(None) + cnt = 0 + + async def _close_cb(): + nonlocal cnt, remote, ifc + cnt += 1 + + cnt = 0 + ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), remote, _close_cb) + create_remote(remote, TestType.FWD_NO_EXCPT) + ifc.fwd_add(b'test-forward_msg') + await ifc.client_loop('') + assert cnt == 0 + del ifc + +@pytest.mark.asyncio +async def test_forward_resp2(): + assert asyncio.get_running_loop() + remote = StreamPtr(None) + cnt = 0 + + async def _close_cb(): + nonlocal cnt, remote, ifc + cnt += 1 + + cnt = 0 + ifc = AsyncStreamClient(fake_reader_fwd(), FakeWriter(), None, _close_cb) + create_remote(remote, TestType.FWD_NO_EXCPT) + ifc.fwd_add(b'test-forward_msg') + await ifc.client_loop('') + assert cnt == 0 + del ifc From 8d67f1745d23634a32cf76e94f95ad2770504249 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 25 Oct 2024 20:36:53 +0200 Subject: [PATCH 3/8] update SonarSource/sonarcloud-github-action --- .github/workflows/python-app.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1a9a38d..3661e5a 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -33,15 +33,6 @@ jobs: runs-on: ubuntu-latest steps: - # - name: Start Mosquitto - # uses: namoshek/mosquitto-github-action@v1 - # with: - # version: '1.6' - # ports: '1883:1883 8883:8883' - # certificates: ${{ github.workspace }}/.ci/tls-certificates - # config: ${{ github.workspace }}/.ci/mosquitto.conf - # password-file: ${{ github.workspace}}/.ci/mosquitto.passwd - # container-name: 'mqtt' - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis @@ -65,7 +56,7 @@ jobs: python -m pytest app --cov=app/src --cov-report=xml coverage report - name: Analyze with SonarCloud - uses: SonarSource/sonarcloud-github-action@v2.2.0 + uses: SonarSource/sonarcloud-github-action@v3.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 10a18237c7cd4d2516fa466f2f5dd071d4bb1d94 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 25 Oct 2024 21:38:36 +0200 Subject: [PATCH 4/8] replace some eval calls --- app/src/gen3plus/infos_g3p.py | 8 ++++++-- app/tests/test_infos_g3p.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index f31f17b..25eb86c 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -15,7 +15,7 @@ class RegisterMap: map = { # 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': ' Date: Fri, 25 Oct 2024 23:41:25 +0200 Subject: [PATCH 5/8] remove all eval() calls --- app/src/gen3/infos_g3.py | 7 ++----- app/src/gen3plus/infos_g3p.py | 32 +++++++------------------------- app/src/infos.py | 35 +++++++++++++++++++++++++++++++++++ app/src/modbus.py | 21 +++++---------------- app/tests/test_infos_g3.py | 12 ------------ app/tests/test_infos_g3p.py | 12 +++++++----- 6 files changed, 56 insertions(+), 63 deletions(-) diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 7c62eac..88f9207 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -183,11 +183,8 @@ class InfosG3(Infos): i += 1 def __modify_val(self, row, result): - if row: - if 'eval' in row: - result = eval(row['eval']) - if 'ratio' in row: - result = round(result * row['ratio'], 2) + if row and 'ratio' in row: + result = round(result * row['ratio'], 2) return result def __store_result(self, addr, result, info_id, node_id): diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 25eb86c..58c53c9 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -1,11 +1,10 @@ -import struct from typing import Generator if __name__ == "app.src.gen3plus.infos_g3p": - from app.src.infos import Infos, Register, ProxyMode + from app.src.infos import Infos, Register, ProxyMode, Fmt else: # pragma: no cover - from infos import Infos, Register, ProxyMode + from infos import Infos, Register, ProxyMode, Fmt class RegisterMap: @@ -19,16 +18,16 @@ class RegisterMap: 0x4102001a: {'reg': Register.HEARTBEAT_INTERVAL, 'fmt': '>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501 + 0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501 0x420100d2: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x420100d4: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x420100d6: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 @@ -123,7 +122,7 @@ class InfosG3P(Infos): if not isinstance(row, dict): continue info_id = row['reg'] - result = self.__get_value(buf, addr, row) + result = Fmt.get_value(buf, addr, row) keys, level, unit, must_incr = self._key_obj(info_id) @@ -137,20 +136,3 @@ class InfosG3P(Infos): if update: self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}' f' : {result}{unit}') - - def __get_value(self, buf, idx, row): - '''Get a value from buf and interpret as in row''' - fmt = row['fmt'] - res = struct.unpack_from(fmt, buf, idx) - result = res[0] - if isinstance(result, (bytearray, bytes)): - result = result.decode().split('\x00')[0] - if 'eval' in row: - result = eval(row['eval']) - if 'ratio' in row: - result = round(result * row['ratio'], 2) - if 'quotient' in row: - result = round(result/row['quotient']) - if 'offset' in row: - result = result + row['offset'] - return result diff --git a/app/src/infos.py b/app/src/infos.py index 88e15c8..c94e219 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -1,5 +1,6 @@ import logging import json +import struct import os from enum import Enum from typing import Generator @@ -123,6 +124,40 @@ class Register(Enum): TEST_REG2 = 10001 +class Fmt: + @staticmethod + def get_value(buf: bytes, idx: int, row: dict): + '''Get a value from buf and interpret as in row defined''' + fmt = row['fmt'] + res = struct.unpack_from(fmt, buf, idx) + result = res[0] + if isinstance(result, (bytearray, bytes)): + result = result.decode().split('\x00')[0] + if 'func' in row: + result = row['func'](res) + if 'ratio' in row: + result = round(result * row['ratio'], 2) + if 'quotient' in row: + result = round(result/row['quotient']) + if 'offset' in row: + result = result + row['offset'] + return result + + @staticmethod + def hex4(val): + return f'{val[0]:04x}' + + @staticmethod + def mac(val): + return "%02x:%02x:%02x:%02x:%02x:%02x" % val + + @staticmethod + def version(val): + x = val[0] + return f'V{(x>>12)}.{(x>>8)&0xf}.{(x>>4)&0xf}{x&0xf:1X}' + # return f'V{x>>12}.{(x>>8)&0xf}.{(x>>4)&0xf}{val[0]&0xf}' + + class ClrAtMidnight: __clr_at_midnight = [Register.PV1_DAILY_GENERATION, Register.PV2_DAILY_GENERATION, Register.PV3_DAILY_GENERATION, Register.PV4_DAILY_GENERATION, Register.PV5_DAILY_GENERATION, Register.PV6_DAILY_GENERATION, Register.DAILY_GENERATION] # noqa: E501 db = {} diff --git a/app/src/modbus.py b/app/src/modbus.py index d186ba7..eec6d17 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -17,9 +17,9 @@ import asyncio from typing import Generator, Callable if __name__ == "app.src.modbus": - from app.src.infos import Register + from app.src.infos import Register, Fmt else: # pragma: no cover - from infos import Register + from infos import Register, Fmt logger = logging.getLogger('data') @@ -44,11 +44,11 @@ class Modbus(): 0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501 0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 - 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf:1X}'"}, # noqa: E501 + 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501 0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x300b: {'reg': Register.GRID_FREQUENCY, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 - 0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'eval': 'result-40'}, # noqa: E501 + 0x300c: {'reg': Register.INVERTER_TEMP, 'fmt': '!H', 'offset': -40}, # noqa: E501 # 0x300d 0x300e: {'reg': Register.RATED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x300f: {'reg': Register.OUTPUT_POWER, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 @@ -229,17 +229,6 @@ class Modbus(): return False - def __get_value(self, buf: bytes, idx: int, row: dict): - '''get a value from the received buffer''' - val = struct.unpack_from(row['fmt'], buf, idx) - result = val[0] - - if 'eval' in row: - result = eval(row['eval']) - if 'ratio' in row: - result = round(result * row['ratio'], 2) - return result - def __process_data(self, info_db, buf: bytes, first_reg, elmlen): '''Generator over received registers, updates the db''' for i in range(0, elmlen): @@ -249,7 +238,7 @@ class Modbus(): info_id = row['reg'] keys, level, unit, must_incr = info_db._key_obj(info_id) if keys: - result = self.__get_value(buf, 3+2*i, row) + result = Fmt.get_value(buf, 3+2*i, row) name, update = info_db.update_db(keys, must_incr, result) yield keys[0], update, result diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py index 6fad692..0c84c20 100644 --- a/app/tests/test_infos_g3.py +++ b/app/tests/test_infos_g3.py @@ -520,15 +520,3 @@ def test_invalid_data_type(invalid_data_seq): val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter assert val == 1 - -def test_result_eval(inv_data_seq2: bytes): - - # add eval to convert temperature from °F to °C - RegisterMap.map[0x00000514]['eval'] = '(result-32)/1.8' - - i = InfosG3() - - for _, _ in i.parse (inv_data_seq2): - pass # side effect is calling generator i.parse() - assert math.isclose(-5.0, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09) - del RegisterMap.map[0x00000514]['eval'] # remove eval diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index c9ceb4e..095a29b 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -256,12 +256,12 @@ def test_build_ha_conf4(): assert tests==1 -def test_exception_and_eval(inverter_data: bytes): +def test_exception_and_calc(inverter_data: bytes): - # add eval to convert temperature from °F to °C + # patch table to convert temperature from °F to °C ofs = RegisterMap.map[0x420100d8]['offset'] - del RegisterMap.map[0x420100d8]['offset'] - RegisterMap.map[0x420100d8]['eval'] = '(result-32)/1.8' + RegisterMap.map[0x420100d8]['quotient'] = 1.8 + RegisterMap.map[0x420100d8]['offset'] = -32/1.8 # map PV1_VOLTAGE to invalid register RegisterMap.map[0x420100e0]['reg'] = Register.TEST_REG2 # set invalid maping entry for OUTPUT_POWER (string instead of dict type) @@ -274,7 +274,9 @@ def test_exception_and_eval(inverter_data: bytes): for key, update in i.parse (inverter_data, 0x42, 1): pass # side effect is calling generator i.parse() assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09) - del RegisterMap.map[0x420100d8]['eval'] # remove eval + del RegisterMap.map[0x420100d8]['quotient'] + del RegisterMap.map[0x420100d8]['offset'] + RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping RegisterMap.map[0x420100de] = backup # reset mapping From a6ad3d4f0d961d90f0a30d6f63a23f17259a2beb Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Fri, 25 Oct 2024 23:49:35 +0200 Subject: [PATCH 6/8] fix linter warnings --- app/src/infos.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/infos.py b/app/src/infos.py index c94e219..4358969 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -154,8 +154,7 @@ class Fmt: @staticmethod def version(val): x = val[0] - return f'V{(x>>12)}.{(x>>8)&0xf}.{(x>>4)&0xf}{x&0xf:1X}' - # return f'V{x>>12}.{(x>>8)&0xf}.{(x>>4)&0xf}{val[0]&0xf}' + return f'V{(x >> 12)}.{(x >> 8) & 0xf}.{(x >> 4) & 0xf}{x & 0xf:1X}' class ClrAtMidnight: From 9b22fe354c3420fe4e0a07f21e44c6fbe17ee415 Mon Sep 17 00:00:00 2001 From: Stefan Allius Date: Sat, 26 Oct 2024 17:30:00 +0200 Subject: [PATCH 7/8] clear remote ptr on disc only for client ifcs --- app/src/async_stream.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 2650235..b023c23 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -221,7 +221,6 @@ class AsyncStream(AsyncIfcImpl): async def disc(self) -> None: """Async disc handler for graceful disconnect""" - self.remote = None if self._writer.is_closing(): return logger.debug(f'AsyncStream.disc() l{self.l_addr} | r{self.r_addr}') @@ -370,6 +369,11 @@ class AsyncStreamClient(AsyncStream): AsyncStream.__init__(self, reader, writer, rstream) self.close_cb = close_cb + async def disc(self) -> None: + logging.debug('AsyncStreamClient.disc()') + self.remote = None + await super().disc() + def close(self) -> None: logging.debug('AsyncStreamClient.close()') self.close_cb = None From 78a35b5513837a1941f126944df5b2ddbf8664c6 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:09:10 +0100 Subject: [PATCH 8/8] report alarm and fault bitfield to ha (#204) * report alarm and fault bitfield to home assistant * initial verson of message builder for SolarmanV5 - for SolarmaV5 we build he param field for the device and inverter message from the internal database - added param description to the info table for constant values, which are not parsed and stored in internal database * define constants for often used format strings * update changelog --- CHANGELOG.md | 3 + app/src/gen3/infos_g3.py | 20 +--- app/src/gen3plus/infos_g3p.py | 70 +++++++++++- app/src/infos.py | 204 +++++++++++++++++++++++++++------- app/src/modbus.py | 10 ++ app/tests/test_infos.py | 23 +++- app/tests/test_infos_g3.py | 4 +- app/tests/test_infos_g3p.py | 64 +++++++++-- 8 files changed, 328 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bd297..a3bc961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add SolarmanV5 messages builder +- report inverter alarms and faults per MQTT [#7](https://github.com/s-allius/tsun-gen3-proxy/issues/7) + ## [0.11.0] - 2024-10-13 - fix healthcheck on infrastructure with IPv6 support [#196](https://github.com/s-allius/tsun-gen3-proxy/issues/196) diff --git a/app/src/gen3/infos_g3.py b/app/src/gen3/infos_g3.py index 88f9207..480fc94 100644 --- a/app/src/gen3/infos_g3.py +++ b/app/src/gen3/infos_g3.py @@ -70,22 +70,10 @@ class RegisterMap: 0x000d0020: {'reg': Register.COLLECT_INTERVAL}, 0x000cf850: {'reg': Register.DATA_UP_INTERVAL}, 0x000c7f38: {'reg': Register.COMMUNICATION_TYPE}, - 0x00000191: {'reg': Register.EVENT_401}, - 0x00000192: {'reg': Register.EVENT_402}, - 0x00000193: {'reg': Register.EVENT_403}, - 0x00000194: {'reg': Register.EVENT_404}, - 0x00000195: {'reg': Register.EVENT_405}, - 0x00000196: {'reg': Register.EVENT_406}, - 0x00000197: {'reg': Register.EVENT_407}, - 0x00000198: {'reg': Register.EVENT_408}, - 0x00000199: {'reg': Register.EVENT_409}, - 0x0000019a: {'reg': Register.EVENT_410}, - 0x0000019b: {'reg': Register.EVENT_411}, - 0x0000019c: {'reg': Register.EVENT_412}, - 0x0000019d: {'reg': Register.EVENT_413}, - 0x0000019e: {'reg': Register.EVENT_414}, - 0x0000019f: {'reg': Register.EVENT_415}, - 0x000001a0: {'reg': Register.EVENT_416}, + 0x00000190: {'reg': Register.EVENT_ALARM}, + 0x000001f4: {'reg': Register.EVENT_FAULT}, + 0x00000258: {'reg': Register.EVENT_BF1}, + 0x000002bc: {'reg': Register.EVENT_BF2}, 0x00000064: {'reg': Register.INVERTER_STATUS}, 0x0000125c: {'reg': Register.MAX_DESIGNED_POWER}, 0x00003200: {'reg': Register.OUTPUT_COEFFICIENT, 'ratio': 100/1024}, diff --git a/app/src/gen3plus/infos_g3p.py b/app/src/gen3plus/infos_g3p.py index 58c53c9..443dfac 100644 --- a/app/src/gen3plus/infos_g3p.py +++ b/app/src/gen3plus/infos_g3p.py @@ -11,28 +11,48 @@ class RegisterMap: # make the class read/only by using __slots__ __slots__ = () + FMT_2_16BIT_VAL = '!HH' + FMT_3_16BIT_VAL = '!HHH' + FMT_4_16BIT_VAL = '!HHHH' + map = { # 0x41020007: {'reg': Register.DEVICE_SNR, 'fmt': '> 16) & 0xff + mtype = (idx >> 24) & 0xff + if ftype != rcv_ftype or mtype != msg_type: + continue + if not isinstance(row, dict): + continue + if 'const' in row: + val = row['const'] + else: + info_id = row['reg'] + val = self.get_db_value(info_id) + if not val: + continue + Fmt.set_value(buf, addr, row, val) + return buf diff --git a/app/src/infos.py b/app/src/infos.py index 4358969..92609a1 100644 --- a/app/src/infos.py +++ b/app/src/infos.py @@ -43,6 +43,8 @@ class Register(Enum): RATED_POWER = 84 INVERTER_TEMP = 85 INVERTER_STATUS = 86 + DETECT_STATUS_1 = 87 + DETECT_STATUS_2 = 88 PV1_VOLTAGE = 100 PV1_CURRENT = 101 PV1_POWER = 102 @@ -85,6 +87,10 @@ class Register(Enum): PV5_TOTAL_GENERATION = 241 PV6_DAILY_GENERATION = 250 PV6_TOTAL_GENERATION = 251 + INV_UNKNOWN_1 = 252 + BOOT_STATUS = 253 + DSP_STATUS = 254 + GRID_VOLTAGE = 300 GRID_CURRENT = 301 GRID_FREQUENCY = 302 @@ -100,22 +106,11 @@ class Register(Enum): IP_ADDRESS = 407 POLLING_INTERVAL = 408 SENSOR_LIST = 409 - EVENT_401 = 500 - EVENT_402 = 501 - EVENT_403 = 502 - EVENT_404 = 503 - EVENT_405 = 504 - EVENT_406 = 505 - EVENT_407 = 506 - EVENT_408 = 507 - EVENT_409 = 508 - EVENT_410 = 509 - EVENT_411 = 510 - EVENT_412 = 511 - EVENT_413 = 512 - EVENT_414 = 513 - EVENT_415 = 514 - EVENT_416 = 515 + SSID = 410 + EVENT_ALARM = 500 + EVENT_FAULT = 501 + EVENT_BF1 = 502 + EVENT_BF2 = 503 TS_INPUT = 600 TS_GRID = 601 TS_TOTAL = 602 @@ -144,17 +139,54 @@ class Fmt: return result @staticmethod - def hex4(val): - return f'{val[0]:04x}' + def hex4(val: tuple | str, reverse=False) -> str | int: + if not reverse: + return f'{val[0]:04x}' + else: + return int(val, 16) @staticmethod - def mac(val): - return "%02x:%02x:%02x:%02x:%02x:%02x" % val + def mac(val: tuple | str, reverse=False) -> str | tuple: + if not reverse: + return "%02x:%02x:%02x:%02x:%02x:%02x" % val + else: + return ( + int(val[0:2], 16), int(val[3:5], 16), + int(val[6:8], 16), int(val[9:11], 16), + int(val[12:14], 16), int(val[15:], 16)) @staticmethod - def version(val): - x = val[0] - return f'V{(x >> 12)}.{(x >> 8) & 0xf}.{(x >> 4) & 0xf}{x & 0xf:1X}' + def version(val: tuple | str, reverse=False) -> str | int: + if not reverse: + x = val[0] + return f'V{(x >> 12)}.{(x >> 8) & 0xf}' \ + f'.{(x >> 4) & 0xf}{x & 0xf:1X}' + else: + arr = val[1:].split('.') + return int(arr[0], 10) << 12 | \ + int(arr[1], 10) << 8 | \ + int(arr[2][:-1], 10) << 4 | \ + int(arr[2][-1:], 16) + + @staticmethod + def set_value(buf: bytearray, idx: int, row: dict, val): + '''Get a value from buf and interpret as in row defined''' + fmt = row['fmt'] + if 'offset' in row: + val = val - row['offset'] + if 'quotient' in row: + val = round(val * row['quotient']) + if 'ratio' in row: + val = round(val / row['ratio']) + if 'func' in row: + val = row['func'](val, reverse=True) + if isinstance(val, str): + val = bytes(val, 'UTF8') + + if isinstance(val, tuple): + struct.pack_into(fmt, buf, idx, *val) + else: + struct.pack_into(fmt, buf, idx, val) class ClrAtMidnight: @@ -251,6 +283,99 @@ class Infos: {{ this.state }} {% endif %} ''' + __inv_alarm_val_tpl = ''' +{% if 'Inverter_Alarm' in value_json and + value_json['Inverter_Alarm'] != None %} + {% set val_int = value_json['Inverter_Alarm'] | int %} + {% if val_int == 0 %} + {% set result = 'noAlarm'%} + {%else%} + {% set result = '' %} + {% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%} + {% endif %} + {% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%} + {% endif %} + {% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%} + {% endif %} + {% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%} + {% endif %} + {% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%} + {% endif %} + {% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%} + {% endif %} + {% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%} + {% endif %} + {% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%} + {% endif %} + {% if val_int | bitwise_and(9)%}{% set result = result + 'noUtility, '%} + {% endif %} + {% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%} + {% endif %} + {% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%} + {% endif %} + {% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%} + {% endif %} + {% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%} + {% endif %} + {% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%} + {% endif %} + {% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%} + {% endif %} + {% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%} + {% endif %} + {% endif %} + {{ result }} +{% else %} + {{ this.state }} +{% endif %} +''' + __inv_fault_val_tpl = ''' +{% if 'Inverter_Fault' in value_json and + value_json['Inverter_Fault'] != None %} + {% set val_int = value_json['Inverter_Fault'] | int %} + {% if val_int == 0 %} + {% set result = 'noFault'%} + {%else%} + {% set result = '' %} + {% if val_int | bitwise_and(1)%}{% set result = result + 'Bit1, '%} + {% endif %} + {% if val_int | bitwise_and(2)%}{% set result = result + 'Bit2, '%} + {% endif %} + {% if val_int | bitwise_and(3)%}{% set result = result + 'Bit3, '%} + {% endif %} + {% if val_int | bitwise_and(4)%}{% set result = result + 'Bit4, '%} + {% endif %} + {% if val_int | bitwise_and(5)%}{% set result = result + 'Bit5, '%} + {% endif %} + {% if val_int | bitwise_and(6)%}{% set result = result + 'Bit6, '%} + {% endif %} + {% if val_int | bitwise_and(7)%}{% set result = result + 'Bit7, '%} + {% endif %} + {% if val_int | bitwise_and(8)%}{% set result = result + 'Bit8, '%} + {% endif %} + {% if val_int | bitwise_and(9)%}{% set result = result + 'Bit9, '%} + {% endif %} + {% if val_int | bitwise_and(10)%}{% set result = result + 'Bit10, '%} + {% endif %} + {% if val_int | bitwise_and(11)%}{% set result = result + 'Bit11, '%} + {% endif %} + {% if val_int | bitwise_and(12)%}{% set result = result + 'Bit12, '%} + {% endif %} + {% if val_int | bitwise_and(13)%}{% set result = result + 'Bit13, '%} + {% endif %} + {% if val_int | bitwise_and(14)%}{% set result = result + 'Bit14, '%} + {% endif %} + {% if val_int | bitwise_and(15)%}{% set result = result + 'Bit15, '%} + {% endif %} + {% if val_int | bitwise_and(16)%}{% set result = result + 'Bit16, '%} + {% endif %} + {% endif %} + {{ result }} +{% else %} + {{ this.state }} +{% endif %} +''' + __output_coef_val_tpl = "{% if 'Output_Coefficient' in value_json and value_json['Output_Coefficient'] != None %}{{value_json['Output_Coefficient']|string() +' %'}}{% else %}{{ this.state }}{% endif %}" # noqa: E501 __info_defs = { @@ -286,7 +411,8 @@ class Infos: 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 - + Register.BOOT_STATUS: {'name': ['inverter', 'BOOT_STATUS'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.DSP_STATUS: {'name': ['inverter', 'DSP_STATUS'], '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': FMT_INT, 'name': 'Active Inverter Connections', 'icon': 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': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501 @@ -303,22 +429,12 @@ class Infos: # 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501 # events - Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_402: {'name': ['events', '402_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_403: {'name': ['events', '403_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_404: {'name': ['events', '404_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_405: {'name': ['events', '405_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_406: {'name': ['events', '406_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_407: {'name': ['events', '407_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_408: {'name': ['events', '408_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_410: {'name': ['events', '410_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_411: {'name': ['events', '411_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_412: {'name': ['events', '412_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_413: {'name': ['events', '413_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_414: {'name': ['events', '414_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 - Register.EVENT_416: {'name': ['events', '416_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.EVENT_ALARM: {'name': ['events', 'Inverter_Alarm'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_alarm_', 'name': 'Inverter Alarm', 'val_tpl': __inv_alarm_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501 + Register.EVENT_FAULT: {'name': ['events', 'Inverter_Fault'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_fault_', 'name': 'Inverter Fault', 'val_tpl': __inv_fault_val_tpl, 'icon': 'mdi:alarm-light'}}, # noqa: E501 + Register.EVENT_BF1: {'name': ['events', 'Inverter_Bitfield_1'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 + Register.EVENT_BF2: {'name': ['events', 'Inverter_bitfield_2'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 + # Register.EVENT_409: {'name': ['events', '409_No_Utility'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + # Register.EVENT_415: {'name': ['events', '415_GridFreqOverRating'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 # grid measures: Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 @@ -328,6 +444,8 @@ class Infos: Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501 Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501 Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501 + Register.DETECT_STATUS_1: {'name': ['env', 'Detect_Status_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + Register.DETECT_STATUS_2: {'name': ['env', 'Detect_Status_2'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 # input measures: Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 @@ -377,6 +495,10 @@ class Infos: Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 + Register.SSID: {'name': ['controller', 'WiFi_SSID'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + + Register.INV_UNKNOWN_1: {'name': ['inv_unknown', 'Unknown_1'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 + } @property @@ -686,6 +808,8 @@ class Infos: def get_db_value(self, id: Register, not_found_result: any = None): '''get database value''' + if id not in self.info_defs: + return not_found_result row = self.info_defs[id] if isinstance(row, dict): keys = row['name'] diff --git a/app/src/modbus.py b/app/src/modbus.py index eec6d17..c3a1d67 100644 --- a/app/src/modbus.py +++ b/app/src/modbus.py @@ -40,10 +40,19 @@ class Modbus(): __crc_tab = [] mb_reg_mapping = { + 0x2000: {'reg': Register.BOOT_STATUS, 'fmt': '!H'}, # noqa: E501 + 0x2001: {'reg': Register.DSP_STATUS, 'fmt': '!H'}, # noqa: E501 0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501 0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 + 0x3001: {'reg': Register.DETECT_STATUS_1, 'fmt': '!H'}, # noqa: E501 + 0x3002: {'reg': Register.DETECT_STATUS_2, 'fmt': '!H'}, # noqa: E501 + 0x3003: {'reg': Register.EVENT_ALARM, 'fmt': '!H'}, # noqa: E501 + 0x3004: {'reg': Register.EVENT_FAULT, 'fmt': '!H'}, # noqa: E501 + 0x3005: {'reg': Register.EVENT_BF1, 'fmt': '!H'}, # noqa: E501 + 0x3006: {'reg': Register.EVENT_BF2, 'fmt': '!H'}, # noqa: E501 + 0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'func': Fmt.version}, # noqa: E501 0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501 0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 @@ -74,6 +83,7 @@ class Modbus(): 0x3026: {'reg': Register.PV3_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 0x3028: {'reg': Register.PV4_DAILY_GENERATION, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501 0x3029: {'reg': Register.PV4_TOTAL_GENERATION, 'fmt': '!L', 'ratio': 0.01}, # noqa: E501 + # 0x302a } def __init__(self, snd_handler: Callable[[bytes, int, str], None], diff --git a/app/tests/test_infos.py b/app/tests/test_infos.py index 8d0c268..18eb5e4 100644 --- a/app/tests/test_infos.py +++ b/app/tests/test_infos.py @@ -3,7 +3,7 @@ import pytest import json, math import logging from app.src.infos import Register, ClrAtMidnight -from app.src.infos import Infos +from app.src.infos import Infos, Fmt def test_statistic_counter(): i = Infos() @@ -256,3 +256,24 @@ def test_key_obj(): assert level == logging.DEBUG assert unit == 'kWh' assert must_incr == True + +def test_hex4_cnv(): + tst_val = (0x12ef, ) + string = Fmt.hex4(tst_val) + assert string == '12ef' + val = Fmt.hex4(string, reverse=True) + assert val == tst_val[0] + +def test_mac_cnv(): + tst_val = (0x12, 0x34, 0x67, 0x89, 0xcd, 0xef) + string = Fmt.mac(tst_val) + assert string == '12:34:67:89:cd:ef' + val = Fmt.mac(string, reverse=True) + assert val == tst_val + +def test_version_cnv(): + tst_val = (0x123f, ) + string = Fmt.version(tst_val) + assert string == 'V1.2.3F' + val = Fmt.version(string, reverse=True) + assert val == tst_val[0] diff --git a/app/tests/test_infos_g3.py b/app/tests/test_infos_g3.py index 0c84c20..1bab29e 100644 --- a/app/tests/test_infos_g3.py +++ b/app/tests/test_infos_g3.py @@ -501,10 +501,10 @@ def test_new_data_types(inv_data_new): else: assert False - assert tests==15 + assert tests==5 assert json.dumps(i.db['inverter']) == json.dumps({"Manufacturer": 0}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {}}) - assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0}) + assert json.dumps(i.db['events']) == json.dumps({"Inverter_Alarm": 0, "Inverter_Fault": 0}) def test_invalid_data_type(invalid_data_seq): i = InfosG3() diff --git a/app/tests/test_infos_g3p.py b/app/tests/test_infos_g3p.py index 095a29b..3fbefa9 100644 --- a/app/tests/test_infos_g3p.py +++ b/app/tests/test_infos_g3p.py @@ -57,6 +57,7 @@ def inverter_data(): # 0x4210 ftype: 0x01 msg += b'\x01\x61\x00\xa8\x02\x54\x01\x5a\x00\x8a\x01\xe4\x01\x5a\x00\xbd' msg += b'\x02\x8f\x00\x11\x00\x01\x00\x00\x00\x0b\x00\x00\x27\x98\x00\x04' msg += b'\x00\x00\x0c\x04\x00\x03\x00\x00\x0a\xe7\x00\x05\x00\x00\x0c\x75' + msg += b'\x00\x00\x00\x00\x06\x16\x02\x00\x00\x00\x55\xaa\x00\x01\x00\x00' msg += b'\x00\x00\x00\x00\xff\xff\x07\xd0\x00\x03\x04\x00\x04\x00\x04\x00' msg += b'\x04\x00\x00\x01\xff\xff\x00\x01\x00\x06\x00\x68\x00\x68\x05\x00' @@ -85,10 +86,21 @@ def test_parse_4110(str_test_ip, device_data: bytes): pass # side effect is calling generator i.parse() assert json.dumps(i.db) == json.dumps({ - 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0"}, + 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": str_test_ip, "Sensor_List": "02b0", "WiFi_SSID": "Allius-Home"}, 'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "MAC-Addr": "40:2a:8f:4f:51:54", "Collector_Fw_Version": "V1.1.00.0B"}, }) +def test_build_4110(str_test_ip, device_data: bytes): + i = InfosG3P(client_mode=False) + i.db.clear() + for key, update in i.parse (device_data, 0x41, 2): + pass # side effect is calling generator i.parse() + + build_msg = i.build(len(device_data), 0x41, 2) + for i in range(11, 20): + build_msg[i] = device_data[i] + assert device_data == build_msg + def test_parse_4210(inverter_data: bytes): i = InfosG3P(client_mode=False) i.db.clear() @@ -98,16 +110,30 @@ def test_parse_4210(inverter_data: bytes): assert json.dumps(i.db) == json.dumps({ "controller": {"Sensor_List": "02b0", "Power_On_Time": 2051}, - "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0}, - "env": {"Inverter_Status": 1, "Inverter_Temp": 14}, + "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "BOOT_STATUS": 0, "DSP_STATUS": 21930, "Max_Designed_Power": 2000, "Output_Coefficient": 100.0}, + "env": {"Inverter_Status": 1, "Detect_Status_1": 2, "Detect_Status_2": 0, "Inverter_Temp": 14}, + "events": {"Inverter_Alarm": 0, "Inverter_Fault": 0, "Inverter_Bitfield_1": 0, "Inverter_bitfield_2": 0}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "input": {"pv1": {"Voltage": 35.3, "Current": 1.68, "Power": 59.6, "Daily_Generation": 0.04, "Total_Generation": 30.76}, "pv2": {"Voltage": 34.6, "Current": 1.38, "Power": 48.4, "Daily_Generation": 0.03, "Total_Generation": 27.91}, "pv3": {"Voltage": 34.6, "Current": 1.89, "Power": 65.5, "Daily_Generation": 0.05, "Total_Generation": 31.89}, "pv4": {"Voltage": 1.7, "Current": 0.01, "Power": 0.0, "Total_Generation": 15.58}}, - "total": {"Daily_Generation": 0.11, "Total_Generation": 101.36} + "total": {"Daily_Generation": 0.11, "Total_Generation": 101.36}, + "inv_unknown": {"Unknown_1": 512} }) + +def test_build_4210(inverter_data: bytes): + i = InfosG3P(client_mode=False) + i.db.clear() + for key, update in i.parse (inverter_data, 0x42, 1): + pass # side effect is calling generator i.parse() + + build_msg = i.build(len(inverter_data), 0x42, 1) + for i in range(11, 31): + build_msg[i] = inverter_data[i] + assert inverter_data == build_msg + def test_build_ha_conf1(): i = InfosG3P(client_mode=False) i.static_init() # initialize counter @@ -269,19 +295,43 @@ def test_exception_and_calc(inverter_data: bytes): RegisterMap.map[0x420100de] = 'invalid_entry' i = InfosG3P(client_mode=False) - # i.db.clear() + i.db.clear() for key, update in i.parse (inverter_data, 0x42, 1): pass # side effect is calling generator i.parse() assert math.isclose(12.2222, round (i.get_db_value(Register.INVERTER_TEMP, 0),4), rel_tol=1e-09, abs_tol=1e-09) + + build_msg = i.build(len(inverter_data), 0x42, 1) + assert build_msg[32:0xde] == inverter_data[32:0xde] + assert build_msg[0xde:0xe2] == b'\x00\x00\x00\x00' + assert build_msg[0xe2:-1] == inverter_data[0xe2:-1] + + + # remove a table entry and test parsing and building del RegisterMap.map[0x420100d8]['quotient'] del RegisterMap.map[0x420100d8]['offset'] - RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping - RegisterMap.map[0x420100de] = backup # reset mapping + i.db.clear() for key, update in i.parse (inverter_data, 0x42, 1): pass # side effect is calling generator i.parse() assert 54 == i.get_db_value(Register.INVERTER_TEMP, 0) + build_msg = i.build(len(inverter_data), 0x42, 1) + assert build_msg[32:0xd8] == inverter_data[32:0xd8] + assert build_msg[0xd8:0xe2] == b'\x006\x00\x00\x02X\x00\x00\x00\x00' + assert build_msg[0xe2:-1] == inverter_data[0xe2:-1] + + # test restore table RegisterMap.map[0x420100d8]['offset'] = ofs + RegisterMap.map[0x420100e0]['reg'] = Register.PV1_VOLTAGE # reset mapping + RegisterMap.map[0x420100de] = backup # reset mapping + + # test orginial table + i.db.clear() + for key, update in i.parse (inverter_data, 0x42, 1): + pass # side effect is calling generator i.parse() + assert 14 == i.get_db_value(Register.INVERTER_TEMP, 0) + + build_msg = i.build(len(inverter_data), 0x42, 1) + assert build_msg[32:-1] == inverter_data[32:-1]