Skip to content

Commit f73e92a

Browse files
authored
Mark entity unavailable if data can't be fetched (home-assistant#156928)
1 parent 74ad506 commit f73e92a

File tree

13 files changed

+448
-71
lines changed

13 files changed

+448
-71
lines changed

homeassistant/components/lcn/binary_sensor.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,17 @@ def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
7373

7474
async def async_update(self) -> None:
7575
"""Update the state of the entity."""
76-
await self.device_connection.request_status_binary_sensors(
77-
SCAN_INTERVAL.seconds
76+
self._attr_available = (
77+
await self.device_connection.request_status_binary_sensors(
78+
SCAN_INTERVAL.seconds
79+
)
80+
is not None
7881
)
7982

8083
def input_received(self, input_obj: InputType) -> None:
8184
"""Set sensor value when LCN input object (command) is received."""
8285
if not isinstance(input_obj, pypck.inputs.ModStatusBinSensors):
8386
return
84-
87+
self._attr_available = True
8588
self._attr_is_on = input_obj.get_state(self.bin_sensor_port.value)
8689
self.async_write_ha_state()

homeassistant/components/lcn/climate.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,22 @@ async def async_set_temperature(self, **kwargs: Any) -> None:
171171

172172
async def async_update(self) -> None:
173173
"""Update the state of the entity."""
174-
await asyncio.gather(
175-
self.device_connection.request_status_variable(
176-
self.variable, SCAN_INTERVAL.seconds
177-
),
178-
self.device_connection.request_status_variable(
179-
self.setpoint, SCAN_INTERVAL.seconds
180-
),
174+
self._attr_available = any(
175+
await asyncio.gather(
176+
self.device_connection.request_status_variable(
177+
self.variable, SCAN_INTERVAL.seconds
178+
),
179+
self.device_connection.request_status_variable(
180+
self.setpoint, SCAN_INTERVAL.seconds
181+
),
182+
)
181183
)
182184

183185
def input_received(self, input_obj: InputType) -> None:
184186
"""Set temperature value when LCN input object is received."""
185187
if not isinstance(input_obj, pypck.inputs.ModStatusVar):
186188
return
187-
189+
self._attr_available = True
188190
if input_obj.get_var() == self.variable:
189191
self._attr_current_temperature = float(
190192
input_obj.get_value().to_var_unit(self.unit)

homeassistant/components/lcn/cover.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,15 @@ async def async_stop_cover(self, **kwargs: Any) -> None:
133133
async def async_update(self) -> None:
134134
"""Update the state of the entity."""
135135
if not self.device_connection.is_group:
136-
await asyncio.gather(
137-
self.device_connection.request_status_output(
138-
pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds
139-
),
140-
self.device_connection.request_status_output(
141-
pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds
142-
),
136+
self._attr_available = any(
137+
await asyncio.gather(
138+
self.device_connection.request_status_output(
139+
pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds
140+
),
141+
self.device_connection.request_status_output(
142+
pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds
143+
),
144+
)
143145
)
144146

145147
def input_received(self, input_obj: InputType) -> None:
@@ -149,7 +151,7 @@ def input_received(self, input_obj: InputType) -> None:
149151
or input_obj.get_output_id() not in self.output_ids
150152
):
151153
return
152-
154+
self._attr_available = True
153155
if input_obj.get_percent() > 0: # motor is on
154156
if input_obj.get_output_id() == self.output_ids[0]:
155157
self._attr_is_opening = True
@@ -272,11 +274,12 @@ async def async_update(self) -> None:
272274
self.motor, self.positioning_mode, SCAN_INTERVAL.seconds
273275
)
274276
)
275-
await asyncio.gather(*coros)
277+
self._attr_available = any(await asyncio.gather(*coros))
276278

277279
def input_received(self, input_obj: InputType) -> None:
278280
"""Set cover states when LCN input object (command) is received."""
279281
if isinstance(input_obj, pypck.inputs.ModStatusRelays):
282+
self._attr_available = True
280283
self._attr_is_opening = input_obj.is_opening(self.motor.value)
281284
self._attr_is_closing = input_obj.is_closing(self.motor.value)
282285

@@ -293,6 +296,7 @@ def input_received(self, input_obj: InputType) -> None:
293296
)
294297
and input_obj.motor == self.motor.value
295298
):
299+
self._attr_available = True
296300
self._attr_current_cover_position = int(input_obj.position)
297301
if self._attr_current_cover_position in [0, 100]:
298302
self._attr_is_opening = False

homeassistant/components/lcn/light.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,11 @@ async def async_turn_off(self, **kwargs: Any) -> None:
149149

