Add attachment and connection status for IOmeter (#140998)

* add binary sensors

* fix: suggestion value_fn

* add snapshot test and split cases
This commit is contained in:
jukrebs 2025-03-25 13:26:07 +01:00 committed by GitHub
parent 77c210fb87
commit 0ddf3c794b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 343 additions and 7 deletions

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import IOmeterConfigEntry, IOMeterCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: IOmeterConfigEntry) -> bool:

View File

@ -0,0 +1,87 @@
"""IOmeter binary sensor."""
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import IOMeterCoordinator, IOmeterData
from .entity import IOmeterEntity
@dataclass(frozen=True, kw_only=True)
class IOmeterBinarySensorDescription(BinarySensorEntityDescription):
"""Describes Iometer binary sensor entity."""
value_fn: Callable[[IOmeterData], bool | None]
SENSOR_TYPES: list[IOmeterBinarySensorDescription] = [
IOmeterBinarySensorDescription(
key="connection_status",
translation_key="connection_status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_registry_enabled_default=False,
value_fn=lambda data: (
data.status.device.core.connection_status == "connected"
if data.status.device.core.connection_status is not None
else None
),
),
IOmeterBinarySensorDescription(
key="attachment_status",
translation_key="attachment_status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_registry_enabled_default=False,
value_fn=lambda data: (
data.status.device.core.attachment_status == "attached"
if data.status.device.core.attachment_status is not None
else None
),
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Sensors."""
coordinator: IOMeterCoordinator = config_entry.runtime_data
async_add_entities(
IOmeterBinarySensor(
coordinator=coordinator,
description=description,
)
for description in SENSOR_TYPES
)
class IOmeterBinarySensor(IOmeterEntity, BinarySensorEntity):
"""Defines a IOmeter binary sensor."""
entity_description: IOmeterBinarySensorDescription
def __init__(
self,
coordinator: IOMeterCoordinator,
description: IOmeterBinarySensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.identifier}_{description.key}"
@property
def is_on(self) -> bool | None:
"""Return the binary sensor state."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -60,6 +60,14 @@
"wifi_rssi": {
"name": "Signal strength Wi-Fi"
}
},
"binary_sensor": {
"connection_status": {
"name": "Core/Bridge connection status"
},
"attachment_status": {
"name": "Core attachment status"
}
}
}
}

View File

@ -1,13 +1,19 @@
"""Tests for the IOmeter integration."""
from unittest.mock import patch
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
async def setup_platform(
hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform]
) -> MockConfigEntry:
"""Fixture for setting up the IOmeter platform."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
with patch("homeassistant.components.iometer.PLATFORMS", platforms):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -54,4 +54,5 @@ def mock_config_entry() -> MockConfigEntry:
title="IOmeter-1ISK0000000000",
data={CONF_HOST: "10.0.0.2"},
unique_id="658c2b34-2017-45f2-a12b-731235f8bb97",
entry_id="01JQ6G5395176MAAWKAAPEZHV6",
)

View File

@ -0,0 +1,97 @@
# serializer version: 1
# name: test_binary_sensors[binary_sensor.iometer_1isk0000000000_core_attachment_status-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.iometer_1isk0000000000_core_attachment_status',
'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': 'Core attachment status',
'platform': 'iometer',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'attachment_status',
'unique_id': '01JQ6G5395176MAAWKAAPEZHV6_attachment_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.iometer_1isk0000000000_core_attachment_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'IOmeter-1ISK0000000000 Core attachment status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.iometer_1isk0000000000_core_attachment_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_binary_sensors[binary_sensor.iometer_1isk0000000000_core_bridge_connection_status-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.iometer_1isk0000000000_core_bridge_connection_status',
'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': 'Core/Bridge connection status',
'platform': 'iometer',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'connection_status',
'unique_id': '01JQ6G5395176MAAWKAAPEZHV6_connection_status',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.iometer_1isk0000000000_core_bridge_connection_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'IOmeter-1ISK0000000000 Core/Bridge connection status',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.iometer_1isk0000000000_core_bridge_connection_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,135 @@
"""Test the IOmeter binary sensors."""
from datetime import timedelta
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
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 . import setup_platform
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_iometer_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test binary sensors."""
await setup_platform(hass, mock_config_entry, [Platform.BINARY_SENSOR])
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_connection_status_sensors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_iometer_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test connection status sensor."""
await setup_platform(hass, mock_config_entry, [Platform.BINARY_SENSOR])
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_bridge_connection_status"
).state
== STATE_ON
)
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_iometer_client.get_current_status.return_value.device.core.connection_status = "disconnected"
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_bridge_connection_status"
).state
== STATE_OFF
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_attachment_status_sensors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_iometer_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test connection status sensor."""
await setup_platform(hass, mock_config_entry, [Platform.BINARY_SENSOR])
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_attachment_status"
).state
== STATE_ON
)
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_iometer_client.get_current_status.return_value.device.core.attachment_status = "detached"
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_attachment_status"
).state
== STATE_OFF
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_attachment_status_sensors_unkown(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_iometer_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test connection status sensor."""
await setup_platform(hass, mock_config_entry, [Platform.BINARY_SENSOR])
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_attachment_status"
).state
== STATE_ON
)
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_iometer_client.get_current_status.return_value.device.core.attachment_status = None
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get(
"binary_sensor.iometer_1isk0000000000_core_attachment_status"
).state
== STATE_UNKNOWN
)

View File

@ -6,10 +6,11 @@ from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.iometer.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from . import setup_platform
from tests.common import MockConfigEntry, async_fire_time_changed
@ -22,7 +23,8 @@ async def test_new_firmware_version(
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
# await setup_integration(hass, mock_config_entry)
await setup_platform(hass, mock_config_entry, [Platform.SENSOR])
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, mock_config_entry.unique_id)}
)