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>
pull/116621/head^2
Maciej Bieniek 2024-05-02 16:16:26 +02:00 committed by GitHub
parent 1ec7a515d2
commit 37d9ed899d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 389 additions and 9 deletions

View File

@ -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__)

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -17,6 +17,14 @@
}
},
"entity": {
"binary_sensor": {
"flood_alarm": {
"name": "Flood alarm"
},
"flood_warning": {
"name": "Flood warning"
}
},
"sensor": {
"water_level": {
"name": "Water level"

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'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': <ANY>,
'entity_id': 'binary_sensor.river_name_station_name_flood_alarm',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'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': <ANY>,
'entity_id': 'binary_sensor.river_name_station_name_flood_warning',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor[binary_sensor.station_name_flood_alarm-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'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': <ANY>,
'entity_id': 'binary_sensor.station_name_flood_alarm',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor[binary_sensor.station_name_flood_warning-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'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': <ANY>,
'entity_id': 'binary_sensor.station_name_flood_warning',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -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"