150150
async def async_update(self) -> None:
151151
"""Update the state of the entity."""
152-
await self.device_connection.request_status_output(
153-
self.output, SCAN_INTERVAL.seconds
152+
self._attr_available = (
153+
await self.device_connection.request_status_output(
154+
self.output, SCAN_INTERVAL.seconds
155+
)
156+
is not None
154157
)
155158

156159
def input_received(self, input_obj: InputType) -> None:
@@ -200,12 +203,15 @@ async def async_turn_off(self, **kwargs: Any) -> None:
200203

201204
async def async_update(self) -> None:
202205
"""Update the state of the entity."""
203-
await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)
206+
self._attr_available = (
207+
await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)
208+
is not None
209+
)
204210

205211
def input_received(self, input_obj: InputType) -> None:
206212
"""Set light state when LCN input object (command) is received."""
207213
if not isinstance(input_obj, pypck.inputs.ModStatusRelays):
208214
return
209-
215+
self._attr_available = True
210216
self._attr_is_on = input_obj.get_state(self.output.value)
211217
self.async_write_ha_state()

homeassistant/components/lcn/quality_scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ rules:
2525
status: exempt
2626
comment: Integration has no configuration parameters
2727
docs-installation-parameters: done
28-
entity-unavailable: todo
28+
entity-unavailable: done
2929
integration-owner: done
3030
log-when-unavailable: done
3131
parallel-updates: done

homeassistant/components/lcn/sensor.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,11 @@ def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
133133

134134
async def async_update(self) -> None:
135135
"""Update the state of the entity."""
136-
await self.device_connection.request_status_variable(
137-
self.variable, SCAN_INTERVAL.seconds
136+
self._attr_available = (
137+
await self.device_connection.request_status_variable(
138+
self.variable, SCAN_INTERVAL.seconds
139+
)
140+
is not None
138141
)
139142

140143
def input_received(self, input_obj: InputType) -> None:
@@ -144,7 +147,7 @@ def input_received(self, input_obj: InputType) -> None:
144147
or input_obj.get_var() != self.variable
145148
):
146149
return
147-
150+
self._attr_available = True
148151
is_regulator = self.variable.name in SETPOINTS
149152
self._attr_native_value = input_obj.get_value().to_var_unit(
150153
self.unit, is_regulator
@@ -171,15 +174,18 @@ def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
171174

172175
async def async_update(self) -> None:
173176
"""Update the state of the entity."""
174-
await self.device_connection.request_status_led_and_logic_ops(
175-
SCAN_INTERVAL.seconds
177+
self._attr_available = (
178+
await self.device_connection.request_status_led_and_logic_ops(
179+
SCAN_INTERVAL.seconds
180+
)
181+
is not None
176182
)
177183

178184
def input_received(self, input_obj: InputType) -> None:
179185
"""Set sensor value when LCN input object (command) is received."""
180186
if not isinstance(input_obj, pypck.inputs.ModStatusLedsAndLogicOps):
181187
return
182-
188+
self._attr_available = True
183189
if self.source in pypck.lcn_defs.LedPort:
184190
self._attr_native_value = input_obj.get_led_state(
185191
self.source.value

homeassistant/components/lcn/switch.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ async def async_turn_off(self, **kwargs: Any) -> None:
9595

9696
async def async_update(self) -> None:
9797
"""Update the state of the entity."""
98-
await self.device_connection.request_status_output(
99-
self.output, SCAN_INTERVAL.seconds
98+
self._attr_available = (
99+
await self.device_connection.request_status_output(
100+
self.output, SCAN_INTERVAL.seconds
101+
)
102+
is not None
100103
)
101104

102105
def input_received(self, input_obj: InputType) -> None:
@@ -106,7 +109,7 @@ def input_received(self, input_obj: InputType) -> None:
106109
or input_obj.get_output_id() != self.output.value
107110
):
108111
return
109-
112+
self._attr_available = True
110113
self._attr_is_on = input_obj.get_percent() > 0
111114
self.async_write_ha_state()
112115

@@ -142,13 +145,16 @@ async def async_turn_off(self, **kwargs: Any) -> None:
142145

143146
async def async_update(self) -> None:
144147
"""Update the state of the entity."""
145-
await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)
148+
self._attr_available = (
149+
await self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)
150+
is not None
151+
)
146152

147153
def input_received(self, input_obj: InputType) -> None:
148154
"""Set switch state when LCN input object (command) is received."""
149155
if not isinstance(input_obj, pypck.inputs.ModStatusRelays):
150156
return
151-
157+
self._attr_available = True
152158
self._attr_is_on = input_obj.get_state(self.output.value)
153159
self.async_write_ha_state()
154160

@@ -183,8 +189,11 @@ async def async_turn_off(self, **kwargs: Any) -> None:
183189

184190
async def async_update(self) -> None:
185191
"""Update the state of the entity."""
186-
await self.device_connection.request_status_variable(
187-
self.setpoint_variable, SCAN_INTERVAL.seconds
192+
self._attr_available = (
193+
await self.device_connection.request_status_variable(
194+
self.setpoint_variable, SCAN_INTERVAL.seconds
195+
)
196+
is not None
188197
)
189198

190199
def input_received(self, input_obj: InputType) -> None:
@@ -194,7 +203,7 @@ def input_received(self, input_obj: InputType) -> None:
194203
or input_obj.get_var() != self.setpoint_variable
195204
):
196205
return
197-
206+
self._attr_available = True
198207
self._attr_is_on = input_obj.get_value().is_locked_regulator()
199208
self.async_write_ha_state()
200209

@@ -236,7 +245,12 @@ async def async_turn_off(self, **kwargs: Any) -> None:
236245

237246
async def async_update(self) -> None:
238247
"""Update the state of the entity."""
239-
await self.device_connection.request_status_locked_keys(SCAN_INTERVAL.seconds)
248+
self._attr_available = (
249+
await self.device_connection.request_status_locked_keys(
250+
SCAN_INTERVAL.seconds
251+
)
252+
is not None
253+
)
240254

241255
def input_received(self, input_obj: InputType) -> None:
242256
"""Set switch state when LCN input object (command) is received."""
@@ -245,6 +259,6 @@ def input_received(self, input_obj: InputType) -> None:
245259
or self.key not in pypck.lcn_defs.Key
246260
):
247261
return
248-
262+
self._attr_available = True
249263
self._attr_is_on = input_obj.get_state(self.table_id, self.key_id)
250264
self.async_write_ha_state()

