From 63b4fd09c1d6a2b5a7fae73e7f3ad62b6d7165b8 Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 26 Mar 2024 16:29:14 +0800 Subject: [PATCH] Add YoLink Water Meter Support (#114148) * Add YS-5006/5007/5008 Water Meter Support * Add YoLink Water Meter Support * Update .coveragerc * fix as suggestion --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 1 + homeassistant/components/yolink/const.py | 2 + homeassistant/components/yolink/sensor.py | 15 +++ homeassistant/components/yolink/strings.json | 8 ++ homeassistant/components/yolink/valve.py | 115 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 homeassistant/components/yolink/valve.py diff --git a/.coveragerc b/.coveragerc index 67c5887f2da..94b294f71a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1687,6 +1687,7 @@ omit = homeassistant/components/yolink/services.py homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py + homeassistant/components/yolink/valve.py homeassistant/components/youless/__init__.py homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index f1d2ec6602b..fec678ce435 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -46,6 +46,7 @@ PLATFORMS = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.VALVE, ] diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 3d341c8b4fb..110b9cb9810 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -14,3 +14,5 @@ ATTR_REPEAT = "repeat" ATTR_TONE = "tone" YOLINK_EVENT = f"{DOMAIN}_event" YOLINK_OFFLINE_TIME = 32400 + +DEV_MODEL_WATER_METER_YS5007 = "YS5007" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index e1635465bc1..6badeefbdb3 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -24,6 +24,7 @@ from yolink.const import ( ATTR_DEVICE_THERMOSTAT, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_GARAGE_DOOR_CONTROLLER, ) from yolink.device import YoLinkDevice @@ -41,6 +42,7 @@ from homeassistant.const import ( EntityCategory, UnitOfLength, UnitOfTemperature, + UnitOfVolume, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -76,6 +78,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_THERMOSTAT, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, @@ -96,6 +99,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ] MCU_DEV_TEMPERATURE_SENSOR = [ @@ -202,6 +206,17 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfLength.METERS, exists_fn=lambda device: device.device_type in ATTR_DEVICE_WATER_DEPTH_SENSOR, ), + YoLinkSensorEntityDescription( + key="meter_reading", + translation_key="water_meter_reading", + device_class=SensorDeviceClass.WATER, + icon="mdi:gauge", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL_INCREASING, + should_update_entity=lambda value: value is not None, + exists_fn=lambda device: device.device_type + in ATTR_DEVICE_WATER_METER_CONTROLLER, + ), ) diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 83e712328f9..bc8fb435e76 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -73,12 +73,20 @@ "enabled": "[%key:common::state::enabled%]", "disabled": "[%key:common::state::disabled%]" } + }, + "water_meter_reading": { + "name": "Water meter reading" } }, "number": { "config_volume": { "name": "Volume" } + }, + "valve": { + "meter_valve_state": { + "name": "Valve state" + } } }, "services": { diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py new file mode 100644 index 00000000000..a24ad7d385d --- /dev/null +++ b/homeassistant/components/yolink/valve.py @@ -0,0 +1,115 @@ +"""YoLink Valve.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from yolink.client_request import ClientRequest +from yolink.const import ATTR_DEVICE_WATER_METER_CONTROLLER +from yolink.device import YoLinkDevice + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DEV_MODEL_WATER_METER_YS5007, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass(frozen=True) +class YoLinkValveEntityDescription(ValveEntityDescription): + """YoLink ValveEntityDescription.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + value: Callable = lambda state: state + + +DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( + YoLinkValveEntityDescription( + key="valve_state", + translation_key="meter_valve_state", + device_class=ValveDeviceClass.WATER, + value=lambda value: value == "closed" if value is not None else None, + exists_fn=lambda device: device.device_type + == ATTR_DEVICE_WATER_METER_CONTROLLER + and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), + ), +) + +DEVICE_TYPE = [ATTR_DEVICE_WATER_METER_CONTROLLER] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink valve from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id].device_coordinators + valve_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE + ] + async_add_entities( + YoLinkValveEntity(config_entry, valve_device_coordinator, description) + for valve_device_coordinator in valve_device_coordinators + for description in DEVICE_TYPES + if description.exists_fn(valve_device_coordinator.device) + ) + + +class YoLinkValveEntity(YoLinkEntity, ValveEntity): + """YoLink Valve Entity.""" + + entity_description: YoLinkValveEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkValveEntityDescription, + ) -> None: + """Init YoLink valve.""" + super().__init__(config_entry, coordinator) + self._attr_supported_features = ( + ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + ) + self.entity_description = description + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + + @callback + def update_entity_state(self, state: dict[str, str | list[str]]) -> None: + """Update HA Entity State.""" + if ( + attr_val := self.entity_description.value( + state.get(self.entity_description.key) + ) + ) is None: + return + self._attr_is_closed = attr_val + self.async_write_ha_state() + + async def _async_invoke_device(self, state: str) -> None: + """Call setState api to change valve state.""" + await self.call_device(ClientRequest("setState", {"valve": state})) + self._attr_is_closed = state == "close" + self.async_write_ha_state() + + async def async_open_valve(self) -> None: + """Open the valve.""" + await self._async_invoke_device("open") + + async def async_close_valve(self) -> None: + """Close valve.""" + await self._async_invoke_device("close")