Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Matter EVSE devicetype #137189

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
57 changes: 57 additions & 0 deletions homeassistant/components/matter/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,61 @@ def _update_from_device(self) -> None:
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
),
]
1 change: 1 addition & 0 deletions homeassistant/components/matter/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class MatterEntityDescription(EntityDescription):
# convert the value from the primary attribute to the value used by HA
measurement_to_ha: Callable[[Any], Any] | None = None
ha_to_native_value: Callable[[Any], Any] | None = None
command_timeout: int | None = None


class MatterEntity(Entity):
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/matter/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@
},
"battery_replacement_description": {
"default": "mdi:battery-sync-outline"
},
"evse_state": {
"default": "mdi:ev-station"
},
"evse_supply_state": {
"default": "mdi:ev-station"
},
"evse_fault_state": {
"default": "mdi:ev-station"
}
},
"switch": {
Expand All @@ -80,6 +89,9 @@
"on": "mdi:lock",
"off": "mdi:lock-off"
}
},
"evse_charging_switch": {
"default": "mdi:ev-station"
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions homeassistant/components/matter/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
}

EVSE_FAULT_STATE_MAP = {
clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error",
clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverVoltage: "over_voltage",
clusters.EnergyEvse.Enums.FaultStateEnum.kUnderVoltage: "under_voltage",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverCurrent: "over_current",
clusters.EnergyEvse.Enums.FaultStateEnum.kContactWetFailure: "contact_wet_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kContactDryFailure: "contact_dry_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kPowerLoss: "power_loss",
clusters.EnergyEvse.Enums.FaultStateEnum.kPowerQuality: "power_quality",
clusters.EnergyEvse.Enums.FaultStateEnum.kPilotShortCircuit: "pilot_short_circuit",
clusters.EnergyEvse.Enums.FaultStateEnum.kEmergencyStop: "emergency_stop",
clusters.EnergyEvse.Enums.FaultStateEnum.kEVDisconnected: "ev_disconnected",
clusters.EnergyEvse.Enums.FaultStateEnum.kWrongPowerSupply: "wrong_power_supply",
clusters.EnergyEvse.Enums.FaultStateEnum.kLiveNeutralSwap: "live_neutral_swap",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverTemperature: "over_temperature",
clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other",
}


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -904,4 +923,77 @@ def _update_from_device(self) -> None:
# don't discover this entry if the supported state list is empty
secondary_value_is_not=[],
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseFaultState",
translation_key="evse_fault_state",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=list(EVSE_FAULT_STATE_MAP.values()),
measurement_to_ha=EVSE_FAULT_STATE_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.FaultState,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseCircuitCapacity",
translation_key="evse_circuit_capacity",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.CircuitCapacity,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseMinimumChargeCurrent",
translation_key="evse_min_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.MinimumChargeCurrent,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseMaximumChargeCurrent",
translation_key="evse_max_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.MaximumChargeCurrent,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseUserMaximumChargeCurrent",
translation_key="evse_user_max_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent,),
),
]
45 changes: 45 additions & 0 deletions homeassistant/components/matter/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
},
"muted": {
"name": "Muted"
},
"evse_charging_status": {
"name": "Charging status"
},
"evse_supply_charging_state": {
"name": "Supply charging state"
}
},
"button": {
Expand Down Expand Up @@ -275,6 +281,42 @@
},
"current_phase": {
"name": "Current phase"
},
"evse_fault_state": {
"name": "Fault state",
"state": {
"no_error": "OK",
"meter_failure": "Meter failure",
"over_voltage": "Overvoltage",
"under_voltage": "Undervoltage",
"over_current": "Overcurrent",
"contact_wet_failure": "Contact wet failure",
"contact_dry_failure": "Contact dry failure",
"power_loss": "Power loss",
"power_quality": "Power quality",
"pilot_short_circuit": "Pilot short circuit",
"emergency_stop": "Emergency stop",
"ev_disconnected": "EV disconnected",
"wrong_power_supply": "Wrong power supply",
"live_neutral_swap": "Live neutral swap",
"over_temperature": "Overtemperature",
"other": "Other fault"
}
},
"evse_circuit_capacity": {
"name": "Circuit capacity"
},
"evse_charge_current": {
"name": "Charge current"
},
"evse_min_charge_current": {
"name": "Min charge current"
},
"evse_max_charge_current": {
"name": "Max charge current"
},
"evse_user_max_charge_current": {
"name": "User max charge current"
}
},
"switch": {
Expand All @@ -286,6 +328,9 @@
},
"child_lock": {
"name": "Child lock"
},
"evse_charging_switch": {
"name": "Enable charging"
}
},
"vacuum": {
Expand Down
91 changes: 91 additions & 0 deletions homeassistant/components/matter/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from chip.clusters import Objects as clusters
from chip.clusters.Objects import ClusterCommand, NullValue
from matter_server.client.models import device_types

from homeassistant.components.switch import (
Expand All @@ -22,6 +24,13 @@
from .helpers import get_matter
from .models import MatterDiscoverySchema

EVSE_SUPPLY_STATE_MAP = {
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics: False,
}


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -58,6 +67,66 @@ def _update_from_device(self) -> None:
)


class MatterGenericCommandSwitch(MatterEntity, SwitchEntity):
lboue marked this conversation as resolved.
Show resolved Hide resolved
"""Representation of a Matter switch."""

entity_description: MatterGenericCommandSwitchEntityDescription

_platform_translation_key = "switch"

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
if self.entity_description.on_command:
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.on_command(),
self.entity_description.command_timeout,
)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
if self.entity_description.off_command:
await self.send_device_command(
self.entity_description.off_command(),
self.entity_description.command_timeout,
)

