Add YoLink new device types support 5009 & 5029 (#144323)

* Leak Stop

* Fix as suggested.
This commit is contained in:
Matrix 2025-05-19 01:59:19 +08:00 committed by GitHub
parent 3ff095cc51
commit 3f59b1c376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 112 additions and 12 deletions

View File

@ -26,7 +26,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.typing import ConfigType
from . import api
from .const import DOMAIN, YOLINK_EVENT
from .const import ATTR_LORA_INFO, DOMAIN, YOLINK_EVENT
from .coordinator import YoLinkCoordinator
from .device_trigger import CONF_LONG_PRESS, CONF_SHORT_PRESS
from .services import async_register_services
@ -72,6 +72,8 @@ class YoLinkHomeMessageListener(MessageListener):
if device_coordinator is None:
return
device_coordinator.dev_online = True
if (loraInfo := msg_data.get(ATTR_LORA_INFO)) is not None:
device_coordinator.dev_net_type = loraInfo.get("devNetType")
device_coordinator.async_set_updated_data(msg_data)
# handling events
if (

View File

@ -11,6 +11,7 @@ from yolink.const import (
ATTR_DEVICE_DOOR_SENSOR,
ATTR_DEVICE_LEAK_SENSOR,
ATTR_DEVICE_MOTION_SENSOR,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_VIBRATION_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
)
@ -51,6 +52,7 @@ SENSOR_DEVICE_TYPE = [
ATTR_DEVICE_VIBRATION_SENSOR,
ATTR_DEVICE_CO_SMOKE_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
]
@ -96,8 +98,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = (
state_key="alarm",
device_class=BinarySensorDeviceClass.MOISTURE,
value=lambda state: state.get("leak") if state is not None else None,
# This property will be lost during valve operation.
should_update_entity=lambda value: value is not None,
exists_fn=lambda device: (
device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER
device.device_type
in [
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
]
),
),
YoLinkBinarySensorEntityDescription(

View File

@ -12,6 +12,7 @@ ATTR_VOLUME = "volume"
ATTR_TEXT_MESSAGE = "message"
ATTR_REPEAT = "repeat"
ATTR_TONE = "tone"
ATTR_LORA_INFO = "loraInfo"
YOLINK_EVENT = f"{DOMAIN}_event"
YOLINK_OFFLINE_TIME = 32400
@ -37,5 +38,7 @@ DEV_MODEL_SWITCH_YS5708_UC = "YS5708-UC"
DEV_MODEL_SWITCH_YS5708_EC = "YS5708-EC"
DEV_MODEL_SWITCH_YS5709_UC = "YS5709-UC"
DEV_MODEL_SWITCH_YS5709_EC = "YS5709-EC"
DEV_MODEL_LEAK_STOP_YS5009 = "YS5009"
DEV_MODEL_LEAK_STOP_YS5029 = "YS5029"
DEV_MODEL_WATER_METER_YS5018_EC = "YS5018-EC"
DEV_MODEL_WATER_METER_YS5018_UC = "YS5018-UC"

View File

@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_DEVICE_STATE, DOMAIN, YOLINK_OFFLINE_TIME
from .const import ATTR_DEVICE_STATE, ATTR_LORA_INFO, DOMAIN, YOLINK_OFFLINE_TIME
_LOGGER = logging.getLogger(__name__)
@ -47,6 +47,7 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]):
self.device = device
self.paired_device = paired_device
self.dev_online = True
self.dev_net_type = None
async def _async_update_data(self) -> dict:
"""Fetch device state."""
@ -83,5 +84,8 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]):
)
raise UpdateFailed from yl_client_err
if device_state is not None:
dev_lora_info = device_state.get(ATTR_LORA_INFO)
if dev_lora_info is not None:
self.dev_net_type = dev_lora_info.get("devNetType")
return device_state
return {}

View File

