diff --git a/nibe/connection/nibegw.py b/nibe/connection/nibegw.py index 113cb11..0f2bd77 100644 --- a/nibe/connection/nibegw.py +++ b/nibe/connection/nibegw.py @@ -11,7 +11,7 @@ from operator import xor import socket import struct -from typing import Dict, Optional, Union +from typing import Dict, Literal, Optional, Union from construct import ( Adapter, @@ -99,6 +99,7 @@ def __init__( listening_port: int = 9999, read_retries: int = 3, write_retries: int = 3, + table_processing_mode: Literal["permissive", "strict"] = "permissive", ) -> None: super().__init__() @@ -110,6 +111,8 @@ def __init__( self._remote_read_port = remote_read_port self._remote_write_port = remote_write_port + self._table_processing_mode = table_processing_mode + self._transport = None self._send_lock = asyncio.Lock() @@ -432,19 +435,29 @@ def _on_raw_coil_value(self, coil_address: int, raw_value: bytes) -> None: self._on_coil_read_error(coil_address, raw_value, e) def _on_raw_coil_set(self, data: dict[int, bytes]) -> None: + successful_coil_data = [] + decode_exception_occurred = False while data: coil_address = min(data.keys()) raw_value = data.pop(coil_address) - coil = self._heatpump.get_coil_by_address(coil_address) try: coil = self._heatpump.get_coil_by_address(coil_address) if coil.size in ("u32", "s32"): raw_value = raw_value + data.pop(coil_address + 1, b"") coil_data = self.coil_encoder.decode(coil, raw_value) - self._on_coil_read_success(coil_data) + successful_coil_data.append(coil_data) + except DecodeException as e: + self._on_coil_read_error(coil_address, raw_value, e) + decode_exception_occurred = True except NibeException as e: self._on_coil_read_error(coil_address, raw_value, e) + if self._table_processing_mode == "permissive" or ( + self._table_processing_mode == "strict" and not decode_exception_occurred + ): + for coil_data in successful_coil_data: + self._on_coil_read_success(coil_data) + def _on_coil_value(self, coil_address: int, value: Union[float, int, str]) -> None: try: coil = self._heatpump.get_coil_by_address(coil_address) diff --git a/nibe/data/extensions.json b/nibe/data/extensions.json index 0cb0445..e07b604 100644 --- a/nibe/data/extensions.json +++ b/nibe/data/extensions.json @@ -497,5 +497,21 @@ "name": "bt71-ext-return-temp-40152" } } + }, + { + "description": "Limit allowed Brine In/Out temperature values for F1155/F1255", + "files": [ + "f1155_f1255.json" + ], + "data": { + "40015": { + "min": -400, + "max": 800 + }, + "40016": { + "min": -400, + "max": 800 + } + } } ] diff --git a/nibe/data/f1155_f1255.json b/nibe/data/f1155_f1255.json index f9e5311..2eecab1 100644 --- a/nibe/data/f1155_f1255.json +++ b/nibe/data/f1155_f1255.json @@ -75,7 +75,9 @@ "unit": "\u00b0C", "size": "s16", "factor": 10, - "name": "eb100-ep14-bt10-brine-in-temp-40015" + "name": "eb100-ep14-bt10-brine-in-temp-40015", + "min": -400, + "max": 800 }, "40016": { "title": "EB100-EP14-BT11 Brine Out Temp", @@ -83,7 +85,9 @@ "unit": "\u00b0C", "size": "s16", "factor": 10, - "name": "eb100-ep14-bt11-brine-out-temp-40016" + "name": "eb100-ep14-bt11-brine-out-temp-40016", + "min": -400, + "max": 800 }, "40017": { "title": "EB100-EP14-BT12 Condensor Out", @@ -425,14 +429,6 @@ "factor": 10, "name": "bt70-hw-comfort-supply-temp-40147" }, - "40152": { - "title": "BT71 Ext. Return Temp", - "info": "External return temperature, BT71", - "unit": "\u00b0C", - "size": "s16", - "factor": 10, - "name": "bt71-ext-return-temp-40152" - }, "40155": { "title": "EQ1-BT57 Collector Temp.", "info": "External collector temperature, BT57, for ACS", @@ -9558,5 +9554,13 @@ "factor": 1, "name": "aux-ers-fire-place-guard-49430", "write": true + }, + "40152": { + "title": "BT71 Ext. Return Temp", + "info": "External return temperature, BT71", + "unit": "\u00b0C", + "size": "s16", + "factor": 10, + "name": "bt71-ext-return-temp-40152" } } diff --git a/tests/connection/test_encoders.py b/tests/connection/test_encoders.py index b53c96c..ccff838 100644 --- a/tests/connection/test_encoders.py +++ b/tests/connection/test_encoders.py @@ -1,5 +1,6 @@ import pytest +from nibe.coil import Coil from nibe.connection.encoders import CoilDataEncoderModbus, CoilDataEncoderNibeGw @@ -25,7 +26,7 @@ ("s32", b"(\x06\x00\x00", 1576, True), ], ) -def test_nibegw_decode( +def test_nibegw_decode_raw_value( size, raw, raw_value, @@ -37,6 +38,29 @@ def test_nibegw_decode( assert CoilDataEncoderNibeGw(False).decode_raw_value(size, raw) == raw_value +@pytest.mark.parametrize( + "size, raw, expected, word_swap", + [ + # Test with integer limits + ("u8", b"\xFF", None, None), + ("s8", b"\x80", None, None), + ("u16", b"\xFF\xFF", None, None), + ("s16", b"\x00\x80", None, None), + ("s32", b"\x00\x00\x00\x80", None, True), + ("s32", b"\x00\x80\x00\x00", None, False), + ], +) +def test_nibegw_decode(size, raw, expected, word_swap): + coil = Coil(address=1, name="test", title="test", size=size) + + if word_swap in (True, None): + result = CoilDataEncoderNibeGw(True).decode(coil, raw).value + assert (result is None and expected is None) or (result == expected) + if word_swap in (False, None): + result = CoilDataEncoderNibeGw(False).decode(coil, raw).value + assert (result is None and expected is None) or (result == expected) + + @pytest.mark.parametrize( "size, raw, raw_value, word_swap", [ @@ -89,7 +113,7 @@ def test_nibegw_encode( ("s32", [0xF9D8, 0xFFFF], -0x628, True), ], ) -def test_modbus_decode( +def test_modbus_decode_raw_value( size, raw, raw_value, @@ -120,7 +144,7 @@ def test_modbus_decode( ("s32", [0xF9D8, 0xFFFF], -0x628, True), ], ) -def test_modbus_encode( +def test_modbus_encode_raw_value( size, raw, raw_value, diff --git a/tests/connection/test_nibegw.py b/tests/connection/test_nibegw.py index e0aaed8..0cd7a5f 100644 --- a/tests/connection/test_nibegw.py +++ b/tests/connection/test_nibegw.py @@ -149,10 +149,11 @@ async def test_read_product_info(nibegw: NibeGW): @pytest.mark.parametrize( - ("raw", "calls"), + ("raw", "table_processing_mode", "calls"), [ ( "5c00206850c9af0000889c7100a9a90a00a3a91400aba90000939c0000949c0000919c3c00929c00008f9c0000909c00003ab95000ada94600a7a91400faa90200ffff0000ffff0000ffff0000ffff0000ffff0000f0", + "strict", [ (40072, 11.3), (40079, 0.0), @@ -168,8 +169,34 @@ async def test_read_product_info(nibegw: NibeGW): (47418, 80), ], ), + ( + "5c00206850c9af0000889c7100a9a90a00a3a91400aba90000939c0000949c0000919c3c00929c00008f9c0000909c0000 3ab97000 ada94600 a7a91400 faa90200 ffff0000 ffff0000 ffff0000 ffff0000 ffff0000 d0", + "permissive", + [ + (40072, 11.3), + (40079, 0.0), + (40081, 6.0), + (40083, 0.0), + (43427, "STOPPED"), + (43431, "ON"), + (43433, "OFF"), + (43435, "OFF"), + (43437, 70), + (43514, 2), + (45001, 0), + # (47418, 80), # This coil will be ignored because of decoding error + ], + ), + ( + "5c00206850c9af0000889c7100a9a90a00a3a91400aba90000939c0000949c0000919c3c00929c00008f9c0000909c0000 3ab97000 ada94600 a7a91400 faa90200 ffff0000 ffff0000 ffff0000 ffff0000 ffff0000 d0", + "strict", + [ + # All will be ignored because of single decoding error + ], + ), ( "5c00206850 489ce400 4c9ce300 4e9ca101 889c4500 d5a1ae00 d6a1a300 fda718f8 c5a5ad98c6a50100 cda5d897cea50100 cfa51fb7d0a50600 98a96d23 99a90000 a0a9cf05 a1a90000 9ca9a01a 9da90000 449c4500 e5", + "strict", [ (40004, 6.9), (40008, 22.8), @@ -199,8 +226,13 @@ async def test_read_product_info(nibegw: NibeGW): ], ) async def test_read_multiple_with_u32( - nibegw: NibeGW, heatpump: HeatPump, raw: str, calls: list[tuple[int, Any]] + nibegw: NibeGW, + heatpump: HeatPump, + raw: str, + table_processing_mode: str, + calls: list[tuple[int, Any]], ): + nibegw._table_processing_mode = table_processing_mode on_coil_update_mock = Mock() heatpump.subscribe("coil_update", on_coil_update_mock) nibegw.datagram_received(