mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Added binary sensor for battery status and external power to the PTDevices integration (#169862)
This commit is contained in:
@@ -11,6 +11,7 @@ from .const import DEFAULT_URL
|
||||
from .coordinator import PTDevicesConfigEntry, PTDevicesCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""PTDevices Binary Sensors."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import override
|
||||
|
||||
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 homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import PTDevicesConfigEntry, PTDevicesCoordinator
|
||||
from .entity import PTDevicesEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class PTDevicesBinarySensors(StrEnum):
|
||||
"""Store keys for PTDevices binary sensors."""
|
||||
|
||||
DEVICE_BATTERY_STATUS = "battery_status"
|
||||
DEVICE_EXTERNAL_POWER = "external_power"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class PTDevicesBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Description for PTDevices binary sensor entities."""
|
||||
|
||||
is_on_fn: Callable[[dict[str, StateType]], bool | None]
|
||||
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS: tuple[PTDevicesBinarySensorEntityDescription, ...] = (
|
||||
PTDevicesBinarySensorEntityDescription(
|
||||
key=PTDevicesBinarySensors.DEVICE_BATTERY_STATUS,
|
||||
translation_key=PTDevicesBinarySensors.DEVICE_BATTERY_STATUS,
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda data: (
|
||||
None
|
||||
if data.get(PTDevicesBinarySensors.DEVICE_BATTERY_STATUS)
|
||||
in (None, "unknown")
|
||||
else data.get(PTDevicesBinarySensors.DEVICE_BATTERY_STATUS) == "low"
|
||||
),
|
||||
),
|
||||
PTDevicesBinarySensorEntityDescription(
|
||||
key=PTDevicesBinarySensors.DEVICE_EXTERNAL_POWER,
|
||||
translation_key=PTDevicesBinarySensors.DEVICE_EXTERNAL_POWER,
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda data: (
|
||||
bool(data.get(PTDevicesBinarySensors.DEVICE_EXTERNAL_POWER))
|
||||
if data.get(PTDevicesBinarySensors.DEVICE_EXTERNAL_POWER) is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: PTDevicesConfigEntry,
|
||||
async_add_entity: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Setup PTDevices binary sensors based on config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
known_sensors: set[tuple[str, str]] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
for device_id in sorted(coordinator.data):
|
||||
device = coordinator.data[device_id]
|
||||
new_sensors = [
|
||||
sensor
|
||||
for sensor in BINARY_SENSOR_DESCRIPTIONS
|
||||
if sensor.key in device and (device_id, sensor.key) not in known_sensors
|
||||
]
|
||||
if not new_sensors:
|
||||
continue
|
||||
known_sensors.update((device_id, sensor.key) for sensor in new_sensors)
|
||||
async_add_entity(
|
||||
PTDevicesBinarySensorEntity(
|
||||
config_entry.runtime_data, sensor, device_id
|
||||
)
|
||||
for sensor in new_sensors
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class PTDevicesBinarySensorEntity(PTDevicesEntity, BinarySensorEntity):
|
||||
"""Defines a PTDevices binary sensor."""
|
||||
|
||||
entity_description: PTDevicesBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PTDevicesCoordinator,
|
||||
description: PTDevicesBinarySensorEntityDescription,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize sensor."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
description.key,
|
||||
device_id,
|
||||
)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
@@ -23,6 +23,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"external_power": {
|
||||
"name": "External power"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"battery_voltage": {
|
||||
"name": "Battery voltage"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"battery_voltage": 5.69,
|
||||
"battery_status": "good",
|
||||
"battery_status_number": 1,
|
||||
"external_power": 1,
|
||||
"volume_level": 2387.837753,
|
||||
"volume_level_oz": 80742.4,
|
||||
"max_volume": 1269,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[binary_sensor.home_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.home_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Battery',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesBinarySensors.DEVICE_BATTERY_STATUS: 'battery_status'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_battery_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[binary_sensor.home_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Battery',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.home_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[binary_sensor.home_external_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.home_external_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'External power',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'External power',
|
||||
'platform': 'ptdevices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PTDevicesBinarySensors.DEVICE_EXTERNAL_POWER: 'external_power'>,
|
||||
'unique_id': '1234_C0FFEEC0FFEE_external_power',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[binary_sensor.home_external_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'power',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home External power',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.home_external_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Test for PTDevices binary sensors."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.ptdevices.coordinator import UPDATE_INTERVAL
|
||||
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_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch(
|
||||
"homeassistant.components.ptdevices._PLATFORMS", [Platform.BINARY_SENSOR]
|
||||
):
|
||||
await setup_integration(hass, mock_ptdevices_config_entry)
|
||||
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_ptdevices_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_battery_status_sensor_states(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test battery status binary sensor state recognition."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2021-01-09 12:00:00+00:00")
|
||||
await setup_integration(hass, mock_ptdevices_config_entry)
|
||||
|
||||
# Make sure the battery status is "normal"
|
||||
assert (state := hass.states.get("binary_sensor.home_battery"))
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Set the new battery status to low
|
||||
mock_ptdevices_interface.get_data.return_value["body"]["C0FFEEC0FFEE"][
|
||||
"battery_status"
|
||||
] = "low"
|
||||
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Make sure the battery status is on (low)
|
||||
assert (state := hass.states.get("binary_sensor.home_battery"))
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_add_remove_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test handling of missing and new binary sensors."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2021-01-09 12:00:00+00:00")
|
||||
await setup_integration(hass, mock_ptdevices_config_entry)
|
||||
|
||||
# Make sure the battery status exists
|
||||
assert (state := hass.states.get("binary_sensor.home_battery"))
|
||||
assert state.state != STATE_UNKNOWN
|
||||
|
||||
# Remove the battery_status
|
||||
mock_ptdevices_interface.get_data.return_value["body"]["C0FFEEC0FFEE"].pop(
|
||||
"battery_status"
|
||||
)
|
||||
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Make sure the battery_status is no longer present
|
||||
assert (state := hass.states.get("binary_sensor.home_battery"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.components.ptdevices.coordinator import UPDATE_INTERVAL
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@@ -29,3 +31,31 @@ async def test_all_entities(
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_ptdevices_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_add_remove_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_ptdevices_interface: AsyncMock,
|
||||
mock_ptdevices_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test handling of missing and new sensors."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2021-01-09 12:00:00+00:00")
|
||||
await setup_integration(hass, mock_ptdevices_config_entry)
|
||||
|
||||
# Make sure the status exists
|
||||
assert (state := hass.states.get("sensor.home_status"))
|
||||
assert state.state != STATE_UNKNOWN
|
||||
|
||||
# Remove the status
|
||||
mock_ptdevices_interface.get_data.return_value["body"]["C0FFEEC0FFEE"].pop("status")
|
||||
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Make sure the status is no longer present
|
||||
assert (state := hass.states.get("sensor.home_status"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
Reference in New Issue
Block a user