Add binary_sensor platform to Rehlko (#145391)

* feat: add binary_sensor platform to Rehlko

* feat: add binary sensor platform

* fix: simplify availability logic

* fix: simplify availability logic

* fix: simplify

* fix: rename sensor

* fix: rename sensor

* fix: rename sensor

* fix: remove unneeded type

* fix: rename sensor to 'Auto run'

* fix: use device_class name
This commit is contained in:
Pete Sage 2025-05-22 03:07:38 -04:00 committed by GitHub
parent 613aa9b2cf
commit 1db5c514e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 359 additions and 2 deletions

View File

@ -22,7 +22,7 @@ from .const import (
) )
from .coordinator import RehlkoConfigEntry, RehlkoRuntimeData, RehlkoUpdateCoordinator from .coordinator import RehlkoConfigEntry, RehlkoRuntimeData, RehlkoUpdateCoordinator
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,108 @@
"""Binary sensor platform for Rehlko integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
DEVICE_DATA_DEVICES,
DEVICE_DATA_ID,
DEVICE_DATA_IS_CONNECTED,
GENERATOR_DATA_DEVICE,
)
from .coordinator import RehlkoConfigEntry
from .entity import RehlkoEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class RehlkoBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Class describing Rehlko binary sensor entities."""
on_value: str | bool = True
off_value: str | bool = False
document_key: str | None = None
connectivity_key: str | None = DEVICE_DATA_IS_CONNECTED
BINARY_SENSORS: tuple[RehlkoBinarySensorEntityDescription, ...] = (
RehlkoBinarySensorEntityDescription(
key=DEVICE_DATA_IS_CONNECTED,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
document_key=GENERATOR_DATA_DEVICE,
# Entity is available when the device is disconnected
connectivity_key=None,
),
RehlkoBinarySensorEntityDescription(
key="switchState",
translation_key="auto_run",
on_value="Auto",
off_value="Off",
),
RehlkoBinarySensorEntityDescription(
key="engineOilPressureOk",
translation_key="oil_pressure",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
on_value=False,
off_value=True,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: RehlkoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the binary sensor platform."""
homes = config_entry.runtime_data.homes
coordinators = config_entry.runtime_data.coordinators
async_add_entities(
RehlkoBinarySensorEntity(
coordinators[device_data[DEVICE_DATA_ID]],
device_data[DEVICE_DATA_ID],
device_data,
sensor_description,
document_key=sensor_description.document_key,
connectivity_key=sensor_description.connectivity_key,
)
for home_data in homes
for device_data in home_data[DEVICE_DATA_DEVICES]
for sensor_description in BINARY_SENSORS
)
class RehlkoBinarySensorEntity(RehlkoEntity, BinarySensorEntity):
"""Representation of a Binary Sensor."""
entity_description: RehlkoBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
if self._rehlko_value == self.entity_description.on_value:
return True
if self._rehlko_value == self.entity_description.off_value:
return False
_LOGGER.warning(
"Unexpected value for %s: %s",
self.entity_description.key,
self._rehlko_value,
)
return None

View File

@ -44,6 +44,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
device_data: dict, device_data: dict,
description: EntityDescription, description: EntityDescription,
document_key: str | None = None, document_key: str | None = None,
connectivity_key: str | None = DEVICE_DATA_IS_CONNECTED,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
@ -62,6 +63,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]), connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]),
) )
self._document_key = document_key self._document_key = document_key
self._connectivity_key = connectivity_key
@property @property
def _device_data(self) -> dict[str, Any]: def _device_data(self) -> dict[str, Any]:
@ -80,4 +82,6 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if entity is available.""" """Return if entity is available."""
return super().available and self._device_data[DEVICE_DATA_IS_CONNECTED] return super().available and (
not self._connectivity_key or self._device_data[self._connectivity_key]
)

View File

@ -31,6 +31,14 @@
} }
}, },
"entity": { "entity": {
"binary_sensor": {
"auto_run": {
"name": "Auto run"
},
"oil_pressure": {
"name": "Oil pressure"
}
},
"sensor": { "sensor": {
"engine_speed": { "engine_speed": {
"name": "Engine speed" "name": "Engine speed"

View File

@ -0,0 +1,144 @@
# serializer version: 1
# name: test_sensors[binary_sensor.generator_1_auto_run-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.generator_1_auto_run',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Auto run',
'platform': 'rehlko',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'auto_run',
'unique_id': 'myemail@email.com_12345_switchState',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.generator_1_auto_run-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Generator 1 Auto run',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.generator_1_auto_run',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_sensors[binary_sensor.generator_1_connectivity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.generator_1_connectivity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Connectivity',
'platform': 'rehlko',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'myemail@email.com_12345_isConnected',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.generator_1_connectivity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Generator 1 Connectivity',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.generator_1_connectivity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_sensors[binary_sensor.generator_1_oil_pressure-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.generator_1_oil_pressure',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Oil pressure',
'platform': 'rehlko',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'oil_pressure',
'unique_id': 'myemail@email.com_12345_engineOilPressureOk',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.generator_1_oil_pressure-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'Generator 1 Oil pressure',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.generator_1_oil_pressure',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,93 @@
"""Tests for the Rehlko binary sensors."""
from __future__ import annotations
import logging
from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.rehlko.const import GENERATOR_DATA_DEVICE
from homeassistant.components.rehlko.coordinator import SCAN_INTERVAL_MINUTES
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(name="platform_binary_sensor", autouse=True)
async def platform_binary_sensor_fixture():
"""Patch Rehlko to only load binary_sensor platform."""
with patch("homeassistant.components.rehlko.PLATFORMS", [Platform.BINARY_SENSOR]):
yield
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
rehlko_config_entry: MockConfigEntry,
load_rehlko_config_entry: None,
) -> None:
"""Test the Rehlko binary sensors."""
await snapshot_platform(
hass, entity_registry, snapshot, rehlko_config_entry.entry_id
)
async def test_binary_sensor_states(
hass: HomeAssistant,
generator: dict[str, Any],
mock_rehlko: AsyncMock,
load_rehlko_config_entry: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the Rehlko binary sensor state logic."""
assert generator["engineOilPressureOk"] is True
state = hass.states.get("binary_sensor.generator_1_oil_pressure")
assert state.state == STATE_OFF
generator["engineOilPressureOk"] = False
freezer.tick(SCAN_INTERVAL_MINUTES)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.generator_1_oil_pressure")
assert state.state == STATE_ON
generator["engineOilPressureOk"] = "Unknown State"
with caplog.at_level(logging.WARNING):
caplog.clear()
freezer.tick(SCAN_INTERVAL_MINUTES)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.generator_1_oil_pressure")
assert state.state == STATE_UNKNOWN
assert "Unknown State" in caplog.text
assert "engineOilPressureOk" in caplog.text
async def test_binary_sensor_connectivity_availability(
hass: HomeAssistant,
generator: dict[str, Any],
mock_rehlko: AsyncMock,
load_rehlko_config_entry: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the connectivity entity availability when device is disconnected."""
state = hass.states.get("binary_sensor.generator_1_connectivity")
assert state.state == STATE_ON
# Entity should be available when device is disconnected
generator[GENERATOR_DATA_DEVICE]["isConnected"] = False
freezer.tick(SCAN_INTERVAL_MINUTES)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.generator_1_connectivity")
assert state.state == STATE_OFF