@ -15,6 +15,7 @@ from yolink.const import (
ATTR_DEVICE_MANIPULATOR,
ATTR_DEVICE_MOTION_SENSOR,
ATTR_DEVICE_MULTI_OUTLET,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_OUTLET,
ATTR_DEVICE_POWER_FAILURE_ALARM,
ATTR_DEVICE_SIREN,
@ -95,6 +96,7 @@ SENSOR_DEVICE_TYPE = [
ATTR_DEVICE_VIBRATION_SENSOR,
ATTR_DEVICE_WATER_DEPTH_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_LOCK,
ATTR_DEVICE_MANIPULATOR,
ATTR_DEVICE_CO_SMOKE_SENSOR,
@ -116,6 +118,7 @@ BATTERY_POWER_SENSOR = [
ATTR_DEVICE_CO_SMOKE_SENSOR,
ATTR_DEVICE_WATER_DEPTH_SENSOR,
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
]
MCU_DEV_TEMPERATURE_SENSOR = [
@ -211,14 +214,14 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
translation_key="power_failure_alarm",
device_class=SensorDeviceClass.ENUM,
options=["normal", "alert", "off"],
exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM,
),
YoLinkSensorEntityDescription(
key="mute",
translation_key="power_failure_alarm_mute",
device_class=SensorDeviceClass.ENUM,
options=["muted", "unmuted"],
exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM,
value=lambda value: "muted" if value is True else "unmuted",
),
YoLinkSensorEntityDescription(
@ -226,7 +229,7 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
translation_key="power_failure_alarm_volume",
device_class=SensorDeviceClass.ENUM,
options=["low", "medium", "high"],
exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM,
value=cvt_volume,
),
YoLinkSensorEntityDescription(
@ -234,14 +237,14 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
translation_key="power_failure_alarm_beep",
device_class=SensorDeviceClass.ENUM,
options=["enabled", "disabled"],
exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM,
value=lambda value: "enabled" if value is True else "disabled",
),
YoLinkSensorEntityDescription(
key="waterDepth",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.METERS,
exists_fn=lambda device: device.device_type in ATTR_DEVICE_WATER_DEPTH_SENSOR,
exists_fn=lambda device: device.device_type == ATTR_DEVICE_WATER_DEPTH_SENSOR,
),
YoLinkSensorEntityDescription(
key="meter_reading",
@ -251,7 +254,29 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
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
device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER
),
),
YoLinkSensorEntityDescription(
key="meter_1_reading",
translation_key="water_meter_1_reading",
device_class=SensorDeviceClass.WATER,
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 == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
),
),
YoLinkSensorEntityDescription(
key="meter_2_reading",
translation_key="water_meter_2_reading",
device_class=SensorDeviceClass.WATER,
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 == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
),
),
YoLinkSensorEntityDescription(

View File

@ -90,6 +90,12 @@
},
"water_meter_reading": {
"name": "Water meter reading"
},
"water_meter_1_reading": {
"name": "Water meter 1 reading"
},
"water_meter_2_reading": {
"name": "Water meter 2 reading"
}
},
"number": {
@ -100,6 +106,12 @@
"valve": {
"meter_valve_state": {
"name": "Valve state"
},
"meter_valve_1_state": {
"name": "Valve 1"
},
"meter_valve_2_state": {
"name": "Valve 2"
}
}
},

View File

@ -6,7 +6,11 @@ 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.const import (
ATTR_DEVICE_MODEL_A,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
ATTR_DEVICE_WATER_METER_CONTROLLER,
)
from yolink.device import YoLinkDevice
from homeassistant.components.valve import (
@ -30,6 +34,7 @@ class YoLinkValveEntityDescription(ValveEntityDescription):
exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True
value: Callable = lambda state: state
channel_index: int | None = None
DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = (
@ -42,9 +47,32 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = (
== ATTR_DEVICE_WATER_METER_CONTROLLER
and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007),
),
YoLinkValveEntityDescription(
key="valve_1_state",
translation_key="meter_valve_1_state",
device_class=ValveDeviceClass.WATER,
value=lambda value: value != "open" if value is not None else None,
exists_fn=lambda device: (
device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
),
channel_index=0,
),
YoLinkValveEntityDescription(
key="valve_2_state",
translation_key="meter_valve_2_state",
device_class=ValveDeviceClass.WATER,
value=lambda value: value != "open" if value is not None else None,
exists_fn=lambda device: (
device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
),
channel_index=1,
),
)
DEVICE_TYPE = [ATTR_DEVICE_WATER_METER_CONTROLLER]
DEVICE_TYPE = [
ATTR_DEVICE_WATER_METER_CONTROLLER,
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
]
async def async_setup_entry(
@ -102,7 +130,17 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity):
async def _async_invoke_device(self, state: str) -> None:
"""Call setState api to change valve state."""
await self.call_device(ClientRequest("setState", {"valve": state}))
if (
self.coordinator.device.device_type
== ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER
):
channel_index = self.entity_description.channel_index
if channel_index is not None:
await self.call_device(
ClientRequest("setState", {"valves": {str(channel_index): state}})
)
else:
await self.call_device(ClientRequest("setState", {"valve": state}))
self._attr_is_closed = state == "close"
self.async_write_ha_state()
@ -113,3 +151,11 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity):
async def async_close_valve(self) -> None:
"""Close valve."""
await self._async_invoke_device("close")
@property
def available(self) -> bool:
"""Return true is device is available."""
if self.coordinator.dev_net_type is not None:
# When the device operates in Class A mode, it cannot be controlled.
return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A
return super().available