diff --git a/homeassistant/components/smlight/binary_sensor.py b/homeassistant/components/smlight/binary_sensor.py index b010c3f7cbd..b5c695617eb 100644 --- a/homeassistant/components/smlight/binary_sensor.py +++ b/homeassistant/components/smlight/binary_sensor.py @@ -6,6 +6,8 @@ from _collections_abc import Callable from dataclasses import dataclass from pysmlight import Sensors +from pysmlight.const import Events as SmEvents +from pysmlight.sse import MessageEvent from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -14,12 +16,15 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import SCAN_INTERNET_INTERVAL from .coordinator import SmDataUpdateCoordinator from .entity import SmEntity +SCAN_INTERVAL = SCAN_INTERNET_INTERVAL + @dataclass(frozen=True, kw_only=True) class SmBinarySensorEntityDescription(BinarySensorEntityDescription): @@ -52,7 +57,13 @@ async def async_setup_entry( coordinator = entry.runtime_data async_add_entities( - SmBinarySensorEntity(coordinator, description) for description in SENSORS + [ + *( + SmBinarySensorEntity(coordinator, description) + for description in SENSORS + ), + SmInternetSensorEntity(coordinator), + ] ) @@ -78,3 +89,47 @@ class SmBinarySensorEntity(SmEntity, BinarySensorEntity): def is_on(self) -> bool: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data.sensors) + + +class SmInternetSensorEntity(SmEntity, BinarySensorEntity): + """Representation of the SLZB internet sensor.""" + + _attr_translation_key = "internet" + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + coordinator: SmDataUpdateCoordinator, + ) -> None: + """Initialize slzb binary sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.unique_id}_{self._attr_translation_key}" + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.client.sse.register_callback( + SmEvents.EVENT_INET_STATE, self.internet_callback + ) + ) + await self.async_update() + + @callback + def internet_callback(self, event: MessageEvent) -> None: + """Update internet state from event.""" + self._attr_is_on = event.data == "ok" + self.async_write_ha_state() + + @property + def should_poll(self) -> bool: + """Poll entity for internet connected updates.""" + return True + + async def async_update(self) -> None: + """Update the sensor. + + This is an async api, device will respond with EVENT_INET_STATE event. + """ + await self.coordinator.client.get_param("inetState") diff --git a/homeassistant/components/smlight/const.py b/homeassistant/components/smlight/const.py index 791b00c3e93..a49ac009a50 100644 --- a/homeassistant/components/smlight/const.py +++ b/homeassistant/components/smlight/const.py @@ -9,4 +9,5 @@ ATTR_MANUFACTURER = "SMLIGHT" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=300) +SCAN_INTERNET_INTERVAL = timedelta(minutes=15) UPTIME_DEVIATION = timedelta(seconds=5) diff --git a/homeassistant/components/smlight/strings.json b/homeassistant/components/smlight/strings.json index ad36711528b..425815f68f0 100644 --- a/homeassistant/components/smlight/strings.json +++ b/homeassistant/components/smlight/strings.json @@ -46,6 +46,9 @@ "ethernet": { "name": "Ethernet" }, + "internet": { + "name": "Internet" + }, "wifi": { "name": "Wi-Fi" } diff --git a/tests/components/smlight/snapshots/test_binary_sensor.ambr b/tests/components/smlight/snapshots/test_binary_sensor.ambr index 5ea936f9647..17dca1c9784 100644 --- a/tests/components/smlight/snapshots/test_binary_sensor.ambr +++ b/tests/components/smlight/snapshots/test_binary_sensor.ambr @@ -46,6 +46,53 @@ 'state': 'on', }) # --- +# name: test_all_binary_sensors[binary_sensor.mock_title_internet-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_title_internet', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Internet', + 'platform': 'smlight', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'internet', + 'unique_id': 'aa:bb:cc:dd:ee:ff_internet', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.mock_title_internet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Mock Title Internet', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_internet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_all_binary_sensors[binary_sensor.mock_title_wi_fi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smlight/test_binary_sensor.py b/tests/components/smlight/test_binary_sensor.py index ddf9b01bf16..ce7d4e3ff6d 100644 --- a/tests/components/smlight/test_binary_sensor.py +++ b/tests/components/smlight/test_binary_sensor.py @@ -1,15 +1,22 @@ """Tests for the SMLIGHT binary sensor platform.""" +from collections.abc import Callable +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +from pysmlight.const import Events +from pysmlight.sse import MessageEvent import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.smlight.const import SCAN_INTERNET_INTERVAL +from homeassistant.const import STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .conftest import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform pytestmark = [ pytest.mark.usefixtures( @@ -17,6 +24,14 @@ pytestmark = [ ) ] +MOCK_INET_STATE = MessageEvent( + type="EVENT_INET_STATE", + message="EVENT_INET_STATE", + data="ok", + origin="http://slzb-06.local", + last_event_id="", +) + @pytest.fixture def platforms() -> list[Platform]: @@ -36,6 +51,8 @@ async def test_all_binary_sensors( await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await hass.config_entries.async_unload(entry.entry_id) + async def test_disabled_by_default_sensors( hass: HomeAssistant, @@ -50,3 +67,43 @@ async def test_disabled_by_default_sensors( assert (entry := entity_registry.async_get("binary_sensor.mock_title_wi_fi")) assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +async def test_internet_sensor_event( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test internet sensor event.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.mock_title_internet") + assert state is not None + assert state.state == STATE_UNKNOWN + + assert len(mock_smlight_client.get_param.mock_calls) == 1 + mock_smlight_client.get_param.assert_called_with("inetState") + + freezer.tick(SCAN_INTERNET_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(mock_smlight_client.get_param.mock_calls) == 2 + mock_smlight_client.get_param.assert_called_with("inetState") + + event_function: Callable[[MessageEvent], None] = next( + ( + call_args[0][1] + for call_args in mock_smlight_client.sse.register_callback.call_args_list + if call_args[0][0] == Events.EVENT_INET_STATE + ), + None, + ) + + event_function(MOCK_INET_STATE) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.mock_title_internet") + assert state is not None + assert state.state == STATE_ON