tests/components/lcn/test_binary_sensor.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
from unittest.mock import patch
44

5+
from freezegun.api import FrozenDateTimeFactory
56
from pypck.inputs import ModStatusBinSensors
67
from pypck.lcn_addr import LcnAddr
78
from syrupy.assertion import SnapshotAssertion
89

10+
from homeassistant.components.lcn.binary_sensor import SCAN_INTERVAL
911
from homeassistant.components.lcn.helpers import get_device_connection
1012
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
1113
from homeassistant.core import HomeAssistant
1214
from homeassistant.helpers import entity_registry as er
1315

14-
from .conftest import MockConfigEntry, init_integration
16+
from .conftest import MockConfigEntry, MockDeviceConnection, init_integration
1517

16-
from tests.common import snapshot_platform
18+
from tests.common import async_fire_time_changed, snapshot_platform
1719

1820
BINARY_SENSOR_SENSOR1 = "binary_sensor.testmodule_binary_sensor1"
1921

@@ -61,6 +63,43 @@ async def test_pushed_binsensor_status_change(
6163
assert state.state == STATE_ON
6264

6365

66+
async def test_availability(
67+
hass: HomeAssistant, freezer: FrozenDateTimeFactory, entry: MockConfigEntry
68+
) -> None:
69+
"""Test the availability of binary_sensor entity."""
70+
await init_integration(hass, entry)
71+
72+
state = hass.states.get(BINARY_SENSOR_SENSOR1)
73+
assert state is not None
74+
assert state.state != STATE_UNAVAILABLE
75+
76+
# no response from device -> unavailable
77+
with patch.object(
78+
MockDeviceConnection, "request_status_binary_sensors", return_value=None
79+
):
80+
freezer.tick(SCAN_INTERVAL)
81+
async_fire_time_changed(hass)
82+
await hass.async_block_till_done(wait_background_tasks=True)
83+
84+
state = hass.states.get(BINARY_SENSOR_SENSOR1)
85+
assert state is not None
86+
assert state.state == STATE_UNAVAILABLE
87+
88+
# response from device -> available
89+
with patch.object(
90+
MockDeviceConnection,
91+
"request_status_binary_sensors",
92+
return_value=ModStatusBinSensors(LcnAddr(0, 7, False), [False] * 8),
93+
):
94+
freezer.tick(SCAN_INTERVAL)
95+
async_fire_time_changed(hass)
96+
await hass.async_block_till_done(wait_background_tasks=True)
97+
98+
state = hass.states.get(BINARY_SENSOR_SENSOR1)
99+
assert state is not None
100+
assert state.state != STATE_UNAVAILABLE
101+
102+
64103
async def test_unload_config_entry(hass: HomeAssistant, entry: MockConfigEntry) -> None:
65104
"""Test the binary sensor is removed when the config entry is unloaded."""
66105
await init_integration(hass, entry)

0 commit comments

Comments
 (0)