From e505ad9003aeb69f8d3dedff1bf41abf49d5b33f Mon Sep 17 00:00:00 2001 From: Robin Lintermann Date: Tue, 24 Feb 2026 23:25:57 +0100 Subject: [PATCH] Update availability of entities when connection changes (#163252) --- homeassistant/components/smarla/entity.py | 26 +++++- .../components/smarla/quality_scale.yaml | 4 +- tests/components/smarla/conftest.py | 1 + tests/components/smarla/test_entity.py | 86 +++++++++++++++++++ 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/components/smarla/test_entity.py diff --git a/homeassistant/components/smarla/entity.py b/homeassistant/components/smarla/entity.py index ba213adc9ab7..59bc9275f29d 100644 --- a/homeassistant/components/smarla/entity.py +++ b/homeassistant/components/smarla/entity.py @@ -1,6 +1,7 @@ """Common base for entities.""" from dataclasses import dataclass +import logging from typing import Any from pysmarlaapi import Federwiege @@ -10,6 +11,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME +_LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class SmarlaEntityDescription(EntityDescription): @@ -30,6 +33,7 @@ class SmarlaBaseEntity(Entity): def __init__(self, federwiege: Federwiege, desc: SmarlaEntityDescription) -> None: """Initialise the entity.""" self.entity_description = desc + self._federwiege = federwiege self._property = federwiege.get_property(desc.service, desc.property) self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}" self._attr_device_info = DeviceInfo( @@ -39,15 +43,35 @@ class SmarlaBaseEntity(Entity): manufacturer=MANUFACTURER_NAME, serial_number=federwiege.serial_number, ) + self._unavailable_logged = False - async def on_change(self, value: Any): + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._federwiege.available + + async def on_availability_change(self, available: bool) -> None: + """Handle availability changes.""" + if not self.available and not self._unavailable_logged: + _LOGGER.info("Entity %s is unavailable", self.entity_id) + self._unavailable_logged = True + elif self.available and self._unavailable_logged: + _LOGGER.info("Entity %s is back online", self.entity_id) + self._unavailable_logged = False + + # Notify ha that state changed + self.async_write_ha_state() + + async def on_change(self, value: Any) -> None: """Notify ha when state changes.""" self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" + await self._federwiege.add_listener(self.on_availability_change) await self._property.add_listener(self.on_change) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" await self._property.remove_listener(self.on_change) + await self._federwiege.remove_listener(self.on_availability_change) diff --git a/homeassistant/components/smarla/quality_scale.yaml b/homeassistant/components/smarla/quality_scale.yaml index 12feaa67350a..7753996a2805 100644 --- a/homeassistant/components/smarla/quality_scale.yaml +++ b/homeassistant/components/smarla/quality_scale.yaml @@ -24,9 +24,9 @@ rules: config-entry-unloading: done docs-configuration-parameters: done docs-installation-parameters: done - entity-unavailable: todo + entity-unavailable: done integration-owner: done - log-when-unavailable: todo + log-when-unavailable: done parallel-updates: done reauthentication-flow: done test-coverage: done diff --git a/tests/components/smarla/conftest.py b/tests/components/smarla/conftest.py index f5ce2bd2588c..49e9723a52b6 100644 --- a/tests/components/smarla/conftest.py +++ b/tests/components/smarla/conftest.py @@ -66,6 +66,7 @@ def mock_federwiege_cls(mock_connection: MagicMock) -> Generator[MagicMock]: ) as mock_federwiege_cls: mock_federwiege = mock_federwiege_cls.return_value mock_federwiege.serial_number = MOCK_ACCESS_TOKEN_JSON["serialNumber"] + mock_federwiege.available = True mock_babywiege_service = MagicMock(spec=Service) mock_babywiege_service.props = { diff --git a/tests/components/smarla/test_entity.py b/tests/components/smarla/test_entity.py new file mode 100644 index 000000000000..1fc493ae9006 --- /dev/null +++ b/tests/components/smarla/test_entity.py @@ -0,0 +1,86 @@ +"""Test Smarla entities.""" + +import logging +from unittest.mock import MagicMock + +import pytest + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import setup_integration, update_property_listeners + +from tests.common import MockConfigEntry + +TEST_ENTITY_ID = "switch.smarla" + + +async def test_entity_availability( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_federwiege: MagicMock, +) -> None: + """Test entity state when device becomes unavailable/available.""" + assert await setup_integration(hass, mock_config_entry) + + # Initially available + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state != STATE_UNAVAILABLE + + # Simulate device becoming unavailable + mock_federwiege.available = False + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + + # Verify state reflects unavailable + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Simulate device becoming available again + mock_federwiege.available = True + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + + # Verify state reflects available again + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state != STATE_UNAVAILABLE + + +async def test_entity_unavailable_logging( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, + mock_federwiege: MagicMock, +) -> None: + """Test logging when device becomes unavailable/available.""" + assert await setup_integration(hass, mock_config_entry) + + caplog.set_level(logging.INFO) + caplog.clear() + + # Verify that log exists when device becomes unavailable + mock_federwiege.available = False + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + assert "is unavailable" in caplog.text + + # Verify that we only log once + caplog.clear() + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + assert "is unavailable" not in caplog.text + + # Verify that log exists when device comes back online + mock_federwiege.available = True + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + assert "back online" in caplog.text + + # Verify that we only log once + caplog.clear() + await update_property_listeners(mock_federwiege) + await hass.async_block_till_done() + assert "back online" not in caplog.text