From 057b7a4ffb233169a3293ae8937061ec66cc50d8 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Wed, 22 Jan 2025 14:53:59 +0800 Subject: [PATCH 1/6] feat: add motor-controller as cover shutter --- custom_components/xiaomi_home/cover.py | 4 +++- custom_components/xiaomi_home/miot/specs/specv2entity.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index 78a6a02..db29774 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -82,6 +82,8 @@ async def async_setup_entry( data.spec.device_class = CoverDeviceClass.CURTAIN elif data.spec.name == 'window-opener': data.spec.device_class = CoverDeviceClass.WINDOW + elif data.spec.name == 'motor-controller': + data.spec.device_class = CoverDeviceClass.SHUTTER new_entities.append( Cover(miot_device=miot_device, entity_data=data)) @@ -145,7 +147,7 @@ class Cover(MIoTServiceEntity, CoverEntity): self._attr_supported_features |= ( CoverEntityFeature.CLOSE) self._prop_motor_value_close = item.value - elif item.name in {'pause'}: + elif item.name in {'pause', 'stop'}: self._attr_supported_features |= ( CoverEntityFeature.STOP) self._prop_motor_value_pause = item.value diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 19023bb..04c2006 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -319,7 +319,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { }, 'entity': 'cover' }, - 'window-opener': 'curtain' + 'window-opener': 'curtain', + 'motor-controller': 'curtain' } """SPEC_PROP_TRANS_MAP From c40db6608cd530472bf3f51ba6ab671cb55b0f37 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Wed, 22 Jan 2025 16:57:43 +0800 Subject: [PATCH 2/6] feat: add airer as cover blind --- custom_components/xiaomi_home/cover.py | 16 +++++++++++----- .../xiaomi_home/miot/specs/specv2entity.py | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index db29774..6b2e5f8 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -84,6 +84,8 @@ async def async_setup_entry( data.spec.device_class = CoverDeviceClass.WINDOW elif data.spec.name == 'motor-controller': data.spec.device_class = CoverDeviceClass.SHUTTER + elif data.spec.name == 'airer': + data.spec.device_class = CoverDeviceClass.BLIND new_entities.append( Cover(miot_device=miot_device, entity_data=data)) @@ -139,11 +141,11 @@ class Cover(MIoTServiceEntity, CoverEntity): 'motor-control value_list is None, %s', self.entity_id) continue for item in prop.value_list.items: - if item.name in {'open'}: + if item.name in {'open', 'up'}: self._attr_supported_features |= ( CoverEntityFeature.OPEN) self._prop_motor_value_open = item.value - elif item.name in {'close'}: + elif item.name in {'close', 'down'}: self._attr_supported_features |= ( CoverEntityFeature.CLOSE) self._prop_motor_value_close = item.value @@ -158,9 +160,9 @@ class Cover(MIoTServiceEntity, CoverEntity): 'status value_list is None, %s', self.entity_id) continue for item in prop.value_list.items: - if item.name in {'opening', 'open'}: + if item.name in {'opening', 'open', 'up'}: self._prop_status_opening = item.value - elif item.name in {'closing', 'close'}: + elif item.name in {'closing', 'close', 'down'}: self._prop_status_closing = item.value elif item.name in {'stop', 'pause'}: self._prop_status_stop = item.value @@ -211,8 +213,10 @@ class Cover(MIoTServiceEntity, CoverEntity): 0: the cover is closed, 100: the cover is fully opened, None: unknown. """ + if self._prop_current_position is None: + return None pos = self.get_prop_value(prop=self._prop_current_position) - if pos is None: + if pos is None or self._prop_position_value_range is None: return None return round(pos*100/self._prop_position_value_range) @@ -235,4 +239,6 @@ class Cover(MIoTServiceEntity, CoverEntity): @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" + if self._prop_current_position is None: + return None return self.get_prop_value(prop=self._prop_current_position) == 0 diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 04c2006..e0943dc 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -320,7 +320,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { 'entity': 'cover' }, 'window-opener': 'curtain', - 'motor-controller': 'curtain' + 'motor-controller': 'curtain', + 'airer': 'curtain' } """SPEC_PROP_TRANS_MAP From cfb7c01a4459c8841c5f16baa7908e70b74d3698 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 23 Jan 2025 19:26:55 +0800 Subject: [PATCH 3/6] feat: use the closed status to be is_closed property as a backup --- custom_components/xiaomi_home/cover.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index 6b2e5f8..b8664f5 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -60,16 +60,16 @@ from homeassistant.components.cover import ( ) from .miot.miot_spec import MIoTSpecProperty -from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity +from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.const import DOMAIN _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback ) -> None: """Set up a config entry.""" device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ @@ -104,6 +104,7 @@ class Cover(MIoTServiceEntity, CoverEntity): _prop_status_opening: Optional[int] _prop_status_closing: Optional[int] _prop_status_stop: Optional[int] + _prop_status_closed: Optional[int] _prop_current_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty] _prop_position_value_min: Optional[int] @@ -127,6 +128,7 @@ class Cover(MIoTServiceEntity, CoverEntity): self._prop_status_opening = None self._prop_status_closing = None self._prop_status_stop = None + self._prop_status_closed = None self._prop_current_position = None self._prop_target_position = None self._prop_position_value_min = None @@ -166,6 +168,8 @@ class Cover(MIoTServiceEntity, CoverEntity): self._prop_status_closing = item.value elif item.name in {'stop', 'pause'}: self._prop_status_stop = item.value + elif item.name in {'closed'}: + self._prop_status_closed = item.value self._prop_status = prop elif prop.name == 'current-position': self._prop_current_position = prop @@ -239,6 +243,11 @@ class Cover(MIoTServiceEntity, CoverEntity): @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" - if self._prop_current_position is None: - return None - return self.get_prop_value(prop=self._prop_current_position) == 0 + if self._prop_current_position: + return self.get_prop_value(prop=self._prop_current_position) == 0 + # The current position is prior to the status when determining + # whether the cover is closed. + return (self.get_prop_value( + prop=self._prop_status) == self._prop_status_closed) if ( + self._prop_status + and self._prop_status_closed is not None) else None From 4d96f75b9a0b54c193bce8b9676722872cc2ae21 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Sun, 26 Jan 2025 14:14:50 +0800 Subject: [PATCH 4/6] fix: the current position --- custom_components/xiaomi_home/cover.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index b8664f5..5cf094f 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -218,7 +218,12 @@ class Cover(MIoTServiceEntity, CoverEntity): 0: the cover is closed, 100: the cover is fully opened, None: unknown. """ if self._prop_current_position is None: - return None + # Assume that the current position is the same as the target + # position when the current position is not defined in the device's + # MIoT-Spec-V2. + return None if (self._prop_target_position + is None) else self.get_prop_value( + prop=self._prop_target_position) pos = self.get_prop_value(prop=self._prop_current_position) if pos is None or self._prop_position_value_range is None: return None From 3675860526481f70fef8fa1a9a8487adbeac8081 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Sun, 26 Jan 2025 16:37:26 +0800 Subject: [PATCH 5/6] feat: add the numerical relationship of the current position and the target position as a backup condition for is_closing and is_opening --- custom_components/xiaomi_home/cover.py | 42 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index 6626538..6dbdaf4 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -232,27 +232,41 @@ class Cover(MIoTServiceEntity, CoverEntity): @property def is_opening(self) -> Optional[bool]: """Return if the cover is opening.""" - if self._prop_status is None: - return None - return self.get_prop_value( - prop=self._prop_status) == self._prop_status_opening + if self._prop_status and self._prop_status_opening is not None: + return self.get_prop_value( + prop=self._prop_status) == self._prop_status_opening + # The status is prior to the numerical relationship of the current + # position and the target position when determining whether the cover + # is opening. + if (self._prop_target_position and + self.current_cover_position is not None): + return (self.current_cover_position + < self.get_prop_value(prop=self._prop_target_position)) + return None @property def is_closing(self) -> Optional[bool]: """Return if the cover is closing.""" - if self._prop_status is None: - return None - return self.get_prop_value( - prop=self._prop_status) == self._prop_status_closing + if self._prop_status and self._prop_status_closing is not None: + return self.get_prop_value( + prop=self._prop_status) == self._prop_status_closing + # The status is prior to the numerical relationship of the current + # position and the target position when determining whether the cover + # is closing. + if (self._prop_target_position and + self.current_cover_position is not None): + return (self.current_cover_position + > self.get_prop_value(prop=self._prop_target_position)) + return None @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" - if self._prop_current_position: - return self.get_prop_value(prop=self._prop_current_position) == 0 + if self.current_cover_position is not None: + return self.current_cover_position == 0 # The current position is prior to the status when determining # whether the cover is closed. - return (self.get_prop_value( - prop=self._prop_status) == self._prop_status_closed) if ( - self._prop_status - and self._prop_status_closed is not None) else None + if self._prop_status and self._prop_status_closed is not None: + return (self.get_prop_value( + prop=self._prop_status) == self._prop_status_closed) + return None From b802fdb2e4ca813bc3cb8e704656326d6cec5099 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Sat, 8 Feb 2025 16:29:14 +0800 Subject: [PATCH 6/6] fix: current position value range --- custom_components/xiaomi_home/cover.py | 118 ++++++++++++------------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/custom_components/xiaomi_home/cover.py b/custom_components/xiaomi_home/cover.py index 6dbdaf4..dcc512f 100644 --- a/custom_components/xiaomi_home/cover.py +++ b/custom_components/xiaomi_home/cover.py @@ -52,12 +52,9 @@ from typing import Optional from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.components.cover import ( - ATTR_POSITION, - CoverEntity, - CoverEntityFeature, - CoverDeviceClass -) +from homeassistant.components.cover import (ATTR_POSITION, CoverEntity, + CoverEntityFeature, + CoverDeviceClass) from .miot.miot_spec import MIoTSpecProperty from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity @@ -66,11 +63,8 @@ from .miot.const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback -) -> None: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback) -> None: """Set up a config entry.""" device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ config_entry.entry_id] @@ -86,8 +80,8 @@ async def async_setup_entry( data.spec.device_class = CoverDeviceClass.SHUTTER elif data.spec.name == 'airer': data.spec.device_class = CoverDeviceClass.BLIND - new_entities.append( - Cover(miot_device=miot_device, entity_data=data)) + new_entities.append(Cover(miot_device=miot_device, + entity_data=data)) if new_entities: async_add_entities(new_entities) @@ -101,19 +95,16 @@ class Cover(MIoTServiceEntity, CoverEntity): _prop_motor_value_close: Optional[int] _prop_motor_value_pause: Optional[int] _prop_status: Optional[MIoTSpecProperty] - _prop_status_opening: Optional[int] - _prop_status_closing: Optional[int] - _prop_status_stop: Optional[int] - _prop_status_closed: Optional[int] + _prop_status_opening: Optional[list[int]] + _prop_status_closing: Optional[list[int]] + _prop_status_stop: Optional[list[int]] + _prop_status_closed: Optional[list[int]] _prop_current_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty] - _prop_position_value_min: Optional[int] - _prop_position_value_max: Optional[int] _prop_position_value_range: Optional[int] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the Cover.""" super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_device_class = entity_data.spec.device_class @@ -125,22 +116,20 @@ class Cover(MIoTServiceEntity, CoverEntity): self._prop_motor_value_close = None self._prop_motor_value_pause = None self._prop_status = None - self._prop_status_opening = None - self._prop_status_closing = None - self._prop_status_stop = None - self._prop_status_closed = None + self._prop_status_opening = [] + self._prop_status_closing = [] + self._prop_status_stop = [] + self._prop_status_closed = [] self._prop_current_position = None self._prop_target_position = None - self._prop_position_value_min = None - self._prop_position_value_max = None self._prop_position_value_range = None # properties for prop in entity_data.props: if prop.name == 'motor-control': if not prop.value_list: - _LOGGER.error( - 'motor-control value_list is None, %s', self.entity_id) + _LOGGER.error('motor-control value_list is None, %s', + self.entity_id) continue for item in prop.value_list.items: if item.name in {'open', 'up'}: @@ -158,20 +147,27 @@ class Cover(MIoTServiceEntity, CoverEntity): self._prop_motor_control = prop elif prop.name == 'status': if not prop.value_list: - _LOGGER.error( - 'status value_list is None, %s', self.entity_id) + _LOGGER.error('status value_list is None, %s', + self.entity_id) continue for item in prop.value_list.items: if item.name in {'opening', 'open', 'up'}: - self._prop_status_opening = item.value + self._prop_status_opening.append(item.value) elif item.name in {'closing', 'close', 'down'}: - self._prop_status_closing = item.value - elif item.name in {'stop', 'pause'}: - self._prop_status_stop = item.value + self._prop_status_closing.append(item.value) + elif item.name in {'stop', 'stopped', 'pause'}: + self._prop_status_stop.append(item.value) elif item.name in {'closed'}: - self._prop_status_closed = item.value + self._prop_status_closed.append(item.value) self._prop_status = prop elif prop.name == 'current-position': + if not prop.value_range: + _LOGGER.error( + 'invalid current-position value_range format, %s', + self.entity_id) + continue + self._prop_position_value_range = (prop.value_range.max_ - + prop.value_range.min_) self._prop_current_position = prop elif prop.name == 'target-position': if not prop.value_range: @@ -179,37 +175,34 @@ class Cover(MIoTServiceEntity, CoverEntity): 'invalid target-position value_range format, %s', self.entity_id) continue - self._prop_position_value_min = prop.value_range.min_ - self._prop_position_value_max = prop.value_range.max_ - self._prop_position_value_range = ( - self._prop_position_value_max - - self._prop_position_value_min) + self._prop_position_value_range = (prop.value_range.max_ - + prop.value_range.min_) self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._prop_target_position = prop async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" - await self.set_property_async( - self._prop_motor_control, self._prop_motor_value_open) + await self.set_property_async(self._prop_motor_control, + self._prop_motor_value_open) async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" - await self.set_property_async( - self._prop_motor_control, self._prop_motor_value_close) + await self.set_property_async(self._prop_motor_control, + self._prop_motor_value_close) async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" - await self.set_property_async( - self._prop_motor_control, self._prop_motor_value_pause) + await self.set_property_async(self._prop_motor_control, + self._prop_motor_value_pause) async def async_set_cover_position(self, **kwargs) -> None: """Set the position of the cover.""" pos = kwargs.get(ATTR_POSITION, None) if pos is None: return None - pos = round(pos*self._prop_position_value_range/100) - await self.set_property_async( - prop=self._prop_target_position, value=pos) + pos = round(pos * self._prop_position_value_range / 100) + await self.set_property_async(prop=self._prop_target_position, + value=pos) @property def current_cover_position(self) -> Optional[int]: @@ -225,16 +218,15 @@ class Cover(MIoTServiceEntity, CoverEntity): is None) else self.get_prop_value( prop=self._prop_target_position) pos = self.get_prop_value(prop=self._prop_current_position) - if pos is None or self._prop_position_value_range is None: - return None - return round(pos*100/self._prop_position_value_range) + return None if pos is None else round(pos * 100 / + self._prop_position_value_range) @property def is_opening(self) -> Optional[bool]: """Return if the cover is opening.""" - if self._prop_status and self._prop_status_opening is not None: - return self.get_prop_value( - prop=self._prop_status) == self._prop_status_opening + if self._prop_status and self._prop_status_opening: + return (self.get_prop_value(prop=self._prop_status) + in self._prop_status_opening) # The status is prior to the numerical relationship of the current # position and the target position when determining whether the cover # is opening. @@ -247,9 +239,9 @@ class Cover(MIoTServiceEntity, CoverEntity): @property def is_closing(self) -> Optional[bool]: """Return if the cover is closing.""" - if self._prop_status and self._prop_status_closing is not None: - return self.get_prop_value( - prop=self._prop_status) == self._prop_status_closing + if self._prop_status and self._prop_status_closing: + return (self.get_prop_value(prop=self._prop_status) + in self._prop_status_closing) # The status is prior to the numerical relationship of the current # position and the target position when determining whether the cover # is closing. @@ -266,7 +258,7 @@ class Cover(MIoTServiceEntity, CoverEntity): return self.current_cover_position == 0 # The current position is prior to the status when determining # whether the cover is closed. - if self._prop_status and self._prop_status_closed is not None: - return (self.get_prop_value( - prop=self._prop_status) == self._prop_status_closed) + if self._prop_status and self._prop_status_closed: + return (self.get_prop_value(prop=self._prop_status) + in self._prop_status_closed) return None