Skip to content

Commit 774f4a1

Browse files
committed
When connection drops, make entities unavailable
1 parent 08f87f5 commit 774f4a1

4 files changed

Lines changed: 434 additions & 297 deletions

File tree

custom_components/powertag_gateway/binary_sensor.py

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: Devic
5454
super().__init__(client, modbus_index, tag_device, "wireless communication valid", unique_id_version, serial_number)
5555

5656
async def async_update(self):
57-
self._attr_is_on = await self._client.tag_wireless_communication_valid(self._modbus_index)
57+
value = await self._client.tag_wireless_communication_valid(self._modbus_index)
58+
if self._handle_availability(value):
59+
self._attr_is_on = value
5860

5961
@staticmethod
6062
def supports_feature_set(feature_class: FeatureClass) -> bool:
@@ -76,7 +78,9 @@ def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: Devic
7678
super().__init__(client, modbus_index, tag_device, "radio communication valid", unique_id_version, serial_number)
7779

7880
async def async_update(self):
79-
self._attr_is_on = await self._client.tag_radio_communication_valid(self._modbus_index)
81+
value = await self._client.tag_radio_communication_valid(self._modbus_index)
82+
if self._handle_availability(value):
83+
self._attr_is_on = value
8084

8185
@staticmethod
8286
def supports_feature_set(feature_class: FeatureClass) -> bool:
@@ -98,20 +102,20 @@ def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: Devic
98102

99103
async def async_update(self):
100104
alarm = await self._client.tag_get_alarm(self._modbus_index)
101-
102-
if alarm.has_alarm != self._attr_is_on:
103-
self._attr_is_on = alarm.has_alarm
104-
self._attr_extra_state_attributes = {
105-
"Voltage loss": alarm.voltage_loss,
106-
"Current overload when voltage loss": alarm.current_overload_when_voltage_loss,
107-
"Current short-circuit": alarm.current_short_circuit,
108-
"Overload 45%": alarm.current_overload_45_percent,
109-
"Load current loss": alarm.load_current_loss,
110-
"Overvoltage 120%": alarm.overvoltage_120_percent,
111-
"Undervoltage 80%": alarm.undervoltage_80_percent,
112-
"Current 50%": alarm.current_50_percent,
113-
"Current 80%": alarm.current_80_percent
114-
}
105+
if self._handle_availability(alarm):
106+
if alarm.has_alarm != self._attr_is_on:
107+
self._attr_is_on = alarm.has_alarm
108+
self._attr_extra_state_attributes = {
109+
"Voltage loss": alarm.voltage_loss,
110+
"Current overload when voltage loss": alarm.current_overload_when_voltage_loss,
111+
"Current short-circuit": alarm.current_short_circuit,
112+
"Overload 45%": alarm.current_overload_45_percent,
113+
"Load current loss": alarm.load_current_loss,
114+
"Overvoltage 120%": alarm.overvoltage_120_percent,
115+
"Undervoltage 80%": alarm.undervoltage_80_percent,
116+
"Current 50%": alarm.current_50_percent,
117+
"Current 80%": alarm.current_80_percent
118+
}
115119

116120
@staticmethod
117121
def supports_feature_set(feature_class: FeatureClass) -> bool:
@@ -133,7 +137,8 @@ def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: Devic
133137

134138
async def async_update(self):
135139
alarm = await self._client.tag_get_alarm(self._modbus_index)
136-
self._attr_is_on = alarm.has_alarm
140+
if self._handle_availability(alarm):
141+
self._attr_is_on = alarm.has_alarm
137142