@callback
def _update_from_device(self) -> None:
"""Update from device."""
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
self._attr_is_on = value

async def send_device_command(
self,
command: ClusterCommand,
command_timeout: int | None = None,
**kwargs: Any,
) -> None:
"""Send device command with timeout."""
await self.matter_client.send_device_command(
node_id=self._endpoint.node.node_id,
endpoint_id=self._endpoint.endpoint_id,
command=command,
timed_request_timeout_ms=command_timeout,
**kwargs,
)


@dataclass(frozen=True)
class MatterGenericCommandSwitchEntityDescription(
SwitchEntityDescription, MatterEntityDescription
):
"""Describe Matter Generic command Switch entities."""

# command: a custom callback to create the command to send to the device
on_command: Callable[[], Any] | None = None
off_command: Callable[[], Any] | None = None
command_timeout: int | None = None


@dataclass(frozen=True)
class MatterNumericSwitchEntityDescription(
SwitchEntityDescription, MatterEntityDescription
Expand Down Expand Up @@ -194,4 +263,26 @@ def _update_from_device(self) -> None:
),
vendor_id=(4874,),
),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=MatterGenericCommandSwitchEntityDescription(
key="EnergyEvseChargingSwitch",
translation_key="evse_charging_switch",
on_command=lambda: clusters.EnergyEvse.Commands.EnableCharging(
chargingEnabledUntil=NullValue,
minimumChargeCurrent=0,
maximumChargeCurrent=0,
),
off_command=clusters.EnergyEvse.Commands.Disable,
command_timeout=3000,
measurement_to_ha=EVSE_SUPPLY_STATE_MAP.get,
),
entity_class=MatterGenericCommandSwitch,
required_attributes=(
clusters.EnergyEvse.Attributes.SupplyState,
clusters.EnergyEvse.Attributes.AcceptedCommandList,
),
value_contains=clusters.EnergyEvse.Commands.EnableCharging.command_id,
allow_multi=True,
),
]
1 change: 1 addition & 0 deletions tests/components/matter/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ async def integration_fixture(
"pressure_sensor",
"room_airconditioner",
"silabs_dishwasher",
"silabs_evse_charging",
"silabs_laundrywasher",
"smoke_detector",
"switch_unit",
Expand Down
Loading
Loading