From a532eaf7c9d2dc52bdecc18ad88b50ac9ee67fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemens=20B=C3=B6swirth?= Date: Wed, 8 Nov 2023 00:59:29 +0000 Subject: [PATCH] try refetching single register if decode fails --- .../idm_heatpump/idm_heatpump.py | 72 ++++++-- .../idm_heatpump/sensor_addresses.py | 167 ++++++++++-------- 2 files changed, 149 insertions(+), 90 deletions(-) diff --git a/custom_components/idm_heatpump/idm_heatpump.py b/custom_components/idm_heatpump/idm_heatpump.py index e7e480e..504f663 100644 --- a/custom_components/idm_heatpump/idm_heatpump.py +++ b/custom_components/idm_heatpump/idm_heatpump.py @@ -38,7 +38,9 @@ class _SensorGroup: sensors: list[BaseSensorAddress] sensor_groups: list[_SensorGroup] = [] - def __init__(self, hostname: str, circuits: list[HeatingCircuit], zones: list[ZoneModule]) -> None: + def __init__( + self, hostname: str, circuits: list[HeatingCircuit], zones: list[ZoneModule] + ) -> None: """Create heatpump.""" self.client = AsyncModbusTcpClient(host=hostname) @@ -72,8 +74,9 @@ def __init__(self, hostname: str, circuits: list[HeatingCircuit], zones: list[Zo if len(self.sensor_groups) == 0 or sensor.address != last_address: self.sensor_groups.append( IdmHeatpump._SensorGroup( - start=sensor.address, count=sensor.size, sensors=[ - sensor] + start=sensor.address, + count=sensor.size, + sensors=[sensor], ) ) else: @@ -84,36 +87,43 @@ def __init__(self, hostname: str, circuits: list[HeatingCircuit], zones: list[Zo ) async def _fetch_registers(self, group: _SensorGroup) -> ReadInputRegistersResponse: - LOGGER.debug("reading registers %d", group.start) + LOGGER.debug("reading registers %d (count=%d)", group.start, group.count) return await self.client.read_input_registers( address=group.start, count=group.count, slave=1, ) + async def _fetch_retry(self, group: _SensorGroup) -> ReadInputRegistersResponse: + try: + return await self._fetch_registers(group) + except ConnectionException: + if not self.client.connected: + await self.client.connect() + return await self._fetch_registers(group) + except asyncio.exceptions.TimeoutError: + if not self.client.connected: + await self.client.connect() + return await self._fetch_registers(group) + async def _fetch_sensors(self, group: _SensorGroup) -> dict | None: LOGGER.debug("fetching registers %d", group.start) try: - try: - result = await self._fetch_registers(group) - except ConnectionException: - if not self.client.connected: - await self.client.connect() - result = await self._fetch_registers(group) - except asyncio.exceptions.TimeoutError: - if not self.client.connected: - await self.client.connect() - result = await self._fetch_registers(group) + result = await self._fetch_retry(group) except ModbusException as exception: LOGGER.error( - "Failed to fetch registers for group %d: %s", group.start, exception + "Failed to fetch registers for group %d: %s", + group.start, + exception, ) return None if result.isError(): LOGGER.error( - "Failed to fetch registers for group %d: %s", group.start, result + "Failed to fetch registers for group %d: %s", + group.start, + result, ) return None @@ -130,10 +140,36 @@ async def _fetch_sensors(self, group: _SensorGroup) -> dict | None: data = {} for sensor in group.sensors: - data[sensor.name] = sensor.decode(decoder) + value = sensor.decode(decoder) + + # if decoding fails refetch single register and try again + if value is not None and isinstance(value, ValueError): + single_result = await self._fetch_retry( + IdmHeatpump._SensorGroup( + start=sensor.address, + count=sensor.size, + sensors=[sensor], + ) + ) + value = sensor.decode( + BinaryPayloadDecoder.fromRegisters( + single_result.registers, + byteorder=Endian.BIG, + wordorder=Endian.LITTLE, + ) + ) + + # if decoding fails again set to None + if value is not None and isinstance(value, ValueError): + value = None + + data[sensor.name] = value + except ModbusException as exception: LOGGER.error( - "Failed to fetch registers for group %d: %s", group.start, exception + "Failed to fetch registers for group %d: %s", + group.start, + exception, ) return None diff --git a/custom_components/idm_heatpump/sensor_addresses.py b/custom_components/idm_heatpump/sensor_addresses.py index 270eb97..ed25953 100644 --- a/custom_components/idm_heatpump/sensor_addresses.py +++ b/custom_components/idm_heatpump/sensor_addresses.py @@ -57,7 +57,7 @@ def size(self) -> int: """Get number of registers this sensor's value occupies.""" @abstractmethod - def decode(self, decoder: BinaryPayloadDecoder) -> _T: + def decode(self, decoder: BinaryPayloadDecoder) -> _T | None: """Decode this sensor's value.""" @abstractmethod @@ -111,7 +111,9 @@ def encode(self, builder: BinaryPayloadBuilder, value: bool): """Encode this sensor's value.""" builder.add_16bit_uint(1 if value else 0) - def entity_description(self, config_entry: ConfigEntry) -> BinarySensorEntityDescription: + def entity_description( + self, config_entry: ConfigEntry + ) -> BinarySensorEntityDescription: """SensorEntityDescription for this sensor.""" return BinarySensorEntityDescription( key=self.name, @@ -132,7 +134,7 @@ class _FloatSensorAddress(IdmSensorAddress[float]): def size(self): return 2 - def decode(self, decoder: BinaryPayloadDecoder) -> float: + def decode(self, decoder: BinaryPayloadDecoder) -> float | None: raw_value = decoder.decode_32bit_float() LOGGER.debug("raw value (float32) for %s: %d", self.name, raw_value) value = round(raw_value * self.scale, self.decimal_digits) @@ -146,7 +148,8 @@ def decode(self, decoder: BinaryPayloadDecoder) -> float: def encode(self, builder: BinaryPayloadBuilder, value: float): assert (self.min_value is None or value >= self.min_value) and ( - self.max_value is None or value <= self.max_value) + self.max_value is None or value <= self.max_value + ) builder.add_32bit_float(value) def entity_description(self, config_entry: ConfigEntry) -> SensorEntityDescription: @@ -169,7 +172,7 @@ class _UCharSensorAddress(IdmSensorAddress[int]): def size(self): return 1 - def decode(self, decoder: BinaryPayloadDecoder) -> int: + def decode(self, decoder: BinaryPayloadDecoder) -> int | None: value = decoder.decode_16bit_uint() LOGGER.debug("raw value (uint16) for %s: %d", self.name, value) return ( @@ -181,7 +184,8 @@ def decode(self, decoder: BinaryPayloadDecoder) -> int: def encode(self, builder: BinaryPayloadBuilder, value: int): assert (self.min_value is None or value >= self.min_value) and ( - self.max_value is None or value <= self.max_value) + self.max_value is None or value <= self.max_value + ) builder.add_16bit_uint(value) def entity_description(self, config_entry: ConfigEntry) -> SensorEntityDescription: @@ -204,7 +208,7 @@ class _WordSensorAddress(IdmSensorAddress[int]): def size(self): return 1 - def decode(self, decoder: BinaryPayloadDecoder) -> int: + def decode(self, decoder: BinaryPayloadDecoder) -> int | None: value = decoder.decode_16bit_int() LOGGER.debug("raw value (int16) for %s: %d", self.name, value) return ( @@ -216,7 +220,8 @@ def decode(self, decoder: BinaryPayloadDecoder) -> int: def encode(self, builder: BinaryPayloadBuilder, value: float): assert (self.min_value is None or value >= self.min_value) and ( - self.max_value is None or value <= self.max_value) + self.max_value is None or value <= self.max_value + ) builder.add_16bit_uint(value) def entity_description(self, config_entry: ConfigEntry) -> SensorEntityDescription: @@ -459,8 +464,8 @@ def heating_circuit_sensors(circuit: HeatingCircuit) -> list[IdmSensorAddress]: T = TypeVar("T") -ZONE_OFFSETS = [2000 + 65*i for i in range(10)] -ROOM_OFFSETS = [2+7*i for i in range(8)] +ZONE_OFFSETS = [2000 + 65 * i for i in range(10)] +ROOM_OFFSETS = [2 + 7 * i for i in range(8)] @dataclass @@ -496,44 +501,48 @@ def sensors(self) -> list[IdmSensorAddress]: state_class=None, supported_features=SensorFeatures.NONE, ), - *[s for room in range(self.room_count) for s in [ - _FloatSensorAddress( - address=ZONE_OFFSETS[self.index]+ROOM_OFFSETS[room], - name=f"zone_{self.index+1}_room_{room+1}_temp_current", - unit=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - min_value=15, - max_value=30, - supported_features=SensorFeatures.NONE, - ), - _FloatSensorAddress( - address=ZONE_OFFSETS[self.index]+ROOM_OFFSETS[room]+2, - name=f"zone_{self.index+1}_room_{room+1}_temp_target", - unit=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - supported_features=SensorFeatures.NONE, - ), - _UCharSensorAddress( - address=ZONE_OFFSETS[self.index]+ROOM_OFFSETS[room]+4, - name=f"zone_{self.index+1}_room_{room+1}_humidity", - unit=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - min_value=0, - max_value=100, - supported_features=SensorFeatures.NONE, - ), - _EnumSensorAddress( - enum=RoomMode, - address=ZONE_OFFSETS[self.index]+ROOM_OFFSETS[room]+5, - name=f"zone_{self.index+1}_room_{room+1}_mode", - device_class=None, - state_class=None, - supported_features=SensorFeatures.NONE, - ), - ]] + *[ + s + for room in range(self.room_count) + for s in [ + _FloatSensorAddress( + address=ZONE_OFFSETS[self.index] + ROOM_OFFSETS[room], + name=f"zone_{self.index+1}_room_{room+1}_temp_current", + unit=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + min_value=15, + max_value=30, + supported_features=SensorFeatures.NONE, + ), + _FloatSensorAddress( + address=ZONE_OFFSETS[self.index] + ROOM_OFFSETS[room] + 2, + name=f"zone_{self.index+1}_room_{room+1}_temp_target", + unit=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + supported_features=SensorFeatures.NONE, + ), + _UCharSensorAddress( + address=ZONE_OFFSETS[self.index] + ROOM_OFFSETS[room] + 4, + name=f"zone_{self.index+1}_room_{room+1}_humidity", + unit=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + min_value=0, + max_value=100, + supported_features=SensorFeatures.NONE, + ), + _EnumSensorAddress( + enum=RoomMode, + address=ZONE_OFFSETS[self.index] + ROOM_OFFSETS[room] + 5, + name=f"zone_{self.index+1}_room_{room+1}_mode", + device_class=None, + state_class=None, + supported_features=SensorFeatures.NONE, + ), + ] + ], ] def binary_sensors(self) -> list[IdmBinarySensorAddress]: @@ -541,25 +550,26 @@ def binary_sensors(self) -> list[IdmBinarySensorAddress]: sensors = [ IdmBinarySensorAddress( - address=ZONE_OFFSETS[self.index]+1, + address=ZONE_OFFSETS[self.index] + 1, name=f"zone_{self.index+1}_dehumidifier", device_class=None, supported_features=SensorFeatures.NONE, ), *[ IdmBinarySensorAddress( - address=ZONE_OFFSETS[self.index]+ROOM_OFFSETS[room]+6, + address=ZONE_OFFSETS[self.index] + ROOM_OFFSETS[room] + 6, name=f"zone_{self.index+1}_room_{room+1}_relay", device_class=None, supported_features=SensorFeatures.NONE, - ) for room in range(self.room_count) + ) + for room in range(self.room_count) ], ] if self.room_9_relay: sensors.append( IdmBinarySensorAddress( - address=ZONE_OFFSETS[self.index]+64, + address=ZONE_OFFSETS[self.index] + 64, name=f"zone_{self.index+1}_room_9_relay", device_class=None, supported_features=SensorFeatures.NONE, @@ -1397,25 +1407,38 @@ def binary_sensors(self) -> list[IdmBinarySensorAddress]: 78: "Aktuelle PV Produktion", 4122: "Aktuelle Leistungsaufnahme Wärmepumpe", **dict( - zn for zone in range(10) for zn in [ - (ZONE_OFFSETS[zone], - f"Zonenmodul {zone+1} Modus"), - (ZONE_OFFSETS[zone]+1, - f"Zonenmodul {zone+1} Entfeuchtungsausgang"), - *[rn for room in range(8) for rn in [ - (ZONE_OFFSETS[zone]+ROOM_OFFSETS[room], - f"Zonenmodul {zone+1} Raum {room+1} Raumtemperatur"), - (ZONE_OFFSETS[zone]+ROOM_OFFSETS[room]+2, - f"Zonenmodul {zone+1} Raum {room+1} Raumsolltemperatur"), - (ZONE_OFFSETS[zone]+ROOM_OFFSETS[room]+4, - f"Zonenmodul {zone+1} Raum {room+1} Raumfeuchte"), - (ZONE_OFFSETS[zone]+ROOM_OFFSETS[room]+5, - f"Zonenmodul {zone+1} Raum {room+1} Betriebsart"), - (ZONE_OFFSETS[zone]+ROOM_OFFSETS[room]+6, - f"Zonenmodul {zone+1} Raum {room+1} Status Relais"), - ]], - (ZONE_OFFSETS[zone]+64, - f"Zonenmodul {zone+1} Raum 9 Status Relais"), + zn + for zone in range(10) + for zn in [ + (ZONE_OFFSETS[zone], f"Zonenmodul {zone+1} Modus"), + (ZONE_OFFSETS[zone] + 1, f"Zonenmodul {zone+1} Entfeuchtungsausgang"), + *[ + rn + for room in range(8) + for rn in [ + ( + ZONE_OFFSETS[zone] + ROOM_OFFSETS[room], + f"Zonenmodul {zone+1} Raum {room+1} Raumtemperatur", + ), + ( + ZONE_OFFSETS[zone] + ROOM_OFFSETS[room] + 2, + f"Zonenmodul {zone+1} Raum {room+1} Raumsolltemperatur", + ), + ( + ZONE_OFFSETS[zone] + ROOM_OFFSETS[room] + 4, + f"Zonenmodul {zone+1} Raum {room+1} Raumfeuchte", + ), + ( + ZONE_OFFSETS[zone] + ROOM_OFFSETS[room] + 5, + f"Zonenmodul {zone+1} Raum {room+1} Betriebsart", + ), + ( + ZONE_OFFSETS[zone] + ROOM_OFFSETS[room] + 6, + f"Zonenmodul {zone+1} Raum {room+1} Status Relais", + ), + ] + ], + (ZONE_OFFSETS[zone] + 64, f"Zonenmodul {zone+1} Raum 9 Status Relais"), ] - ) + ), }