Files
ha-core/homeassistant/components/matter/binary_sensor.py
Ludovic BOUÉ 4adf5ce826 Support for Matter 1.4 Water Heater device type (#131505)
* Create water_heater.json

* Update water_heater.json

* Update water_heater.json

* TankVolume

* TankPercentage

* WaterHeaterMode

WaterHeaterMode

* Update sensor.py

* ruff-format

* Update water_heater.json

 Attributes of WaterHeaterManagement Cluster on Endpoint 2
ClusterId 148 (0x0094)

* Update test_sensor.py

water_heater fixture

* Update test_sensor.py

* SensorDeviceClass=VOLUME_STORAGE for `TankVolume`

* `BoostStateEnum` map

* WaterHeaterManagementBoostState

* Update sensor.py

* WaterHeaterManagementEstimatedHeatRequired

* Fix UnitOfEnergy

* Format

* Add `device_types.WaterHeater` to Climate

* Strings for Tank sensors

* WaterHeater icons

* Update icons.json

* Update strings.json

* Update water_heater.json

* ruff-format

* Fix tests

* Fix sensor.py

* Fix icons

* WaterHeaterManagementEstimatedHeatRequired

* WaterHeaterManagementBoostState

* BoostState as a binary sensor

* ElectricalPowerMeasurement values

* Fix tests

* Create water_heater.py

* Update climate.py from dev branch

* Resolve conflicts

* ruff-format

* Add Platform.WATER_HEATER

* Update water_heater.py

* Update water_heater.py

* Update water_heater.py

* Update water_heater.py

* Add WaterHeaterManagement sensors

* Update tests

* Add select test

* Add strings

* First try with water_heater

* Testing current_operation

* BoostState attribute

* target_temperature attributes

* target_temperature attribute

* set_temperature and set_operation_mode

* turn_on / turn_off

* Trigger Boost command

* Fix WaterHeaterBoostInfoStruct

* Add test file

* Add climate cluster to fixture

* Add climate cluster to fixture

* Add tests

* Add ON_OFF feature

* Update tests

* Update tests

* Translate WaterHeaterMode

* Change description

* Update test and snapshots

* Update snapshots

* Set entity name to None to make the device name be the name of the entity

* Format

* Update water_heater.py

* Fix format

* ruff-format

* Import ServiceValidationError

* Update homeassistant/components/matter/water_heater.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update water_heater.py

* Update test_water_heater.py

* Update test_water_heater.ambr

* Update test_water_heater.py

* Update select.py

* Update snapshots

* Rename to boost_info

* Set WaterHeaterMode

* Update snapshots

* Update snapshots

* Fix for warning
W7431: Argument 3 should be of type AddConfigEntryEntitiesCallback in async_setup_entry (hass-argument-type)

* Update strings.json

* Update strings and tests

* Fix missing brace

* Update tests

* fix test

* Updates strings

* Fix async_set_temperature

* Update tests

* Update tests

* Update homeassistant/components/matter/water_heater.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Sort strings in strings.json

* Update homeassistant/components/matter/water_heater.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove unused line

* Remove min/max target temperatures

* Remove BOOST_STATE_MAP

* Add comment

* Remove SUPPORT_FLAGS_HEATER

* Remove system_mode_value check

* Update homeassistant/components/matter/water_heater.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Reformat async_set_temperature()

* Update snapshots

* Remove MatterWaterHeaterMode selector

* Update snapshots

* Rename test to test_water_heater_set_temperature

* Add test_water_heater_set_operation_mode

* Remove reset_mock

* Update tests/components/matter/test_water_heater.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add test_update_from_water_heater

* Add test_water_heater_turn_on_off

* Add test_water_heater_boostmode

* Fix SystemMode value for STATE_HIGH_DEMAND

* Add disable boost from water heater device side test

* Remove unused lines

* Remove unused lines

* Fix test indentation

* Fix water heater tests

* Check for None

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-25 15:28:28 +02:00

338 lines
14 KiB
Python

"""Matter binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
from chip.clusters import Objects as clusters
from chip.clusters.Objects import uint
from chip.clusters.Types import Nullable, NullValue
from matter_server.client.models import device_types
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import MatterEntity, MatterEntityDescription
from .helpers import get_matter
from .models import MatterDiscoverySchema
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Matter binary sensor from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.BINARY_SENSOR, async_add_entities)
@dataclass(frozen=True)
class MatterBinarySensorEntityDescription(
BinarySensorEntityDescription, MatterEntityDescription
):
"""Describe Matter binary sensor entities."""
class MatterBinarySensor(MatterEntity, BinarySensorEntity):
"""Representation of a Matter binary sensor."""
entity_description: MatterBinarySensorEntityDescription
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value: bool | uint | int | Nullable | None
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value in (None, NullValue):
value = None
elif value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
if TYPE_CHECKING:
value = cast(bool | None, value)
self._attr_is_on = value
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
# device specific: translate Hue motion to sensor to HA Motion sensor
# instead of generic occupancy sensor
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="HueMotionSensor",
device_class=BinarySensorDeviceClass.MOTION,
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
vendor_id=(4107,),
product_name=("Hue motion sensor",),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="OccupancySensor",
device_class=BinarySensorDeviceClass.OCCUPANCY,
# The first bit = if occupied
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="BatteryChargeLevel",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
measurement_to_ha=lambda x: x
!= clusters.PowerSource.Enums.BatChargeLevelEnum.kOk,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.PowerSource.Attributes.BatChargeLevel,),
# only add binary battery sensor if a regular percentage based is not available
absent_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
),
# BooleanState sensors (tied to device type)
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ContactSensor",
device_class=BinarySensorDeviceClass.DOOR,
# value is inverted on matter to what we expect
measurement_to_ha=lambda x: not x,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.ContactSensor,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WaterLeakDetector",
translation_key="water_leak",
device_class=BinarySensorDeviceClass.MOISTURE,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.WaterLeakDetector,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WaterFreezeDetector",
translation_key="water_freeze",
device_class=BinarySensorDeviceClass.COLD,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.WaterFreezeDetector,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="RainSensor",
translation_key="rain",
device_class=BinarySensorDeviceClass.MOISTURE,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.RainSensor,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="LockDoorStateSensor",
device_class=BinarySensorDeviceClass.DOOR,
measurement_to_ha={
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmDeviceMutedSensor",
measurement_to_ha=lambda x: (
x == clusters.SmokeCoAlarm.Enums.MuteStateEnum.kMuted
),
translation_key="muted",
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.DeviceMuted,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmEndfOfServiceSensor",
measurement_to_ha=lambda x: (
x == clusters.SmokeCoAlarm.Enums.EndOfServiceEnum.kExpired
),
translation_key="end_of_service",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.EndOfServiceAlert,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmBatteryAlertSensor",
measurement_to_ha=lambda x: (
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
),
translation_key="battery_alert",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.BatteryAlert,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmTestInProgressSensor",
translation_key="test_in_progress",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.TestInProgress,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmHardwareFaultAlertSensor",
translation_key="hardware_fault",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.HardwareFaultAlert,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmSmokeStateSensor",
device_class=BinarySensorDeviceClass.SMOKE,
measurement_to_ha=lambda x: (
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.SmokeState,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmInterconnectSmokeAlarmSensor",
device_class=BinarySensorDeviceClass.SMOKE,
measurement_to_ha=lambda x: (
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
),
translation_key="interconnected_smoke_alarm",
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.InterconnectSmokeAlarm,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="SmokeCoAlarmInterconnectCOAlarmSensor",
device_class=BinarySensorDeviceClass.CO,
measurement_to_ha=lambda x: (
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
),
translation_key="interconnected_co_alarm",
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.InterconnectCOAlarm,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvseChargingStatusSensor",
translation_key="evse_charging_status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
measurement_to_ha={
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDischarging: False,
clusters.EnergyEvse.Enums.StateEnum.kSessionEnding: False,
clusters.EnergyEvse.Enums.StateEnum.kFault: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.State,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvsePlugStateSensor",
translation_key="evse_plug_state",
device_class=BinarySensorDeviceClass.PLUG,
measurement_to_ha={
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDischarging: True,
clusters.EnergyEvse.Enums.StateEnum.kSessionEnding: False,
clusters.EnergyEvse.Enums.StateEnum.kFault: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.State,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvseSupplyStateSensor",
translation_key="evse_supply_charging_state",
device_class=BinarySensorDeviceClass.RUNNING,
measurement_to_ha={
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.SupplyState,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WaterHeaterManagementBoostStateSensor",
translation_key="boost_state",
measurement_to_ha=lambda x: (
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,),
),
]