Merge branch 'main' into entity-category

This commit is contained in:
LiShuzhen 2025-03-05 15:38:49 +08:00
commit 603594144f
10 changed files with 837 additions and 514 deletions

View File

@ -1,5 +1,26 @@
# CHANGELOG
## v0.2.0
This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.
这个版本修改了一些传感器默认单位,更新后会导致 Home Assistant 弹出一些兼容性提示,您可以重新添加集成解决。
### Added
- Add prop trans rule for surge-power. [#595](https://github.com/XiaoMi/ha_xiaomi_home/pull/595)
- Support modify spec and value conversion. [#663](https://github.com/XiaoMi/ha_xiaomi_home/pull/663)
- Support the electric blanket. [#781](https://github.com/XiaoMi/ha_xiaomi_home/pull/781)
- Add device with motor-control service as cover entity. [#688](https://github.com/XiaoMi/ha_xiaomi_home/pull/688)
### Changed
- Update README file. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681) [#747](https://github.com/XiaoMi/ha_xiaomi_home/pull/747)
- Update CONTRIBUTING.md. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681)
- Refactor climate.py. [#614](https://github.com/XiaoMi/ha_xiaomi_home/pull/614)
### Fixed
- Fix variable name or comment errors & fix test_lan error. [#678](https://github.com/XiaoMi/ha_xiaomi_home/pull/678) [#690](https://github.com/XiaoMi/ha_xiaomi_home/pull/690)
- Fix water heater error & some type error. [#684](https://github.com/XiaoMi/ha_xiaomi_home/pull/684)
- Fix fan level with value-list & fan reverse [#689](https://github.com/XiaoMi/ha_xiaomi_home/pull/689)
- Fix sensor display precision [#708](https://github.com/XiaoMi/ha_xiaomi_home/pull/708)
- Fix event:motion-detected without arguments [#712](https://github.com/XiaoMi/ha_xiaomi_home/pull/712)
## v0.1.5b2
### Added
- Support binary sensors to be displayed as text sensor entities and binary sensor entities. [#592](https://github.com/XiaoMi/ha_xiaomi_home/pull/592)
@ -91,10 +112,10 @@ This version will cause some Xiaomi routers that do not support access (#564) to
### Changed
### Fixed
- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
## v0.1.0
### Added

File diff suppressed because it is too large Load Diff

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

@ -52,7 +52,12 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.components.fan import (
FanEntity,
FanEntityFeature,
DIRECTION_FORWARD,
DIRECTION_REVERSE
)
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
@ -167,20 +172,21 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse':
if prop.format_ == 'bool':
if prop.format_ == bool:
self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True
elif prop.value_list:
for item in prop.value_list.items:
if item.name in {'foreward'}:
if item.name in {'foreward', 'forward'}:
self._prop_wind_reverse_forward = item.value
elif item.name in {'reversal', 'reverse'}:
self._prop_wind_reverse_reverse = item.value
if (
self._prop_wind_reverse_forward is None
or self._prop_wind_reverse_reverse is None
):
# NOTICE: Value may be 0 or False
_LOGGER.info(
_LOGGER.error(
'invalid wind-reverse, %s', self.entity_id)
continue
self._attr_supported_features |= FanEntityFeature.DIRECTION
@ -202,9 +208,9 @@ class Fan(MIoTServiceEntity, FanEntity):
if self._speed_names:
await self.set_property_async(
prop=self._prop_fan_level,
value=self.get_map_value(
value=self.get_map_key(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
value=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else:
await self.set_property_async(
@ -233,9 +239,9 @@ class Fan(MIoTServiceEntity, FanEntity):
if self._speed_names:
await self.set_property_async(
prop=self._prop_fan_level,
value=self.get_map_value(
value=self.get_map_key(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
value=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else:
await self.set_property_async(
@ -264,7 +270,7 @@ class Fan(MIoTServiceEntity, FanEntity):
prop=self._prop_wind_reverse,
value=(
self._prop_wind_reverse_reverse
if self.current_direction == 'reverse'
if direction == DIRECTION_REVERSE
else self._prop_wind_reverse_forward))
async def async_oscillate(self, oscillating: bool) -> None:
@ -293,9 +299,9 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Return the current direction of the fan."""
if not self._prop_wind_reverse:
return None
return 'reverse' if self.get_prop_value(
return DIRECTION_REVERSE if self.get_prop_value(
prop=self._prop_wind_reverse
) == self._prop_wind_reverse_reverse else 'forward'
) == self._prop_wind_reverse_reverse else DIRECTION_FORWARD
@property
def percentage(self) -> Optional[int]:

View File

@ -25,7 +25,7 @@
"cryptography",
"psutil"
],
"version": "v0.1.5b2",
"version": "v0.2.0",
"zeroconf": [
"_miot-central._tcp.local."
]

View File

@ -1213,10 +1213,12 @@ class MipsLocalClient(_MipsClient):
or 'did' not in msg
or 'siid' not in msg
or 'eiid' not in msg
or 'arguments' not in msg
# or 'arguments' not in msg
):
# self.log_error(f'on_event_msg, recv unknown msg, {payload}')
return
if 'arguments' not in msg:
msg['arguments'] = []
if handler:
self.log_debug('local, on event_occurred, %s', payload)
handler(msg, ctx)

View File

@ -542,7 +542,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
self.unit = unit
self.value_range = value_range
self.value_list = value_list
self.precision = precision or 1
self.precision = precision if precision is not None else 1
self.expr = expr
self.spec_id = hash(
@ -1200,6 +1200,20 @@ 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_value_list(self, siid: int, piid: int) -> Optional[list]:
value_list = self.__get_prop_item(siid=siid, piid=piid,
key='value-list')
if not isinstance(value_list, list):
return None
return value_list
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
if not self._selected:
return None
@ -1476,6 +1490,14 @@ 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
custom_list = self._spec_modify.get_prop_value_list(
siid=service['iid'], piid=property_['iid'])
if custom_list:
spec_prop.value_list = custom_list
# Parse service event
for event in service.get('events', []):
if (

View File

@ -42,3 +42,19 @@ 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
urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
prop.5.2:
value-list:
- value: 1
description: low
- value: 128
description: medium
- value: 255
description: high

View File

@ -225,6 +225,31 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'entity': 'air-conditioner'
},
'air-condition-outlet': 'air-conditioner',
'thermostat': {
'required': {
'thermostat': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'target-temperature', 'mode', 'fan-level',
'temperature'}
},
}
},
'optional': {
'environment': {
'required': {},
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
},
'entity': 'thermostat'
},
'heater': {
'required': {
'heater': {
@ -247,7 +272,48 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
},
'entity': 'heater'
}
},
'bath-heater': {
'required': {
'ptc-bath-heater': {
'required': {},
'optional': {
'properties': {
'target-temperature', 'heat-level',
'temperature', 'mode'
}
},
}
},
'optional': {
'fan-control': {
'required': {},
'optional': {
'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
},
}
},
'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
@ -339,7 +405,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) > 配置 > 更新实体转换规则
## 文档