Merge branch 'XiaoMi:main' into main

This commit is contained in:
ted 2025-02-25 10:45:50 +08:00 committed by GitHub
commit 6ad8da42be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 176 additions and 75 deletions

View File

@ -83,6 +83,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
for data in miot_device.entity_list.get('thermostat', []):
new_entities.append(
Thermostat(miot_device=miot_device, entity_data=data))
for data in miot_device.entity_list.get('electric-blanket', []):
new_entities.append(
ElectricBlanket(miot_device=miot_device, entity_data=data))
if new_entities:
async_add_entities(new_entities)
@ -106,7 +109,8 @@ class FeatureOnOff(MIoTServiceEntity, ClimateEntity):
# the on/off feature of the entity.
prop.service.name == 'air-conditioner' or
prop.service.name == 'heater' or
prop.service.name == 'thermostat'):
prop.service.name == 'thermostat' or
prop.service.name == 'electric-blanket'):
self._attr_supported_features |= (
ClimateEntityFeature.TURN_ON)
self._attr_supported_features |= (
@ -179,12 +183,14 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity):
self._mode_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'heat-level' and prop.service.name == 'heater':
def _init_preset_modes(self, service_name: str, prop_name: str) -> None:
"""Initialize the preset modes."""
for prop in self.entity_data.props:
if prop.name == prop_name and prop.service.name == service_name:
if not prop.value_list:
_LOGGER.error('invalid heater heat-level value_list, %s',
self.entity_id)
_LOGGER.error('invalid %s %s value_list, %s',service_name,
prop_name, self.entity_id)
continue
self._mode_map = prop.value_list.to_map()
self._attr_preset_modes = prop.value_list.descriptions
@ -439,6 +445,8 @@ class Heater(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
self._attr_icon = 'mdi:radiator'
# hvac modes
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
# preset modes
self._init_preset_modes('heater', 'heat-level')
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode."""
@ -729,3 +737,31 @@ class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,
key=self.get_prop_value(
prop=self._prop_mode))
if self._prop_mode else None)
class ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,
FeatureTemperature, FeaturePresetMode):
"""Electric blanket"""
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the electric blanket."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_icon = 'mdi:rug'
# hvac modes
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
# preset modes
self._init_preset_modes('electric-blanket', 'mode')
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the target hvac mode."""
await self.set_property_async(
prop=self._prop_on,
value=False if hvac_mode == HVACMode.OFF else True)
@property
def hvac_mode(self) -> Optional[HVACMode]:
"""The current hvac mode."""
return (HVACMode.HEAT if self.get_prop_value(
prop=self._prop_on) else HVACMode.OFF)

View File

@ -52,25 +52,19 @@ 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
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]
@ -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)
@ -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
@ -120,50 +116,58 @@ 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_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:
@ -171,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]:
@ -209,28 +210,55 @@ class Cover(MIoTServiceEntity, CoverEntity):
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

View File

@ -1204,6 +1204,13 @@ class _SpecModify:
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
@ -1480,6 +1487,10 @@ class MIoTSpecParser:
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 (

View File

@ -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

View File

@ -295,7 +295,24 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}
},
'entity': 'bath-heater',
}
},
'electric-blanket': {
'required': {
'electric-blanket': {
'required': {
'properties': {
'on': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'temperature'}
},
}
},
'optional': {},
'entity': 'electric-blanket'
},
}
"""SPEC_SERVICE_TRANS_MAP
@ -373,7 +390,9 @@ SPEC_SERVICE_TRANS_MAP: dict = {
},
'entity': 'cover'
},
'window-opener': 'curtain'
'window-opener': 'curtain',
'motor-controller': 'curtain',
'airer': 'curtain'
}
"""SPEC_PROP_TRANS_MAP

View File

@ -376,7 +376,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
}
```
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/translations/` 路径下的 `specv2entity.py``spec_filter.json``multi_lang.json` 文件的内容,需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的 `specv2entity.py``spec_filter.json``multi_lang.json` 文件的内容,需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则
## 文档