138143
@staticmethod
139144
def supports_feature_set(feature_class: FeatureClass) -> bool:
@@ -154,17 +159,18 @@ def __init__(self, client: SchneiderModbus, tag_device: DeviceInfo, serial_numbe
154159

155160
async def async_update(self):
156161
status = await self._client.status()
157-
self._attr_is_on = status != LinkStatus.OPERATING
158-
159-
self._attr_extra_state_attributes["status"] = {
160-
LinkStatus.OPERATING: "Operating",
161-
LinkStatus.START_UP: "Starting up",
162-
LinkStatus.DOWNGRADED: "Downgraded",
163-
LinkStatus.E2PROM_ERROR: "E2PROM error",
164-
LinkStatus.FLASH_ERROR: "Flash error",
165-
LinkStatus.RAM_ERROR: "RAM error",
166-
LinkStatus.GENERAL_FAILURE: "General failure"
167-
}[status]
162+
if self._handle_availability(status):
163+
self._attr_is_on = status != LinkStatus.OPERATING
164+
165+
self._attr_extra_state_attributes["status"] = {
166+
LinkStatus.OPERATING: "Operating",
167+
LinkStatus.START_UP: "Starting up",
168+
LinkStatus.DOWNGRADED: "Downgraded",
169+
LinkStatus.E2PROM_ERROR: "E2PROM error",
170+
LinkStatus.FLASH_ERROR: "Flash error",
171+
LinkStatus.RAM_ERROR: "RAM error",
172+
LinkStatus.GENERAL_FAILURE: "General failure"
173+
}[status]
168174

169175
@staticmethod
170176
def supports_feature_set(feature_class: FeatureClass) -> bool:
@@ -187,13 +193,14 @@ def __init__(self, client: SchneiderModbus, tag_device: DeviceInfo, serial_numbe
187193

188194
async def async_update(self):
189195
status = await self._client.health()
190-
self._attr_is_on = status != PanelHealth.NOMINAL
191-
192-
self._attr_extra_state_attributes["status"] = {
193-
PanelHealth.NOMINAL: "Nominal",
194-
PanelHealth.DEGRADED: "Degraded",
195-
PanelHealth.OUT_OF_ORDER: "Out of order"
196-
}[status]
196+
if self._handle_availability(status):
197+
self._attr_is_on = status != PanelHealth.NOMINAL
198+
199+
self._attr_extra_state_attributes["status"] = {
200+
PanelHealth.NOMINAL: "Nominal",
201+
PanelHealth.DEGRADED: "Degraded",
202+
PanelHealth.OUT_OF_ORDER: "Out of order"
203+
}[status]
197204

198205
@staticmethod
199206
def supports_feature_set(feature_class: FeatureClass) -> bool:

custom_components/powertag_gateway/entity_base.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ async def tag_device_info(
7171
"name": await client.tag_name(modbus_index),
7272
}
7373
if not is_unreachable:
74-
kwargs["suggested_area"] = (await client.tag_usage(modbus_index)).name
74+
usage = await client.tag_usage(modbus_index)
75+
if usage is not None:
76+
kwargs["suggested_area"] = usage.name
7577

7678
if not is_unreachable and logging.DEBUG >= _LOGGER.level:
7779
position = await client.tag_position(modbus_index)
@@ -146,6 +148,10 @@ def __init__(
146148
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
147149
raise NotImplementedError()
148150

151+
def _handle_availability(self, value: object):
152+
self._attr_available = value is not None
153+
return self._attr_available
154+
149155

150156
class WirelessDeviceEntity(Entity):
151157
def __init__(
@@ -180,6 +186,10 @@ def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
180186
def supports_firmware_version(firmware_version: str) -> bool:
181187
return True
182188

189+
def _handle_availability(self, value: object):
190+
self._attr_available = value is not None
191+
return self._attr_available
192+
183193

184194
def collect_entities(
185195
client: SchneiderModbus,

custom_components/powertag_gateway/schneider_modbus.py

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pymodbus.constants import DeviceInformation # type: ignore
99
from pymodbus.pdu import ExceptionResponse # type: ignore
1010
from pymodbus.client.mixin import ModbusClientMixin # type: ignore
11+
from pymodbus.exceptions import ModbusIOException # type: ignore
1112

1213
GATEWAY_SLAVE_ID = 255
1314
SYNTHESIS_TABLE_SLAVE_ID_START = 247
@@ -249,26 +250,26 @@ async def firmware_version(self) -> str | None:
249250

250251
# Status
251252

252-
async def status(self) -> LinkStatus:
253+
async def status(self) -> LinkStatus | None:
253254
"""PowerTag Link gateway status and diagnostic register"""
254255
assert self.type_of_gateway in [
255256
TypeOfGateway.POWERTAG_LINK,
256257
TypeOfGateway.SMARTLINK,
257258
]
258259
bitmap = self.__read_int_16(0x0070, GATEWAY_SLAVE_ID)
259260
try:
260-
return LinkStatus(bitmap)
261+
return LinkStatus(bitmap) if bitmap is not None else None
261262
except Exception as e:
262263
_LOGGER.error(
263264
f"Could not map status, defaulting to GENERAL_FAILURE ({str(e)}"
264265
)
265266
return LinkStatus.GENERAL_FAILURE
266267

267-
async def health(self) -> PanelHealth:
268+
async def health(self) -> PanelHealth | None:
268269
"""PowerTag Link gateway status and diagnostic register"""
269270
assert self.type_of_gateway == TypeOfGateway.PANEL_SERVER
270271
code = await self.__read_int_16(0x009E, GATEWAY_SLAVE_ID)
271-
return PanelHealth(code)
272+
return PanelHealth(code) if code is not None else None
272273

273274
# Date and Time
274275

@@ -332,9 +333,10 @@ async def tag_power_factor_total(self, tag_index: int) -> float | None:
332333

333334
async def tag_power_factor_sign_convention(
334335
self, tag_index: int
335-
) -> PowerFactorSignConvention:
336+
) -> PowerFactorSignConvention | None:
336337
"""Power factor sign convention"""
337-
return PowerFactorSignConvention(await self.__read_int_16(0xC0D, tag_index))
338+
power_factor_sign = await self.__read_int_16(0xC0D, tag_index)
339+
return PowerFactorSignConvention(power_factor_sign) if power_factor_sign is not None else None
338340

339341
# Frequency Metering Data
340342

@@ -553,13 +555,18 @@ async def tag_power_active_demand_total_maximum_timestamp(
553555
async def tag_is_alarm_valid(self, tag_index: int) -> AlarmDetails | bool | None:
554556
"""Validity of the alarm bitmap"""
555557
if self.type_of_gateway is TypeOfGateway.PANEL_SERVER:
556-
return AlarmDetails(await self.__read_int_32(0xCE1, tag_index) or 0)
558+
alarm_valid = await self.__read_int_32(0xCE1, tag_index)
559+
return AlarmDetails(alarm_valid) if alarm_valid is not None else None
557560
else:
558-
return (await self.__read_int_32(0xCE1, tag_index) & 0b1) != 0
561+
alarm_valid = await self.__read_int_32(0xCE1, tag_index)
562+
if alarm_valid is None:
563+
return None
564+
return (alarm_valid & 0b1) != 0
559565

560-
async def tag_get_alarm(self, tag_index: int) -> AlarmDetails:
566+
async def tag_get_alarm(self, tag_index: int) -> AlarmDetails | None:
561567
"""Alarms"""
562-
return AlarmDetails(await self.__read_int_32(0xCE3, tag_index) or 0)
568+
alarm = await self.__read_int_32(0xCE3, tag_index)
569+
return AlarmDetails(alarm) if alarm is not None else None
563570

564571
async def tag_current_at_voltage_loss(
565572
self, tag_index: int, phase: Phase
@@ -593,21 +600,25 @@ async def tag_circuit(self, tag_index: int) -> str | None:
593600
"""Circuit identifier of the wireless device. The user can enter maximum five characters."""
594601
return await self.__read_string(0x7922, 3, tag_index)
595602

596-
async def tag_usage(self, tag_index: int) -> DeviceUsage:
603+
async def tag_usage(self, tag_index: int) -> DeviceUsage | None:
597604
"""Indicates the usage of the wireless device."""
598-
return DeviceUsage(await self.__read_int_16(0x7925, tag_index))
605+
usage = await self.__read_int_16(0x7925, tag_index)
606+
return DeviceUsage(usage) if usage is not None else None
599607

600-
async def tag_phase_sequence(self, tag_index: int) -> PhaseSequence:
608+
async def tag_phase_sequence(self, tag_index: int) -> PhaseSequence | None:
601609
"""Phase sequence."""
602-
return PhaseSequence(await self.__read_int_16(0x7926, tag_index))
610+
sequence = await self.__read_int_16(0x7926, tag_index)
611+
return PhaseSequence(sequence) if sequence is not None else None
603612

604-
async def tag_position(self, tag_index: int) -> Position:
613+
async def tag_position(self, tag_index: int) -> Position | None:
605614
"""Mounting position"""
606-
return Position(await self.__read_int_16(0x7927, tag_index))
615+
position = await self.__read_int_16(0x7927, tag_index)
616+
return Position(position) if position is not None else None
607617

608-
async def tag_circuit_diagnostic(self, tag_index: int) -> Position:
618+
async def tag_circuit_diagnostic(self, tag_index: int) -> Position | None:
609619
"""Circuit diagnostics"""
610-
return Position(await self.__read_int_16(0x7928, tag_index))
620+
position = await self.__read_int_16(0x7928, tag_index)
621+
return Position(position) if position is not None else None
611622

612623
async def tag_rated_current(self, tag_index: int) -> int | None:
613624
"""Rated current of the protective device to the wireless device"""
@@ -617,6 +628,8 @@ async def tag_electrical_network_system_type(
617628
self, tag_index: int
618629
) -> ElectricalNetworkSystemType | None:
619630
code = await self.__read_int_16(0x792A, tag_index)
631+
if code is None:
632+
return None
620633
system_type = [e for e in ElectricalNetworkSystemType if e.value[0] == code]
621634
return system_type[0] if system_type else None
622635

@@ -630,11 +643,12 @@ async def tag_reset_peak_demands(self, tag_index: int):
630643
"""Reset All Peak Demands"""
631644
await self.__write_int_16(0x792E, tag_index, 1)
632645

633-
async def tag_power_supply_type(self, tag_index: int) -> Position:
646+
async def tag_power_supply_type(self, tag_index: int) -> Position | None:
634647
"""Power supply type"""
635648
if self.type_of_gateway == TypeOfGateway.SMARTLINK:
636649
return Position.INVALID
637-
return Position(await self.__read_int_16(0x792F, tag_index))
650+
position = await self.__read_int_16(0x792F, tag_index)
651+
return Position(position) if position is not None else None
638652

639653
# Device identification
640654

@@ -928,16 +942,15 @@ async def __async_read(
928942
timeout=5.0,
929943
)
930944
if result.isError():
931-
_LOGGER.debug(
932-
f"Modbus error reading {address} from slave ID {slave_id}"
933-
)
945+
_LOGGER.debug(f"Modbus error reading {address} from slave ID {slave_id}")
934946
return None
935947
return result.registers
936948

937949
except asyncio.TimeoutError:
938-
_LOGGER.debug(
939-
f"Timeout when fetching address {address} from slave ID {slave_id}"
940-
)
950+
_LOGGER.debug(f"Timeout when fetching address {address} from slave ID {slave_id}")
951+
return None
952+
except ModbusIOException as e:
953+
_LOGGER.error(f"Error when fetching {address} from slave ID {slave_id}: {e}")
941954
return None
942955

943956
async def __async_write(
@@ -958,6 +971,7 @@ async def __async_write(
958971
_LOGGER.debug(
959972
f"Timeout when writing to address {address} to slave ID {slave_id}"
960973
)
974+
return None
961975

962976
async def __identify(self, _: int):
963977
# data = self.client.read_device_information(read_code=DeviceInformation.REGULAR, device_id=0xFF)
@@ -983,6 +997,8 @@ async def __write_string(self, address: int, slave_id: int, string: str):
983997

984998
async def __read_float_32(self, address: int, slave_id: int) -> float | None:
985999
registers = await self.__async_read(address, 2, slave_id)
1000+
if registers is None:
1001+
return None
9861002
result = self.client.convert_from_registers(
9871003
registers, ModbusClientMixin.DATATYPE.FLOAT32
9881004
)
@@ -994,6 +1010,8 @@ async def __read_float_32(self, address: int, slave_id: int) -> float | None:
9941010

9951011
async def __read_int_16(self, address: int, slave_id: int) -> int | None:
9961012
registers = await self.__async_read(address, 1, slave_id)
1013+
if registers is None:
1014+
return None
9971015
result = self.client.convert_from_registers(
9981016
registers, ModbusClientMixin.DATATYPE.UINT16
9991017
)
@@ -1007,13 +1025,17 @@ async def __write_int_16(self, address: int, slave_id: int, value: int):
10071025

10081026
async def __read_int_32(self, address: int, slave_id: int) -> int | None:
10091027
registers = await self.__async_read(address, 2, slave_id)
1028+
if registers is None:
1029+
return None
10101030
result = self.client.convert_from_registers(
10111031
registers, ModbusClientMixin.DATATYPE.UINT32
10121032
)
10131033
return result if result != 0x8000_0000 else None
10141034

10151035
async def __read_int_64(self, address: int, slave_id: int) -> int | None:
10161036
registers = await self.__async_read(address, 4, slave_id)
1037+
if registers is None:
1038+
return None
10171039
result = self.client.convert_from_registers(
10181040
registers, ModbusClientMixin.DATATYPE.UINT64
10191041
)
@@ -1027,6 +1049,8 @@ async def __write_int_64(self, address: int, slave_id: int, value: int):
10271049

10281050
async def __read_date_time(self, address: int, slave_id) -> datetime | None:
10291051
registers = await self.__async_read(address, 4, slave_id)
1052+
if registers is None:
1053+
return None
10301054

10311055
year_raw = self.client.convert_from_registers(
10321056
registers[0:1], ModbusClientMixin.DATATYPE.UINT16

0 commit comments

Comments
 (0)