Update the update coordinator API to make it easier to use (#31471)
* Update the update coordinator API to make it easier to use * failed_last_update -> last_update_success
This commit is contained in:
parent
d407b8e215
commit
0d474e1183
9 changed files with 69 additions and 41 deletions
|
@ -79,17 +79,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
light_coordinator = DataUpdateCoordinator(
|
light_coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
"light",
|
name="light",
|
||||||
partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
||||||
SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
|
request_refresh_debouncer=Debouncer(
|
||||||
|
bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# First do a refresh to see if we can reach the hub.
|
# First do a refresh to see if we can reach the hub.
|
||||||
# Otherwise we will declare not ready.
|
# Otherwise we will declare not ready.
|
||||||
await light_coordinator.async_refresh()
|
await light_coordinator.async_refresh()
|
||||||
|
|
||||||
if light_coordinator.failed_last_update:
|
if not light_coordinator.last_update_success:
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
update_lights = partial(
|
update_lights = partial(
|
||||||
|
@ -122,10 +124,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
group_coordinator = DataUpdateCoordinator(
|
group_coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
"group",
|
name="group",
|
||||||
partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
||||||
SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
|
request_refresh_debouncer=Debouncer(
|
||||||
|
bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
update_groups = partial(
|
update_groups = partial(
|
||||||
|
@ -277,7 +281,7 @@ class HueLight(Light):
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return if light is available."""
|
"""Return if light is available."""
|
||||||
return not self.coordinator.failed_last_update and (
|
return self.coordinator.last_update_success and (
|
||||||
self.is_group
|
self.is_group
|
||||||
or self.bridge.allow_unreachable
|
or self.bridge.allow_unreachable
|
||||||
or self.light.state["reachable"]
|
or self.light.state["reachable"]
|
||||||
|
|
|
@ -42,10 +42,12 @@ class SensorManager:
|
||||||
self.coordinator = DataUpdateCoordinator(
|
self.coordinator = DataUpdateCoordinator(
|
||||||
bridge.hass,
|
bridge.hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
"sensor",
|
name="sensor",
|
||||||
self.async_update_data,
|
update_method=self.async_update_data,
|
||||||
self.SCAN_INTERVAL,
|
update_interval=self.SCAN_INTERVAL,
|
||||||
debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True),
|
request_refresh_debouncer=debounce.Debouncer(
|
||||||
|
bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_update_data(self):
|
async def async_update_data(self):
|
||||||
|
@ -183,7 +185,7 @@ class GenericHueSensor(entity.Entity):
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return if sensor is available."""
|
"""Return if sensor is available."""
|
||||||
return not self.bridge.sensor_manager.coordinator.failed_last_update and (
|
return self.bridge.sensor_manager.coordinator.last_update_success and (
|
||||||
self.bridge.allow_unreachable or self.sensor.config["reachable"]
|
self.bridge.allow_unreachable or self.sensor.config["reachable"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,11 @@ async def async_setup(hass, config):
|
||||||
return Updater(update_available, newest, release_notes)
|
return Updater(update_available, newest, release_notes)
|
||||||
|
|
||||||
coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator(
|
coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator(
|
||||||
hass, _LOGGER, "Home Assistant update", check_new_version, timedelta(days=1)
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="Home Assistant update",
|
||||||
|
update_method=check_new_version,
|
||||||
|
update_interval=timedelta(days=1),
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator.async_refresh()
|
await coordinator.async_refresh()
|
||||||
|
|
|
@ -38,7 +38,7 @@ class UpdaterBinary(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return not self.coordinator.failed_last_update
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Debouncer:
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
*,
|
||||||
cooldown: float,
|
cooldown: float,
|
||||||
immediate: bool,
|
immediate: bool,
|
||||||
function: Optional[Callable[..., Awaitable[Any]]] = None,
|
function: Optional[Callable[..., Awaitable[Any]]] = None,
|
||||||
|
|
|
@ -26,6 +26,7 @@ class DataUpdateCoordinator:
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
|
*,
|
||||||
name: str,
|
name: str,
|
||||||
update_method: Callable[[], Awaitable],
|
update_method: Callable[[], Awaitable],
|
||||||
update_interval: timedelta,
|
update_interval: timedelta,
|
||||||
|
@ -43,16 +44,20 @@ class DataUpdateCoordinator:
|
||||||
self._listeners: List[CALLBACK_TYPE] = []
|
self._listeners: List[CALLBACK_TYPE] = []
|
||||||
self._unsub_refresh: Optional[CALLBACK_TYPE] = None
|
self._unsub_refresh: Optional[CALLBACK_TYPE] = None
|
||||||
self._request_refresh_task: Optional[asyncio.TimerHandle] = None
|
self._request_refresh_task: Optional[asyncio.TimerHandle] = None
|
||||||
self.failed_last_update = False
|
self.last_update_success = True
|
||||||
|
|
||||||
if request_refresh_debouncer is None:
|
if request_refresh_debouncer is None:
|
||||||
request_refresh_debouncer = Debouncer(
|
request_refresh_debouncer = Debouncer(
|
||||||
hass,
|
hass,
|
||||||
logger,
|
logger,
|
||||||
REQUEST_REFRESH_DEFAULT_COOLDOWN,
|
cooldown=REQUEST_REFRESH_DEFAULT_COOLDOWN,
|
||||||
REQUEST_REFRESH_DEFAULT_IMMEDIATE,
|
immediate=REQUEST_REFRESH_DEFAULT_IMMEDIATE,
|
||||||
|
function=self.async_refresh,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
request_refresh_debouncer.function = self.async_refresh
|
||||||
|
|
||||||
self._debounced_refresh = request_refresh_debouncer
|
self._debounced_refresh = request_refresh_debouncer
|
||||||
request_refresh_debouncer.function = self.async_refresh
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
||||||
|
@ -110,19 +115,19 @@ class DataUpdateCoordinator:
|
||||||
self.data = await self.update_method()
|
self.data = await self.update_method()
|
||||||
|
|
||||||
except UpdateFailed as err:
|
except UpdateFailed as err:
|
||||||
if not self.failed_last_update:
|
if self.last_update_success:
|
||||||
self.logger.error("Error fetching %s data: %s", self.name, err)
|
self.logger.error("Error fetching %s data: %s", self.name, err)
|
||||||
self.failed_last_update = True
|
self.last_update_success = False
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
self.failed_last_update = True
|
self.last_update_success = False
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
"Unexpected error fetching %s data: %s", self.name, err
|
"Unexpected error fetching %s data: %s", self.name, err
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if self.failed_last_update:
|
if not self.last_update_success:
|
||||||
self.failed_last_update = False
|
self.last_update_success = True
|
||||||
self.logger.info("Fetching %s data recovered")
|
self.logger.info("Fetching %s data recovered")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -703,7 +703,7 @@ def test_available():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(allow_unreachable=False),
|
bridge=Mock(allow_unreachable=False),
|
||||||
is_group=False,
|
is_group=False,
|
||||||
)
|
)
|
||||||
|
@ -717,7 +717,7 @@ def test_available():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(allow_unreachable=True),
|
bridge=Mock(allow_unreachable=True),
|
||||||
is_group=False,
|
is_group=False,
|
||||||
)
|
)
|
||||||
|
@ -731,7 +731,7 @@ def test_available():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(allow_unreachable=False),
|
bridge=Mock(allow_unreachable=False),
|
||||||
is_group=True,
|
is_group=True,
|
||||||
)
|
)
|
||||||
|
@ -748,7 +748,7 @@ def test_hs_color():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(),
|
bridge=Mock(),
|
||||||
is_group=False,
|
is_group=False,
|
||||||
)
|
)
|
||||||
|
@ -762,7 +762,7 @@ def test_hs_color():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(),
|
bridge=Mock(),
|
||||||
is_group=False,
|
is_group=False,
|
||||||
)
|
)
|
||||||
|
@ -776,7 +776,7 @@ def test_hs_color():
|
||||||
colorgamuttype=LIGHT_GAMUT_TYPE,
|
colorgamuttype=LIGHT_GAMUT_TYPE,
|
||||||
colorgamut=LIGHT_GAMUT,
|
colorgamut=LIGHT_GAMUT,
|
||||||
),
|
),
|
||||||
coordinator=Mock(failed_last_update=False),
|
coordinator=Mock(last_update_success=True),
|
||||||
bridge=Mock(),
|
bridge=Mock(),
|
||||||
is_group=False,
|
is_group=False,
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,11 @@ async def test_immediate_works(hass):
|
||||||
"""Test immediate works."""
|
"""Test immediate works."""
|
||||||
calls = []
|
calls = []
|
||||||
debouncer = debounce.Debouncer(
|
debouncer = debounce.Debouncer(
|
||||||
hass, None, 0.01, True, CoroutineMock(side_effect=lambda: calls.append(None))
|
hass,
|
||||||
|
None,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=True,
|
||||||
|
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
||||||
)
|
)
|
||||||
|
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
|
@ -37,7 +41,11 @@ async def test_not_immediate_works(hass):
|
||||||
"""Test immediate works."""
|
"""Test immediate works."""
|
||||||
calls = []
|
calls = []
|
||||||
debouncer = debounce.Debouncer(
|
debouncer = debounce.Debouncer(
|
||||||
hass, None, 0.01, False, CoroutineMock(side_effect=lambda: calls.append(None))
|
hass,
|
||||||
|
None,
|
||||||
|
cooldown=0.01,
|
||||||
|
immediate=False,
|
||||||
|
function=CoroutineMock(side_effect=lambda: calls.append(None)),
|
||||||
)
|
)
|
||||||
|
|
||||||
await debouncer.async_call()
|
await debouncer.async_call()
|
||||||
|
|
|
@ -23,7 +23,11 @@ def crd(hass):
|
||||||
return len(calls)
|
return len(calls)
|
||||||
|
|
||||||
crd = update_coordinator.DataUpdateCoordinator(
|
crd = update_coordinator.DataUpdateCoordinator(
|
||||||
hass, LOGGER, "test", refresh, timedelta(seconds=10),
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name="test",
|
||||||
|
update_method=refresh,
|
||||||
|
update_interval=timedelta(seconds=10),
|
||||||
)
|
)
|
||||||
return crd
|
return crd
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@ async def test_async_refresh(crd):
|
||||||
assert crd.data is None
|
assert crd.data is None
|
||||||
await crd.async_refresh()
|
await crd.async_refresh()
|
||||||
assert crd.data == 1
|
assert crd.data == 1
|
||||||
assert crd.failed_last_update is False
|
assert crd.last_update_success is True
|
||||||
|
|
||||||
updates = []
|
updates = []
|
||||||
|
|
||||||
|
@ -58,12 +62,12 @@ async def test_request_refresh(crd):
|
||||||
assert crd.data is None
|
assert crd.data is None
|
||||||
await crd.async_request_refresh()
|
await crd.async_request_refresh()
|
||||||
assert crd.data == 1
|
assert crd.data == 1
|
||||||
assert crd.failed_last_update is False
|
assert crd.last_update_success is True
|
||||||
|
|
||||||
# Second time we hit the debonuce
|
# Second time we hit the debonuce
|
||||||
await crd.async_request_refresh()
|
await crd.async_request_refresh()
|
||||||
assert crd.data == 1
|
assert crd.data == 1
|
||||||
assert crd.failed_last_update is False
|
assert crd.last_update_success is True
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_fail(crd, caplog):
|
async def test_refresh_fail(crd, caplog):
|
||||||
|
@ -73,7 +77,7 @@ async def test_refresh_fail(crd, caplog):
|
||||||
await crd.async_refresh()
|
await crd.async_refresh()
|
||||||
|
|
||||||
assert crd.data is None
|
assert crd.data is None
|
||||||
assert crd.failed_last_update is True
|
assert crd.last_update_success is False
|
||||||
assert "Error fetching test data" in caplog.text
|
assert "Error fetching test data" in caplog.text
|
||||||
|
|
||||||
crd.update_method = CoroutineMock(return_value=1)
|
crd.update_method = CoroutineMock(return_value=1)
|
||||||
|
@ -81,7 +85,7 @@ async def test_refresh_fail(crd, caplog):
|
||||||
await crd.async_refresh()
|
await crd.async_refresh()
|
||||||
|
|
||||||
assert crd.data == 1
|
assert crd.data == 1
|
||||||
assert crd.failed_last_update is False
|
assert crd.last_update_success is True
|
||||||
|
|
||||||
crd.update_method = CoroutineMock(side_effect=ValueError)
|
crd.update_method = CoroutineMock(side_effect=ValueError)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
@ -89,7 +93,7 @@ async def test_refresh_fail(crd, caplog):
|
||||||
await crd.async_refresh()
|
await crd.async_refresh()
|
||||||
|
|
||||||
assert crd.data == 1 # value from previous fetch
|
assert crd.data == 1 # value from previous fetch
|
||||||
assert crd.failed_last_update is True
|
assert crd.last_update_success is False
|
||||||
assert "Unexpected error fetching test data" in caplog.text
|
assert "Unexpected error fetching test data" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue