Skip to content

Commit

Permalink
feat: add device with motor-control service as cover entity (#688)
Browse files Browse the repository at this point in the history
  • Loading branch information
SusanPhevos authored Feb 25, 2025
1 parent 20b0004 commit 0ce94f7
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 67 deletions.
160 changes: 94 additions & 66 deletions custom_components/xiaomi_home/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,19 @@
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
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,
) -> 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]
Expand All @@ -82,8 +76,12 @@ async def async_setup_entry(
data.spec.device_class = CoverDeviceClass.CURTAIN
elif data.spec.name == 'window-opener':
data.spec.device_class = CoverDeviceClass.WINDOW
new_entities.append(
Cover(miot_device=miot_device, entity_data=data))
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))

if new_entities:
async_add_entities(new_entities)
Expand All @@ -97,18 +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_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
Expand All @@ -120,117 +116,149 @@ def __init__(
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_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'}:
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
elif item.name in {'pause'}:
elif item.name in {'pause', 'stop'}:
self._attr_supported_features |= (
CoverEntityFeature.STOP)
self._prop_motor_value_pause = item.value
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'}:
self._prop_status_opening = item.value
elif item.name in {'closing', 'close'}:
self._prop_status_closing = item.value
elif item.name in {'stop', 'pause'}:
self._prop_status_stop = item.value
if item.name in {'opening', 'open', 'up'}:
self._prop_status_opening.append(item.value)
elif item.name in {'closing', 'close', 'down'}:
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.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:
_LOGGER.error(
'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]:
"""Return the current position.
0: the cover is closed, 100: the cover is fully opened, None: unknown.
"""
if self._prop_current_position is 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:
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 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:
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.
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:
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.
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."""
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.
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
11 changes: 11 additions & 0 deletions custom_components/xiaomi_home/miot/miot_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,13 @@ def get_prop_access(self, siid: int, piid: int) -> Optional[list]:
return None
return access

def get_prop_value_range(self, siid: int, piid: int) -> Optional[list]:
value_range = self.__get_prop_item(siid=siid, piid=piid,
key='value-range')
if not isinstance(value_range, list):
return None
return value_range

def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
if not self._selected:
return None
Expand Down Expand Up @@ -1474,6 +1481,10 @@ async def __parse(self, urn: str) -> MIoTSpecInstance:
siid=service['iid'], piid=property_['iid'])
if custom_access:
spec_prop.access = custom_access
custom_range = self._spec_modify.get_prop_value_range(
siid=service['iid'], piid=property_['iid'])
if custom_range:
spec_prop.value_range = custom_range
# Parse service event
for event in service.get('events', []):
if (
Expand Down
7 changes: 7 additions & 0 deletions custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
name: upload-speed
icon: mdi:upload
unit: B/s
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
prop.2.3:
value-range:
- 0
- 1
- 1
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1
4 changes: 3 additions & 1 deletion custom_components/xiaomi_home/miot/specs/specv2entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@
},
'entity': 'cover'
},
'window-opener': 'curtain'
'window-opener': 'curtain',
'motor-controller': 'curtain',
'airer': 'curtain'
}

"""SPEC_PROP_TRANS_MAP
Expand Down

0 comments on commit 0ce94f7

Please sign in to comment.