mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Add smoke detector extended properties to homematicip_cloud (#161629)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -42,6 +42,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .helpers import smoke_detector_channel_data_exists
|
||||
|
||||
ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
|
||||
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
|
||||
@@ -125,6 +126,8 @@ async def async_setup_entry(
|
||||
entities.append(HomematicipPresenceDetector(hap, device))
|
||||
if isinstance(device, SmokeDetector):
|
||||
entities.append(HomematicipSmokeDetector(hap, device))
|
||||
if smoke_detector_channel_data_exists(device, "chamberDegraded"):
|
||||
entities.append(HomematicipSmokeDetectorChamberDegraded(hap, device))
|
||||
if isinstance(device, WaterSensor):
|
||||
entities.append(HomematicipWaterDetector(hap, device))
|
||||
if isinstance(device, (RainSensor, WeatherSensorPlus, WeatherSensorPro)):
|
||||
@@ -322,6 +325,23 @@ class HomematicipSmokeDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
return False
|
||||
|
||||
|
||||
class HomematicipSmokeDetectorChamberDegraded(
|
||||
HomematicipGenericEntity, BinarySensorEntity
|
||||
):
|
||||
"""Representation of the HomematicIP smoke detector chamber health."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize smoke detector chamber health sensor."""
|
||||
super().__init__(hap, device, post="Chamber Degraded")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if smoke chamber is degraded."""
|
||||
return self._device.chamberDegraded
|
||||
|
||||
|
||||
class HomematicipWaterDetector(HomematicipGenericEntity, BinarySensorEntity):
|
||||
"""Representation of the HomematicIP water detector."""
|
||||
|
||||
|
||||
@@ -59,3 +59,16 @@ def get_channels_from_device(device: Device, channel_type: FunctionalChannelType
|
||||
for ch in device.functionalChannels
|
||||
if ch.functionalChannelType == channel_type
|
||||
]
|
||||
|
||||
|
||||
def smoke_detector_channel_data_exists(device: Device, field: str) -> bool:
|
||||
"""Check if a smoke detector's channel payload contains a specific field.
|
||||
|
||||
The library always initializes device attributes with defaults, so hasattr
|
||||
cannot distinguish between actual API data and defaults. This checks the
|
||||
raw channel payload to determine if the field was actually sent by the API.
|
||||
"""
|
||||
channels = get_channels_from_device(
|
||||
device, FunctionalChannelType.SMOKE_DETECTOR_CHANNEL
|
||||
)
|
||||
return bool(channels and field in getattr(channels[0], "_rawJSONData", {}))
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import FunctionalChannelType, ValveState
|
||||
@@ -27,6 +29,7 @@ from homematicip.device import (
|
||||
PassageDetector,
|
||||
PresenceDetectorIndoor,
|
||||
RoomControlDeviceAnalog,
|
||||
SmokeDetector,
|
||||
SwitchMeasuring,
|
||||
TemperatureDifferenceSensor2,
|
||||
TemperatureHumiditySensorDisplay,
|
||||
@@ -43,6 +46,7 @@ from homematicip.device import (
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -65,7 +69,70 @@ from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .helpers import get_channels_from_device
|
||||
from .helpers import get_channels_from_device, smoke_detector_channel_data_exists
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HmipSmokeDetectorSensorDescription(SensorEntityDescription):
|
||||
"""Describes HmIP smoke detector sensor entity."""
|
||||
|
||||
value_fn: Callable[[SmokeDetector], StateType | datetime]
|
||||
channel_field: str # Field name in the raw channel payload
|
||||
|
||||
|
||||
SMOKE_DETECTOR_SENSORS: tuple[HmipSmokeDetectorSensorDescription, ...] = (
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="dirt_level",
|
||||
translation_key="smoke_detector_dirt_level",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="dirtLevel",
|
||||
value_fn=lambda d: (
|
||||
round(d.dirtLevel * 100, 1) if d.dirtLevel is not None else None
|
||||
),
|
||||
),
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="smoke_alarm_counter",
|
||||
translation_key="smoke_detector_alarm_counter",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="smokeAlarmCounter",
|
||||
value_fn=lambda d: d.smokeAlarmCounter,
|
||||
),
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="smoke_test_counter",
|
||||
translation_key="smoke_detector_test_counter",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="smokeTestCounter",
|
||||
value_fn=lambda d: d.smokeTestCounter,
|
||||
),
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="last_smoke_alarm",
|
||||
translation_key="smoke_detector_last_alarm",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="lastSmokeAlarmTimestamp",
|
||||
value_fn=lambda d: (
|
||||
datetime.fromtimestamp(d.lastSmokeAlarmTimestamp / 1000, tz=UTC)
|
||||
if d.lastSmokeAlarmTimestamp
|
||||
else None
|
||||
),
|
||||
),
|
||||
HmipSmokeDetectorSensorDescription(
|
||||
key="last_smoke_test",
|
||||
translation_key="smoke_detector_last_test",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_registry_enabled_default=False,
|
||||
channel_field="lastSmokeTestTimestamp",
|
||||
value_fn=lambda d: (
|
||||
datetime.fromtimestamp(d.lastSmokeTestTimestamp / 1000, tz=UTC)
|
||||
if d.lastSmokeTestTimestamp
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
|
||||
ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle"
|
||||
@@ -289,6 +356,15 @@ async def async_setup_entry(
|
||||
and getattr(channel, "valvePosition", None) is not None
|
||||
)
|
||||
|
||||
# Handle smoke detector extended sensors (e.g., HmIP-SWSD-2)
|
||||
entities.extend(
|
||||
HmipSmokeDetectorSensor(hap, device, description)
|
||||
for device in hap.home.devices
|
||||
if isinstance(device, SmokeDetector)
|
||||
for description in SMOKE_DETECTOR_SENSORS
|
||||
if smoke_detector_channel_data_exists(device, description.channel_field)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -936,6 +1012,33 @@ class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEnt
|
||||
return state_attr
|
||||
|
||||
|
||||
class HmipSmokeDetectorSensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Sensor for HomematicIP smoke detector extended properties."""
|
||||
|
||||
entity_description: HmipSmokeDetectorSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hap: HomematicipHAP,
|
||||
device: SmokeDetector,
|
||||
description: HmipSmokeDetectorSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the smoke detector sensor."""
|
||||
super().__init__(hap, device, post=description.key)
|
||||
self.entity_description = description
|
||||
self._sensor_unique_id = f"{device.id}_{description.key}"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self._sensor_unique_id
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the sensor value."""
|
||||
return self.entity_description.value_fn(self._device)
|
||||
|
||||
|
||||
def _get_wind_direction(wind_direction_degree: float) -> str:
|
||||
"""Convert wind direction degree to named direction."""
|
||||
if 11.25 <= wind_direction_degree < 33.75:
|
||||
|
||||
@@ -29,6 +29,21 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"smoke_detector_alarm_counter": {
|
||||
"name": "Alarm counter"
|
||||
},
|
||||
"smoke_detector_dirt_level": {
|
||||
"name": "Dirt level"
|
||||
},
|
||||
"smoke_detector_last_alarm": {
|
||||
"name": "Last alarm"
|
||||
},
|
||||
"smoke_detector_last_test": {
|
||||
"name": "Last test"
|
||||
},
|
||||
"smoke_detector_test_counter": {
|
||||
"name": "Test counter"
|
||||
},
|
||||
"tilt_state": {
|
||||
"state": {
|
||||
"neutral": "Neutral",
|
||||
|
||||
@@ -5377,7 +5377,13 @@
|
||||
],
|
||||
"index": 1,
|
||||
"label": "",
|
||||
"smokeDetectorAlarmType": "IDLE_OFF"
|
||||
"smokeDetectorAlarmType": "IDLE_OFF",
|
||||
"chamberDegraded": false,
|
||||
"dirtLevel": 0.15,
|
||||
"smokeAlarmCounter": 2,
|
||||
"smokeTestCounter": 5,
|
||||
"lastSmokeAlarmTimestamp": 1704067200000,
|
||||
"lastSmokeTestTimestamp": 1706745600000
|
||||
}
|
||||
},
|
||||
"homeId": "00000000-0000-0000-0000-000000000001",
|
||||
|
||||
@@ -328,6 +328,27 @@ async def test_hmip_smoke_detector(
|
||||
assert ha_state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_hmip_smoke_detector_chamber_degraded(
|
||||
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
|
||||
) -> None:
|
||||
"""Test HomematicipSmokeDetectorChamberDegraded."""
|
||||
entity_id = "binary_sensor.rauchwarnmelder_chamber_degraded"
|
||||
entity_name = "Rauchwarnmelder Chamber Degraded"
|
||||
device_model = "HmIP-SWSD"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Rauchwarnmelder"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
hass, mock_hap, entity_id, entity_name, device_model
|
||||
)
|
||||
|
||||
assert ha_state.state == STATE_OFF
|
||||
await async_manipulate_test_data(hass, hmip_device, "chamberDegraded", True)
|
||||
ha_state = hass.states.get(entity_id)
|
||||
assert ha_state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_hmip_water_detector(
|
||||
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
|
||||
) -> None:
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(
|
||||
test_devices=None, test_groups=None
|
||||
)
|
||||
|
||||
assert len(mock_hap.hmip_device_by_entity_id) == 342
|
||||
assert len(mock_hap.hmip_device_by_entity_id) == 343
|
||||
|
||||
|
||||
async def test_hmip_remove_device(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from homematicip.base.enums import ValveState
|
||||
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN
|
||||
from homeassistant.components.homematicip_cloud.entity import (
|
||||
ATTR_CONFIG_PENDING,
|
||||
ATTR_DEVICE_OVERHEATED,
|
||||
@@ -39,6 +40,7 @@ from homeassistant.const import (
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics
|
||||
|
||||
@@ -861,3 +863,111 @@ async def test_hmip_water_valve_water_volume_since_open(
|
||||
assert ha_state.state == "67.0"
|
||||
assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfVolume.LITERS
|
||||
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
|
||||
async def test_hmip_smoke_detector_dirt_level(
|
||||
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
|
||||
) -> None:
|
||||
"""Test HomematicipSmokeDetectorDirtLevel."""
|
||||
entity_id = "sensor.rauchwarnmelder_dirt_level"
|
||||
entity_name = "Rauchwarnmelder dirt_level"
|
||||
device_model = "HmIP-SWSD"
|
||||
|
||||
# Pre-register the entity as enabled before platform loads
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
"3014F7110000000000000018_dirt_level",
|
||||
suggested_object_id="rauchwarnmelder_dirt_level",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Rauchwarnmelder"]
|
||||
)
|
||||
|
||||
# Need to wait for entity to be added after enabling
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
hass, mock_hap, entity_id, entity_name, device_model
|
||||
)
|
||||
|
||||
assert ha_state.state == "15.0"
|
||||
assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
|
||||
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
||||
|
||||
await async_manipulate_test_data(hass, hmip_device, "dirtLevel", 0.25)
|
||||
ha_state = hass.states.get(entity_id)
|
||||
assert ha_state.state == "25.0"
|
||||
|
||||
|
||||
async def test_hmip_smoke_detector_alarm_counter(
|
||||
hass: HomeAssistant,
|
||||
default_mock_hap_factory: HomeFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test HomematicipSmokeDetectorAlarmCounter."""
|
||||
entity_id = "sensor.rauchwarnmelder_smoke_alarm_counter"
|
||||
entity_name = "Rauchwarnmelder smoke_alarm_counter"
|
||||
device_model = "HmIP-SWSD"
|
||||
|
||||
# Pre-register the entity as enabled before platform loads
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
"3014F7110000000000000018_smoke_alarm_counter",
|
||||
suggested_object_id="rauchwarnmelder_smoke_alarm_counter",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Rauchwarnmelder"]
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
hass, mock_hap, entity_id, entity_name, device_model
|
||||
)
|
||||
|
||||
assert ha_state.state == "2"
|
||||
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
await async_manipulate_test_data(hass, hmip_device, "smokeAlarmCounter", 3)
|
||||
ha_state = hass.states.get(entity_id)
|
||||
assert ha_state.state == "3"
|
||||
|
||||
|
||||
async def test_hmip_smoke_detector_test_counter(
|
||||
hass: HomeAssistant,
|
||||
default_mock_hap_factory: HomeFactory,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test HomematicipSmokeDetectorTestCounter."""
|
||||
entity_id = "sensor.rauchwarnmelder_smoke_test_counter"
|
||||
entity_name = "Rauchwarnmelder smoke_test_counter"
|
||||
device_model = "HmIP-SWSD"
|
||||
|
||||
# Pre-register the entity as enabled before platform loads
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
"3014F7110000000000000018_smoke_test_counter",
|
||||
suggested_object_id="rauchwarnmelder_smoke_test_counter",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Rauchwarnmelder"]
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ha_state, _hmip_device = get_and_check_entity_basics(
|
||||
hass, mock_hap, entity_id, entity_name, device_model
|
||||
)
|
||||
|
||||
assert ha_state.state == "5"
|
||||
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
Reference in New Issue
Block a user