mirror of
https://github.com/home-assistant/core.git
synced 2025-09-06 05:11:35 +02:00
Add binary sensor platform to qbus integration (#149975)
This commit is contained in:
144
homeassistant/components/qbus/binary_sensor.py
Normal file
144
homeassistant/components/qbus/binary_sensor.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Support for Qbus binary sensor."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttDevice, QbusMqttOutput
|
||||
from qbusmqttapi.factory import QbusMqttTopicFactory
|
||||
from qbusmqttapi.state import QbusMqttDeviceState, QbusMqttWeatherState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import QbusConfigEntry
|
||||
from .entity import (
|
||||
QbusEntity,
|
||||
create_device_identifier,
|
||||
create_unique_id,
|
||||
determine_new_outputs,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class QbusWeatherDescription(BinarySensorEntityDescription):
|
||||
"""Description for Qbus weather entities."""
|
||||
|
||||
property: str
|
||||
|
||||
|
||||
_WEATHER_DESCRIPTIONS = (
|
||||
QbusWeatherDescription(
|
||||
key="raining",
|
||||
property="raining",
|
||||
translation_key="raining",
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="twilight",
|
||||
property="twilight",
|
||||
translation_key="twilight",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensor entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
added_outputs: list[QbusMqttOutput] = []
|
||||
added_controllers: list[str] = []
|
||||
|
||||
def _create_weather_entities() -> list[BinarySensorEntity]:
|
||||
new_outputs = determine_new_outputs(
|
||||
coordinator, added_outputs, lambda output: output.type == "weatherstation"
|
||||
)
|
||||
|
||||
return [
|
||||
QbusWeatherBinarySensor(output, description)
|
||||
for output in new_outputs
|
||||
for description in _WEATHER_DESCRIPTIONS
|
||||
]
|
||||
|
||||
def _create_controller_entities() -> list[BinarySensorEntity]:
|
||||
if coordinator.data and coordinator.data.id not in added_controllers:
|
||||
added_controllers.extend(coordinator.data.id)
|
||||
return [QbusControllerConnectedBinarySensor(coordinator.data)]
|
||||
|
||||
return []
|
||||
|
||||
def _check_outputs() -> None:
|
||||
entities = [*_create_weather_entities(), *_create_controller_entities()]
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_outputs()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
|
||||
|
||||
|
||||
class QbusWeatherBinarySensor(QbusEntity, BinarySensorEntity):
|
||||
"""Representation of a Qbus weather binary sensor."""
|
||||
|
||||
_state_cls = QbusMqttWeatherState
|
||||
|
||||
entity_description: QbusWeatherDescription
|
||||
|
||||
def __init__(
|
||||
self, mqtt_output: QbusMqttOutput, description: QbusWeatherDescription
|
||||
) -> None:
|
||||
"""Initialize binary sensor entity."""
|
||||
|
||||
super().__init__(mqtt_output, id_suffix=description.key)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
async def _handle_state_received(self, state: QbusMqttWeatherState) -> None:
|
||||
if value := state.read_property(self.entity_description.property, None):
|
||||
self._attr_is_on = (
|
||||
None if value is None else cast(str, value).lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
class QbusControllerConnectedBinarySensor(BinarySensorEntity):
|
||||
"""Representation of the Qbus controller connected sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
|
||||
def __init__(self, controller: QbusMqttDevice) -> None:
|
||||
"""Initialize binary sensor entity."""
|
||||
self._controller = controller
|
||||
|
||||
self._attr_unique_id = create_unique_id(controller.serial_number, "connected")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={create_device_identifier(controller)}
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
topic = QbusMqttTopicFactory().get_device_state_topic(self._controller.id)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{topic}",
|
||||
self._state_received,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _state_received(self, state: QbusMqttDeviceState) -> None:
|
||||
self._attr_is_on = state.properties.connected if state.properties else None
|
||||
self.async_schedule_update_ha_state()
|
@@ -6,6 +6,7 @@ from homeassistant.const import Platform
|
||||
|
||||
DOMAIN: Final = "qbus"
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
|
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from qbusmqttapi.discovery import QbusDiscovery, QbusMqttDevice, QbusMqttOutput
|
||||
from qbusmqttapi.discovery import QbusDiscovery, QbusMqttDevice
|
||||
from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory
|
||||
|
||||
from homeassistant.components.mqtt import (
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
@@ -32,7 +33,7 @@ type QbusConfigEntry = ConfigEntry[QbusControllerCoordinator]
|
||||
QBUS_KEY: HassKey[QbusConfigCoordinator] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]):
|
||||
class QbusControllerCoordinator(DataUpdateCoordinator[QbusMqttDevice | None]):
|
||||
"""Qbus data coordinator."""
|
||||
|
||||
_STATE_REQUEST_DELAY = 3
|
||||
@@ -63,8 +64,8 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]):
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> list[QbusMqttOutput]:
|
||||
return self._controller.outputs if self._controller else []
|
||||
async def _async_update_data(self) -> QbusMqttDevice | None:
|
||||
return self._controller
|
||||
|
||||
def shutdown(self, event: Event | None = None) -> None:
|
||||
"""Shutdown Qbus coordinator."""
|
||||
@@ -140,14 +141,19 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]):
|
||||
"%s - Receiving controller state %s", self.config_entry.unique_id, msg.topic
|
||||
)
|
||||
|
||||
if self._controller is None or self._controller_activated:
|
||||
if self._controller is None:
|
||||
return
|
||||
|
||||
state = self._message_factory.parse_device_state(msg.payload)
|
||||
|
||||
if state and state.properties and state.properties.connectable is False:
|
||||
if state and state.properties:
|
||||
async_dispatcher_send(self.hass, f"{DOMAIN}_{msg.topic}", state)
|
||||
|
||||
if not self._controller_activated and state.properties.connectable is False:
|
||||
_LOGGER.debug(
|
||||
"%s - Activating controller %s", self.config_entry.unique_id, state.id
|
||||
"%s - Activating controller %s",
|
||||
self.config_entry.unique_id,
|
||||
state.id,
|
||||
)
|
||||
self._controller_activated = True
|
||||
request = self._message_factory.create_device_activate_request(
|
||||
|
@@ -7,7 +7,7 @@ from collections.abc import Callable
|
||||
import re
|
||||
from typing import Generic, TypeVar, cast
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttOutput
|
||||
from qbusmqttapi.discovery import QbusMqttDevice, QbusMqttOutput
|
||||
from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory
|
||||
from qbusmqttapi.state import QbusMqttState
|
||||
|
||||
@@ -44,11 +44,15 @@ def determine_new_outputs(
|
||||
|
||||
added_ref_ids = {k.ref_id for k in added_outputs}
|
||||
|
||||
new_outputs = [
|
||||
new_outputs = (
|
||||
[
|
||||
output
|
||||
for output in coordinator.data
|
||||
for output in coordinator.data.outputs
|
||||
if filter_fn(output) and output.ref_id not in added_ref_ids
|
||||
]
|
||||
if coordinator.data
|
||||
else []
|
||||
)
|
||||
|
||||
if new_outputs:
|
||||
added_outputs.extend(new_outputs)
|
||||
@@ -64,9 +68,14 @@ def format_ref_id(ref_id: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def create_main_device_identifier(mqtt_output: QbusMqttOutput) -> tuple[str, str]:
|
||||
"""Create the identifier referring to the main device this output belongs to."""
|
||||
return (DOMAIN, format_mac(mqtt_output.device.mac))
|
||||
def create_device_identifier(mqtt_device: QbusMqttDevice) -> tuple[str, str]:
|
||||
"""Create the device identifier."""
|
||||
return (DOMAIN, format_mac(mqtt_device.mac))
|
||||
|
||||
|
||||
def create_unique_id(serial_number: str, suffix: str) -> str:
|
||||
"""Create the unique id."""
|
||||
return f"ctd_{serial_number}_{suffix}"
|
||||
|
||||
|
||||
class QbusEntity(Entity, Generic[StateT], ABC):
|
||||
@@ -95,16 +104,18 @@ class QbusEntity(Entity, Generic[StateT], ABC):
|
||||
)
|
||||
|
||||
ref_id = format_ref_id(mqtt_output.ref_id)
|
||||
unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}"
|
||||
suffix = ref_id or ""
|
||||
|
||||
if id_suffix:
|
||||
unique_id += f"_{id_suffix}"
|
||||
suffix += f"_{id_suffix}"
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_unique_id = create_unique_id(
|
||||
mqtt_output.device.serial_number, suffix
|
||||
)
|
||||
|
||||
if link_to_main_device:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={create_main_device_identifier(mqtt_output)}
|
||||
identifiers={create_device_identifier(mqtt_output.device)}
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@@ -112,7 +123,7 @@ class QbusEntity(Entity, Generic[StateT], ABC):
|
||||
manufacturer=MANUFACTURER,
|
||||
identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")},
|
||||
suggested_area=mqtt_output.location.title(),
|
||||
via_device=create_main_device_identifier(mqtt_output),
|
||||
via_device=create_device_identifier(mqtt_output.device),
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
12
homeassistant/components/qbus/icons.json
Normal file
12
homeassistant/components/qbus/icons.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"raining": {
|
||||
"default": "mdi:weather-pouring"
|
||||
},
|
||||
"twilight": {
|
||||
"default": "mdi:weather-sunset"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,6 +17,14 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"raining": {
|
||||
"name": "Raining"
|
||||
},
|
||||
"twilight": {
|
||||
"name": "Twilight"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"daylight": {
|
||||
"name": "Daylight"
|
||||
|
146
tests/components/qbus/snapshots/test_binary_sensor.ambr
Normal file
146
tests/components/qbus/snapshots/test_binary_sensor.ambr
Normal file
@@ -0,0 +1,146 @@
|
||||
# serializer version: 1
|
||||
# name: test_binary_sensor[binary_sensor.ctd_000001-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.ctd_000001',
|
||||
'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': None,
|
||||
'platform': 'qbus',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'ctd_000001_connected',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.ctd_000001-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'connectivity',
|
||||
'friendly_name': 'CTD 000001',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.ctd_000001',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.weersensor_raining-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.weersensor_raining',
|
||||
'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': 'Raining',
|
||||
'platform': 'qbus',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'raining',
|
||||
'unique_id': 'ctd_000001_21007_raining',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.weersensor_raining-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Weersensor Raining',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.weersensor_raining',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.weersensor_twilight-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.weersensor_twilight',
|
||||
'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': 'Twilight',
|
||||
'platform': 'qbus',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'twilight',
|
||||
'unique_id': 'ctd_000001_21007_twilight',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.weersensor_twilight-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Weersensor Twilight',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.weersensor_twilight',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
27
tests/components/qbus/test_binary_sensor.py
Normal file
27
tests/components/qbus/test_binary_sensor.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Test Qbus binary sensors."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
setup_integration_deferred: Callable[[], Awaitable],
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test binary sensor."""
|
||||
|
||||
with patch("homeassistant.components.qbus.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
await setup_integration_deferred()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
Reference in New Issue
Block a user