From 37d9ed899d2cc8a4c2dbfe8681716e825f75baab Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 2 May 2024 16:16:26 +0200 Subject: [PATCH] Add `binary_sensor` platform to IMGW-PIB integration (#116624) * Add binary_sensor platform * Add tests * Remove state attributes * Remove attrs from strings.json * Use base entity class --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/__init__.py | 2 +- .../components/imgw_pib/binary_sensor.py | 82 ++++++++ homeassistant/components/imgw_pib/entity.py | 22 ++ homeassistant/components/imgw_pib/icons.json | 14 ++ homeassistant/components/imgw_pib/sensor.py | 10 +- .../components/imgw_pib/strings.json | 8 + .../snapshots/test_binary_sensor.ambr | 195 ++++++++++++++++++ .../components/imgw_pib/test_binary_sensor.py | 65 ++++++ 8 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/imgw_pib/binary_sensor.py create mode 100644 homeassistant/components/imgw_pib/entity.py create mode 100644 tests/components/imgw_pib/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/imgw_pib/test_binary_sensor.py diff --git a/homeassistant/components/imgw_pib/__init__.py b/homeassistant/components/imgw_pib/__init__.py index f3dd66eb23d..54511e76020 100644 --- a/homeassistant/components/imgw_pib/__init__.py +++ b/homeassistant/components/imgw_pib/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_STATION_ID from .coordinator import ImgwPibDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/imgw_pib/binary_sensor.py b/homeassistant/components/imgw_pib/binary_sensor.py new file mode 100644 index 00000000000..1c4cc738f8f --- /dev/null +++ b/homeassistant/components/imgw_pib/binary_sensor.py @@ -0,0 +1,82 @@ +"""IMGW-PIB binary sensor platform.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from imgw_pib.model import HydrologicalData + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ImgwPibConfigEntry +from .coordinator import ImgwPibDataUpdateCoordinator +from .entity import ImgwPibEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class ImgwPibBinarySensorEntityDescription(BinarySensorEntityDescription): + """IMGW-PIB sensor entity description.""" + + value: Callable[[HydrologicalData], bool | None] + + +BINARY_SENSOR_TYPES: tuple[ImgwPibBinarySensorEntityDescription, ...] = ( + ImgwPibBinarySensorEntityDescription( + key="flood_warning", + translation_key="flood_warning", + device_class=BinarySensorDeviceClass.SAFETY, + value=lambda data: data.flood_warning, + ), + ImgwPibBinarySensorEntityDescription( + key="flood_alarm", + translation_key="flood_alarm", + device_class=BinarySensorDeviceClass.SAFETY, + value=lambda data: data.flood_alarm, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ImgwPibConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add a IMGW-PIB binary sensor entity from a config_entry.""" + coordinator = entry.runtime_data.coordinator + + async_add_entities( + ImgwPibBinarySensorEntity(coordinator, description) + for description in BINARY_SENSOR_TYPES + if getattr(coordinator.data, description.key) is not None + ) + + +class ImgwPibBinarySensorEntity(ImgwPibEntity, BinarySensorEntity): + """Define IMGW-PIB binary sensor entity.""" + + entity_description: ImgwPibBinarySensorEntityDescription + + def __init__( + self, + coordinator: ImgwPibDataUpdateCoordinator, + description: ImgwPibBinarySensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{coordinator.station_id}_{description.key}" + self.entity_description = description + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/imgw_pib/entity.py b/homeassistant/components/imgw_pib/entity.py new file mode 100644 index 00000000000..ef55c0e9a4e --- /dev/null +++ b/homeassistant/components/imgw_pib/entity.py @@ -0,0 +1,22 @@ +"""Define the IMGW-PIB entity.""" + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION +from .coordinator import ImgwPibDataUpdateCoordinator + + +class ImgwPibEntity(CoordinatorEntity[ImgwPibDataUpdateCoordinator]): + """Define IMGW-PIB entity.""" + + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ImgwPibDataUpdateCoordinator, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/imgw_pib/icons.json b/homeassistant/components/imgw_pib/icons.json index 29aa19a4b56..7ad72efca80 100644 --- a/homeassistant/components/imgw_pib/icons.json +++ b/homeassistant/components/imgw_pib/icons.json @@ -1,5 +1,19 @@ { "entity": { + "binary_sensor": { + "flood_warning": { + "default": "mdi:check-circle", + "state": { + "on": "mdi:home-flood" + } + }, + "flood_alarm": { + "default": "mdi:check-circle", + "state": { + "on": "mdi:home-flood" + } + } + }, "sensor": { "water_level": { "default": "mdi:waves" diff --git a/homeassistant/components/imgw_pib/sensor.py b/homeassistant/components/imgw_pib/sensor.py index 1df651faa52..d3f2162c056 100644 --- a/homeassistant/components/imgw_pib/sensor.py +++ b/homeassistant/components/imgw_pib/sensor.py @@ -17,11 +17,10 @@ from homeassistant.const import UnitOfLength, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ImgwPibConfigEntry -from .const import ATTRIBUTION from .coordinator import ImgwPibDataUpdateCoordinator +from .entity import ImgwPibEntity PARALLEL_UPDATES = 1 @@ -70,13 +69,9 @@ async def async_setup_entry( ) -class ImgwPibSensorEntity( - CoordinatorEntity[ImgwPibDataUpdateCoordinator], SensorEntity -): +class ImgwPibSensorEntity(ImgwPibEntity, SensorEntity): """Define IMGW-PIB sensor entity.""" - _attr_attribution = ATTRIBUTION - _attr_has_entity_name = True entity_description: ImgwPibSensorEntityDescription def __init__( @@ -88,7 +83,6 @@ class ImgwPibSensorEntity( super().__init__(coordinator) self._attr_unique_id = f"{coordinator.station_id}_{description.key}" - self._attr_device_info = coordinator.device_info self.entity_description = description @property diff --git a/homeassistant/components/imgw_pib/strings.json b/homeassistant/components/imgw_pib/strings.json index 9a17dcf7087..b4246861d4c 100644 --- a/homeassistant/components/imgw_pib/strings.json +++ b/homeassistant/components/imgw_pib/strings.json @@ -17,6 +17,14 @@ } }, "entity": { + "binary_sensor": { + "flood_alarm": { + "name": "Flood alarm" + }, + "flood_warning": { + "name": "Flood warning" + } + }, "sensor": { "water_level": { "name": "Water level" diff --git a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..f314a4be590 --- /dev/null +++ b/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr @@ -0,0 +1,195 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-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': None, + 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood alarm', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_alarm', + 'unique_id': '123_flood_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'River Name (Station Name) Flood alarm', + }), + 'context': , + 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-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': None, + 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood warning', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_warning', + 'unique_id': '123_flood_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'River Name (Station Name) Flood warning', + }), + 'context': , + 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-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': None, + 'entity_id': 'binary_sensor.station_name_flood_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood alarm', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_alarm', + 'unique_id': '123_flood_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'alarm_level': 630.0, + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'Station Name Flood alarm', + }), + 'context': , + 'entity_id': 'binary_sensor.station_name_flood_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_warning-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': None, + 'entity_id': 'binary_sensor.station_name_flood_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flood warning', + 'platform': 'imgw_pib', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flood_warning', + 'unique_id': '123_flood_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.station_name_flood_warning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by IMGW-PIB', + 'device_class': 'safety', + 'friendly_name': 'Station Name Flood warning', + 'warning_level': 590.0, + }), + 'context': , + 'entity_id': 'binary_sensor.station_name_flood_warning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/imgw_pib/test_binary_sensor.py b/tests/components/imgw_pib/test_binary_sensor.py new file mode 100644 index 00000000000..185d4b18575 --- /dev/null +++ b/tests/components/imgw_pib/test_binary_sensor.py @@ -0,0 +1,65 @@ +"""Test the IMGW-PIB binary sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from imgw_pib import ApiError +from syrupy import SnapshotAssertion + +from homeassistant.components.imgw_pib.const import UPDATE_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +ENTITY_ID = "binary_sensor.river_name_station_name_flood_alarm" + + +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_imgw_pib_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test states of the binary sensor.""" + with patch("homeassistant.components.imgw_pib.PLATFORMS", [Platform.BINARY_SENSOR]): + await init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_availability( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_imgw_pib_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure that we mark the entities unavailable correctly when service is offline.""" + await init_integration(hass, mock_config_entry) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "off" + + mock_imgw_pib_client.get_hydrological_data.side_effect = ApiError("API Error") + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_imgw_pib_client.get_hydrological_data.side_effect = None + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "off"