Skip to content

Commit

Permalink
try refetching single register if decode fails
Browse files Browse the repository at this point in the history
  • Loading branch information
kodebach committed Nov 8, 2023
1 parent 253afe7 commit a532eaf
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 90 deletions.
72 changes: 54 additions & 18 deletions custom_components/idm_heatpump/idm_heatpump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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

Expand Down
167 changes: 95 additions & 72 deletions custom_components/idm_heatpump/sensor_addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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 (
Expand All @@ -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:
Expand All @@ -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 (
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -496,70 +501,75 @@ 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]:
"""Get data for zone module binary sensors."""

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,
Expand Down Expand Up @@ -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"),
]
)
),
}

0 comments on commit a532eaf

Please sign in to comment.