fix: merge conflicts

This commit is contained in:
LiShuzhen 2025-06-03 15:46:33 +08:00
commit 0ca1806e15
29 changed files with 2283 additions and 1103 deletions

View File

@ -1,7 +1,7 @@
name: Bug Report / 报告问题 name: Bug Report / 报告问题
description: Create a report to help us improve. / 报告问题以帮助我们改进 description: Create a report to help us improve. / 报告问题以帮助我们改进
body: body:
- type: input - type: textarea
attributes: attributes:
label: Describe the Bug / 描述问题 label: Describe the Bug / 描述问题
description: | description: |

View File

@ -1,4 +1,107 @@
# CHANGELOG # CHANGELOG
## v0.3.2
> Xiaomi Home has been added to the Home Assistant Community Store (HACS) as a default since May 8, 2025.
### Added
- Modify MIoT-Spec-V2 property format by spec_modify.yaml. [#1111](https://github.com/XiaoMi/ha_xiaomi_home/pull/1111)
### Changed
- Update the instructions of Xiaomi Home integration installation from HACS. [#102](https://github.com/XiaoMi/ha_xiaomi_home/pull/102) [#1088](https://github.com/XiaoMi/ha_xiaomi_home/pull/1088)
- Add an alongside switch entity for zimi.waterheater.h03 and xiaomi.waterheater.yms2. [#1115](https://github.com/XiaoMi/ha_xiaomi_home/pull/1115)
### Fixed
- Fix Chinese encoding in LAN Control. [#1114](https://github.com/XiaoMi/ha_xiaomi_home/pull/1114)
- Fix the MIoT-Spec-V2 of lxzn.switch.jcbcsm power consumption, voltage and current, shhf.light.sfla10 fan direction, zhimi.fan.za4 fan-level, zhimi.fan.sa1 fan-level. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)
- Revise the Chinese descriptions of loock.lock.t2pv1 door state value-list. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)
## v0.3.1
### Changed
- Setting the fan speed level when the fan is off will turning the fan on first. [#1031](https://github.com/XiaoMi/ha_xiaomi_home/pull/1031)
### Fixed
- Fix update device list error when there is no shared devices. [#1024](https://github.com/XiaoMi/ha_xiaomi_home/pull/1024)
- Fix the humidifier get_prop_value error when the property is None. [#1035](https://github.com/XiaoMi/ha_xiaomi_home/pull/1035)
- Fix the MIoT-Spec-V2 of zhimi.fan.v3 fan-level, cuco.plug.cp1md voltage and current, zimi.plug.zncz01 electric-power, giot.plug.v8icm power-consumption unit, yunmi.kettle.r3 tds unit, and dmaker.fan.p5 fan-level. [#1037](https://github.com/XiaoMi/ha_xiaomi_home/pull/1037)
## v0.3.0
注意v0.3.0 变更了部分实体 unique_id 的生成规则,如果勾选 xiaomi_home > 配置 > 更新实体转换规则,会导致部分实体已配置的自动化失效。如果想要避免重新配置大量自动化,可使用这个[补丁](https://github.com/XiaoMi/ha_xiaomi_home/pull/972)。
CAUTION: v0.3.0 changes the unique_id of some entities. If you check the option `xiaomi_home > CONFIGURE > Update entity conversion rules`, it may cause the automation settings for these entities to fail. To avoid having to reconfigure a large number of automation settings, you can use this [patch](https://github.com/XiaoMi/ha_xiaomi_home/pull/972).
### Added
- Import the devices in the shared homes and the separated shared devices. [#1021](https://github.com/XiaoMi/ha_xiaomi_home/pull/1021)
- Support _attr_hvac_action of the climate entity. [#956](https://github.com/XiaoMi/ha_xiaomi_home/pull/956)
- Add custom defined MIoT-Spec-V2 instance via spec_add.json. [#953](https://github.com/XiaoMi/ha_xiaomi_home/pull/953)
### Fixed
- Ignore 'Event loop is closed' when unsub a closed event loop. [#991](https://github.com/XiaoMi/ha_xiaomi_home/pull/991)
- Fix contact-state for linp.magnet.m1 and loock.safe.v1. [#977](https://github.com/XiaoMi/ha_xiaomi_home/pull/977)
- Fix the mode initialization error of aupu.bhf_light.s368m. [#955](https://github.com/XiaoMi/ha_xiaomi_home/pull/955)
- Fix the MIoT-Spec-V2 of lumi.gateway.mcn001, qmi.plug.psv3, lumi.motion.acn001, izq.sensor_occupy.24, linp.sensor_occupy.hb01 and yunmi.waterpuri.s20. [#949](https://github.com/XiaoMi/ha_xiaomi_home/pull/949)
## v0.2.4
### Added
- Convert the submersion-state, the contact-state and the occupancy-status property to the binary_sensor entity. [#905](https://github.com/XiaoMi/ha_xiaomi_home/pull/905)
### Changed
- suittc.airrtc.wk168 mode descriptions are set to strings of numbers from 1 to 16. [#921](https://github.com/XiaoMi/ha_xiaomi_home/pull/921)
- Do not set _attr_suggested_display_precision when the spec.expr is set in spec_modify.yaml [#929](https://github.com/XiaoMi/ha_xiaomi_home/pull/929)
- Set "unknown event msg" log to info level.
### Fixed
- hhcc.plantmonitor.v1 soil moisture and soil ec icon and unit. [#927](https://github.com/XiaoMi/ha_xiaomi_home/pull/27)
- cuco.plug.cp2 voltage and power value ratio.
- cgllc.airmonitor.s1 unit ppb.
- roswan.waterpuri.lte01 tds unit.
- lumi.relay.c2acn01 power consumption unit
- xiaomi.bhf_light.s1 fan level of ventilation.
## v0.2.3
### Changed
- Specify the service name and the property name during the climate entity's on/off feature initialization. [#899](https://github.com/XiaoMi/ha_xiaomi_home/pull/899)
- Remove the useless total-battery property from `SPEC_PROP_TRANS_MAP`.
### Fixed
- Fix the hvac mode setting error when changing the preset mode of the ptc-bath-heater.
- Fix the ambiguous descriptions of yeelink.bhf_light.v10 ptc-bath-heater mode value-list.
- Fix the power consumption value of chuangmi.plug.212a01. [#910](https://github.com/XiaoMi/ha_xiaomi_home/pull/910)
## v0.2.2
This version has modified the conversion rules of the climate entity, which will have effect on the devices with the ptc-bath-heater, the air-conditioner and the air-fresh service. After updating, you need to restart Home Assistant and check `xiaomi_home > CONFIGURE >
Update entity conversion rules > NEXT` to reload the integration.
这个版本修改了浴霸、空调、新风机的实体转换规则,更新之后需要重启 Home Assistant并且勾选 `xiaomi_home > 配置 > 更新实体转换规则 > 下一步` 重新加载集成。
### Added
- Add conversion rules for the air-conditioner service and the air-fresh service. [#879](https://github.com/XiaoMi/ha_xiaomi_home/pull/879)
### Changed
- Convert the mode of the ptc bath heater to the preset mode of the climate entity. [#874](https://github.com/XiaoMi/ha_xiaomi_home/pull/874)
- Use Home Assistant default icon when device_class is set. [#855](https://github.com/XiaoMi/ha_xiaomi_home/pull/855)
### Fixed
- Fix xiaomi.aircondition.m9 humidity-range unit. [#878](https://github.com/XiaoMi/ha_xiaomi_home/pull/878)
- Fix MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2. [#866](https://github.com/XiaoMi/ha_xiaomi_home/pull/866)
## v0.2.1
### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)
- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)
- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)
- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)
## 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 ## v0.1.5b2
### Added ### Added

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### Method 2: [HACS](https://hacs.xyz/) ### Method 2: [HACS](https://hacs.xyz/)
HACS > Overflow Menu > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > Xiaomi Home in New or Available for download section of HACS > DOWNLOAD One-click installation from HACS:
> Xiaomi Home has not been added to the HACS store as a default yet. It's coming soon. [![Open your Home Assistant instance and open the Xiaomi Home integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
Or, HACS > In the search box, type **Xiaomi Home** > Click **Xiaomi Home**, getting into the detail page > DOWNLOAD
### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp) ### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)
@ -47,7 +49,7 @@ Download and copy `custom_components/xiaomi_home` folder to `config/custom_compo
[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account [Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home) [![Open your Home Assistant instance and start setting up a new Xiaomi Home integration instance.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
### Add MIoT Devices ### Add MIoT Devices
@ -59,7 +61,7 @@ After a Xiaomi account login and its user configuration are completed, you can c
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) [![Open your Home Assistant instance and show Xiaomi Home integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
### Update Configurations ### Update Configurations

View File

@ -156,7 +156,8 @@ async def async_setup_entry(
device.entity_list[platform].remove(entity) device.entity_list[platform].remove(entity)
entity_id = device.gen_service_entity_id( entity_id = device.gen_service_entity_id(
ha_domain=platform, ha_domain=platform,
siid=entity.spec.iid) # type: ignore siid=entity.spec.iid,
description=entity.spec.description)
if er.async_get(entity_id_or_uuid=entity_id): if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id) er.async_remove(entity_id=entity_id)
if platform in device.prop_list: if platform in device.prop_list:

View File

@ -89,4 +89,8 @@ class BinarySensor(MIoTPropertyEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""On/Off state. True if the binary sensor is on, False otherwise.""" """On/Off state. True if the binary sensor is on, False otherwise."""
if self.spec.name == 'contact-state':
return self._value is False
elif self.spec.name == 'occupancy-status':
return bool(self._value)
return self._value is True return self._value is True

File diff suppressed because it is too large Load Diff

View File

@ -565,8 +565,12 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[ for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items(): 'homes'][device_source].items():
# i18n # i18n
tip_central = '' tip_central = ''
group_id = home_info.get('group_id', None) group_id = home_info.get('group_id', None)
@ -582,7 +586,8 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# i18n # i18n
tip_central = self._miot_i18n.translate( tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway') key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get('did', None) home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = ( home_list[home_id] = (
f'{home_info["home_name"]} ' f'{home_info["home_name"]} '
f'[ {len(dev_list)} {tip_devices} {tip_central} ]') f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
@ -660,8 +665,12 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not home_selected: if not home_selected:
return await self.__show_homes_select_form( return await self.__show_homes_select_form(
'no_family_selected') 'no_family_selected')
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[ for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items(): 'homes'][device_source].items():
if home_id in home_selected: if home_id in home_selected:
self._home_selected[home_id] = home_info self._home_selected[home_id] = home_info
self._area_name_rule = user_input.get( self._area_name_rule = user_input.get(
@ -1420,8 +1429,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[ for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items(): 'homes'][device_source].items():
# i18n # i18n
tip_central = '' tip_central = ''
group_id = home_info.get('group_id', None) group_id = home_info.get('group_id', None)
@ -1460,8 +1473,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return await self.__show_homes_select_form('no_family_selected') return await self.__show_homes_select_form('no_family_selected')
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode) self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
self._home_selected = {} self._home_selected = {}
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[ for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items(): 'homes'][device_source].items():
if home_id in self._home_selected_list: if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info self._home_selected[home_id] = home_info
# Get device list # Get device list

View File

@ -47,17 +47,14 @@ Cover entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Optional from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.cover import ( from homeassistant.components.cover import (ATTR_POSITION, CoverEntity,
ATTR_POSITION,
CoverEntity,
CoverEntityFeature, CoverEntityFeature,
CoverDeviceClass CoverDeviceClass)
)
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
@ -66,11 +63,8 @@ from .miot.const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
hass: HomeAssistant, async_add_entities: AddEntitiesCallback) -> None:
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id] config_entry.entry_id]
@ -82,8 +76,12 @@ async def async_setup_entry(
data.spec.device_class = CoverDeviceClass.CURTAIN data.spec.device_class = CoverDeviceClass.CURTAIN
elif data.spec.name == 'window-opener': elif data.spec.name == 'window-opener':
data.spec.device_class = CoverDeviceClass.WINDOW data.spec.device_class = CoverDeviceClass.WINDOW
new_entities.append( elif data.spec.name == 'motor-controller':
Cover(miot_device=miot_device, entity_data=data)) 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: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -97,18 +95,20 @@ class Cover(MIoTServiceEntity, CoverEntity):
_prop_motor_value_close: Optional[int] _prop_motor_value_close: Optional[int]
_prop_motor_value_pause: Optional[int] _prop_motor_value_pause: Optional[int]
_prop_status: Optional[MIoTSpecProperty] _prop_status: Optional[MIoTSpecProperty]
_prop_status_opening: Optional[int] _prop_status_opening: Optional[list[int]]
_prop_status_closing: Optional[int] _prop_status_closing: Optional[list[int]]
_prop_status_stop: Optional[int] _prop_status_stop: Optional[list[int]]
_prop_status_closed: Optional[list[int]]
_prop_current_position: Optional[MIoTSpecProperty] _prop_current_position: Optional[MIoTSpecProperty]
_prop_target_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty]
_prop_position_value_min: Optional[int] _prop_position_value_min: Optional[int]
_prop_position_value_max: Optional[int] _prop_position_value_max: Optional[int]
_prop_position_value_range: Optional[int] _prop_position_value_range: Optional[int]
_prop_pos_closing: bool
_prop_pos_opening: bool
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Cover.""" """Initialize the Cover."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = entity_data.spec.device_class self._attr_device_class = entity_data.spec.device_class
@ -120,50 +120,64 @@ class Cover(MIoTServiceEntity, CoverEntity):
self._prop_motor_value_close = None self._prop_motor_value_close = None
self._prop_motor_value_pause = None self._prop_motor_value_pause = None
self._prop_status = None self._prop_status = None
self._prop_status_opening = None self._prop_status_opening = []
self._prop_status_closing = None self._prop_status_closing = []
self._prop_status_stop = None self._prop_status_stop = []
self._prop_status_closed = []
self._prop_current_position = None self._prop_current_position = None
self._prop_target_position = None self._prop_target_position = None
self._prop_position_value_min = None self._prop_position_value_min = None
self._prop_position_value_max = None self._prop_position_value_max = None
self._prop_position_value_range = None self._prop_position_value_range = None
self._prop_pos_closing = False
self._prop_pos_opening = False
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'motor-control': if prop.name == 'motor-control':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('motor-control value_list is None, %s',
'motor-control value_list is None, %s', self.entity_id) self.entity_id)
continue continue
for item in prop.value_list.items: for item in prop.value_list.items:
if item.name in {'open'}: if item.name in {'open', 'up'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.OPEN) CoverEntityFeature.OPEN)
self._prop_motor_value_open = item.value self._prop_motor_value_open = item.value
elif item.name in {'close'}: elif item.name in {'close', 'down'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.CLOSE) CoverEntityFeature.CLOSE)
self._prop_motor_value_close = item.value self._prop_motor_value_close = item.value
elif item.name in {'pause'}: elif item.name in {'pause', 'stop'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.STOP) CoverEntityFeature.STOP)
self._prop_motor_value_pause = item.value self._prop_motor_value_pause = item.value
self._prop_motor_control = prop self._prop_motor_control = prop
elif prop.name == 'status': elif prop.name == 'status':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('status value_list is None, %s',
'status value_list is None, %s', self.entity_id) self.entity_id)
continue continue
for item in prop.value_list.items: 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 self._prop_status_opening.append(item.value)
elif item.name in {'closing', 'close'}: elif item.name in {'closing', 'close', 'down'}:
self._prop_status_closing = item.value self._prop_status_closing.append(item.value)
elif item.name in {'stop', 'pause'}: elif item.name in {'stop', 'stopped', 'pause'}:
self._prop_status_stop = item.value self._prop_status_stop.append(item.value)
elif item.name in {'closed'}:
self._prop_status_closed.append(item.value)
self._prop_status = prop self._prop_status = prop
elif prop.name == 'current-position': 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_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = (prop.value_range.max_ -
prop.value_range.min_)
self._prop_current_position = prop self._prop_current_position = prop
elif prop.name == 'target-position': elif prop.name == 'target-position':
if not prop.value_range: if not prop.value_range:
@ -173,35 +187,65 @@ class Cover(MIoTServiceEntity, CoverEntity):
continue continue
self._prop_position_value_min = prop.value_range.min_ self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_ self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = ( self._prop_position_value_range = (prop.value_range.max_ -
self._prop_position_value_max - prop.value_range.min_)
self._prop_position_value_min)
self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._attr_supported_features |= CoverEntityFeature.SET_POSITION
self._prop_target_position = prop self._prop_target_position = prop
# For the device that has the current position property but no status
# property, the current position property will be used to determine the
# opening and the closing status.
if (self._prop_status is None) and (self._prop_current_position
is not None):
self.sub_prop_changed(self._prop_current_position,
self._position_changed_handler)
def _position_changed_handler(self, prop: MIoTSpecProperty,
ctx: Any) -> None:
self._prop_pos_closing = False
self._prop_pos_opening = False
self.async_write_ha_state()
async def async_open_cover(self, **kwargs) -> None: async def async_open_cover(self, **kwargs) -> None:
"""Open the cover.""" """Open the cover."""
await self.set_property_async( current = None if (self._prop_current_position
self._prop_motor_control, self._prop_motor_value_open) is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current < self._prop_position_value_max):
self._prop_pos_opening = True
self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_open)
async def async_close_cover(self, **kwargs) -> None: async def async_close_cover(self, **kwargs) -> None:
"""Close the cover.""" """Close the cover."""
await self.set_property_async( current = None if (self._prop_current_position
self._prop_motor_control, self._prop_motor_value_close) is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current > self._prop_position_value_min):
self._prop_pos_opening = False
self._prop_pos_closing = True
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_close)
async def async_stop_cover(self, **kwargs) -> None: async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover.""" """Stop the cover."""
await self.set_property_async( self._prop_pos_opening = False
self._prop_motor_control, self._prop_motor_value_pause) self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_pause)
async def async_set_cover_position(self, **kwargs) -> None: async def async_set_cover_position(self, **kwargs) -> None:
"""Set the position of the cover.""" """Set the position of the cover."""
pos = kwargs.get(ATTR_POSITION, None) pos = kwargs.get(ATTR_POSITION, None)
if pos is None: if pos is None:
return None return None
pos = round(pos*self._prop_position_value_range/100) current = self.current_cover_position
await self.set_property_async( if current is not None:
prop=self._prop_target_position, value=pos) self._prop_pos_opening = pos > current
self._prop_pos_closing = pos < current
pos = round(pos * self._prop_position_value_range / 100)
await self.set_property_async(prop=self._prop_target_position,
value=pos)
@property @property
def current_cover_position(self) -> Optional[int]: def current_cover_position(self) -> Optional[int]:
@ -209,28 +253,47 @@ class Cover(MIoTServiceEntity, CoverEntity):
0: the cover is closed, 100: the cover is fully opened, None: unknown. 0: the cover is closed, 100: the cover is fully opened, None: unknown.
""" """
pos = self.get_prop_value(prop=self._prop_current_position) if self._prop_current_position is None:
if pos 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.
if self._prop_target_position is None:
return None return None
return round(pos*100/self._prop_position_value_range) self._prop_pos_opening = False
self._prop_pos_closing = False
return self.get_prop_value(prop=self._prop_target_position)
pos = self.get_prop_value(prop=self._prop_current_position)
return None if pos is None else round(pos * 100 /
self._prop_position_value_range)
@property @property
def is_opening(self) -> Optional[bool]: def is_opening(self) -> Optional[bool]:
"""Return if the cover is opening.""" """Return if the cover is opening."""
if self._prop_status is None: if self._prop_status and self._prop_status_opening:
return None return (self.get_prop_value(prop=self._prop_status)
return self.get_prop_value( in self._prop_status_opening)
prop=self._prop_status) == self._prop_status_opening # The status has higher priority when determining whether the cover
# is opening.
return self._prop_pos_opening
@property @property
def is_closing(self) -> Optional[bool]: def is_closing(self) -> Optional[bool]:
"""Return if the cover is closing.""" """Return if the cover is closing."""
if self._prop_status is None: if self._prop_status and self._prop_status_closing:
return None return (self.get_prop_value(prop=self._prop_status)
return self.get_prop_value( in self._prop_status_closing)
prop=self._prop_status) == self._prop_status_closing # The status has higher priority when determining whether the cover
# is closing.
return self._prop_pos_closing
@property @property
def is_closed(self) -> Optional[bool]: def is_closed(self) -> Optional[bool]:
"""Return if the cover is closed.""" """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

@ -46,6 +46,7 @@ off Xiaomi or its affiliates' products.
Event entities for Xiaomi Home. Event entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any from typing import Any
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -57,6 +58,8 @@ from .miot.miot_spec import MIoTSpecEvent
from .miot.miot_device import MIoTDevice, MIoTEventEntity from .miot.miot_device import MIoTDevice, MIoTEventEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -89,4 +92,5 @@ class Event(MIoTEventEntity, EventEntity):
self, name: str, arguments: dict[str, Any] | None = None self, name: str, arguments: dict[str, Any] | None = None
) -> None: ) -> None:
"""An event is occurred.""" """An event is occurred."""
_LOGGER.debug('%s, attributes: %s', name, str(arguments))
self._trigger_event(event_type=name, event_attributes=arguments) self._trigger_event(event_type=name, event_attributes=arguments)

View File

@ -172,7 +172,7 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.OSCILLATE self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._prop_horizontal_swing = prop self._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse': elif prop.name == 'wind-reverse':
if prop.format_ == 'bool': if prop.format_ == bool:
self._prop_wind_reverse_forward = False self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True self._prop_wind_reverse_reverse = True
elif prop.value_list: elif prop.value_list:
@ -186,7 +186,7 @@ class Fan(MIoTServiceEntity, FanEntity):
or self._prop_wind_reverse_reverse is None or self._prop_wind_reverse_reverse is None
): ):
# NOTICE: Value may be 0 or False # NOTICE: Value may be 0 or False
_LOGGER.info( _LOGGER.error(
'invalid wind-reverse, %s', self.entity_id) 'invalid wind-reverse, %s', self.entity_id)
continue continue
self._attr_supported_features |= FanEntityFeature.DIRECTION self._attr_supported_features |= FanEntityFeature.DIRECTION
@ -236,6 +236,9 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan speed.""" """Set the percentage of the fan speed."""
if percentage > 0: if percentage > 0:
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
if self._speed_names: if self._speed_names:
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, prop=self._prop_fan_level,
@ -249,9 +252,6 @@ class Fan(MIoTServiceEntity, FanEntity):
value=int(percentage_to_ranged_value( value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max), low_high_range=(self._speed_min, self._speed_max),
percentage=percentage))) percentage=percentage)))
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
else: else:
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)

View File

@ -52,11 +52,10 @@ from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.humidifier import ( from homeassistant.components.humidifier import (HumidifierEntity,
HumidifierEntity,
HumidifierDeviceClass, HumidifierDeviceClass,
HumidifierEntityFeature HumidifierEntityFeature,
) HumidifierAction)
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
@ -82,8 +81,8 @@ async def async_setup_entry(
Humidifier(miot_device=miot_device, entity_data=data)) Humidifier(miot_device=miot_device, entity_data=data))
for data in miot_device.entity_list.get('dehumidifier', []): for data in miot_device.entity_list.get('dehumidifier', []):
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
new_entities.append(Humidifier( new_entities.append(
miot_device=miot_device, entity_data=data)) Humidifier(miot_device=miot_device, entity_data=data))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -99,9 +98,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
_mode_map: dict[Any, Any] _mode_map: dict[Any, Any]
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Humidifier.""" """Initialize the Humidifier."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = entity_data.device_class self._attr_device_class = entity_data.device_class
@ -130,12 +128,10 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
# mode # mode
elif prop.name == 'mode': elif prop.name == 'mode':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('mode value_list is None, %s', self.entity_id)
'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
self._attr_available_modes = list( self._attr_available_modes = list(self._mode_map.values())
self._mode_map.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop self._prop_mode = prop
# relative-humidity # relative-humidity
@ -152,33 +148,45 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
await self.set_property_async( if self._prop_target_humidity is None:
prop=self._prop_target_humidity, value=humidity) return
await self.set_property_async(prop=self._prop_target_humidity,
value=humidity)
async def async_set_mode(self, mode: str) -> None: async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode.""" """Set new target preset mode."""
await self.set_property_async( await self.set_property_async(prop=self._prop_mode,
prop=self._prop_mode, value=self.get_map_key(
value=self.get_map_key(map_=self._mode_map, value=mode)) map_=self._mode_map, value=mode))
@property @property
def is_on(self) -> Optional[bool]: def is_on(self) -> Optional[bool]:
"""Return if the humidifier is on.""" """Return if the humidifier is on."""
return self.get_prop_value(prop=self._prop_on) return self.get_prop_value(prop=self._prop_on)
@property
def action(self) -> Optional[HumidifierAction]:
"""Return the current status of the device."""
if not self.is_on:
return HumidifierAction.OFF
if self._attr_device_class == HumidifierDeviceClass.HUMIDIFIER:
return HumidifierAction.HUMIDIFYING
return HumidifierAction.DRYING
@property @property
def current_humidity(self) -> Optional[int]: def current_humidity(self) -> Optional[int]:
"""Return the current humidity.""" """Return the current humidity."""
return self.get_prop_value(prop=self._prop_humidity) return (self.get_prop_value(
prop=self._prop_humidity) if self._prop_humidity else None)
@property @property
def target_humidity(self) -> Optional[int]: def target_humidity(self) -> Optional[int]:
"""Return the target humidity.""" """Return the target humidity."""
return self.get_prop_value(prop=self._prop_target_humidity) return (self.get_prop_value(prop=self._prop_target_humidity)
if self._prop_target_humidity else None)
@property @property
def mode(self) -> Optional[str]: def mode(self) -> Optional[str]:
"""Return the current preset mode.""" """Return the current preset mode."""
return self.get_map_value( return self.get_map_value(map_=self._mode_map,
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))

View File

@ -179,7 +179,7 @@ class Light(MIoTServiceEntity, LightEntity):
) / prop.value_range.step) ) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX > self._VALUE_RANGE_MODE_COUNT_MAX
): ):
_LOGGER.info( _LOGGER.error(
'too many mode values, %s, %s, %s', 'too many mode values, %s, %s, %s',
self.entity_id, prop.name, prop.value_range) self.entity_id, prop.name, prop.value_range)
else: else:

View File

@ -20,12 +20,12 @@
], ],
"requirements": [ "requirements": [
"construct>=2.10.56", "construct>=2.10.56",
"paho-mqtt<2.0.0", "paho-mqtt",
"numpy", "numpy",
"cryptography", "cryptography",
"psutil" "psutil"
], ],
"version": "v0.1.5b2", "version": "v0.3.2",
"zeroconf": [ "zeroconf": [
"_miot-central._tcp.local." "_miot-central._tcp.local."
] ]

View File

@ -646,7 +646,8 @@ class MIoTClient:
result = await self._miot_lan.set_prop_async( result = await self._miot_lan.set_prop_async(
did=did, siid=siid, piid=piid, value=value) did=did, siid=siid, piid=piid, value=value)
_LOGGER.debug( _LOGGER.debug(
'lan set prop, %s, %s, %s -> %s', did, siid, piid, result) 'lan set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
rc = (result or {}).get( rc = (result or {}).get(
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) 'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
if rc in [0, 1]: if rc in [0, 1]:
@ -879,16 +880,7 @@ class MIoTClient:
sub_from = self._sub_source_list.pop(did, None) sub_from = self._sub_source_list.pop(did, None)
# Unsub # Unsub
if sub_from: if sub_from:
if sub_from == 'cloud': self.__unsub_from(sub_from, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif sub_from == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Storage # Storage
await self._storage.save_async( await self._storage.save_async(
domain='miot_devices', domain='miot_devices',
@ -936,6 +928,39 @@ class MIoTClient:
delay_sec, lambda: self._main_loop.create_task( delay_sec, lambda: self._main_loop.create_task(
self.refresh_user_cert_async())) self.refresh_user_cert_async()))
@final
def __unsub_from(self, sub_from: str, did: str) -> None:
mips: Any = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
try:
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
except RuntimeError as e:
if 'Event loop is closed' in str(e):
# Ignore unsub exception when loop is closed
pass
else:
raise
@final
def __sub_from(self, sub_from: str, did: str) -> None:
mips = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
@final @final
def __update_device_msg_sub(self, did: str) -> None: def __update_device_msg_sub(self, did: str) -> None:
if did not in self._device_list_cache: if did not in self._device_list_cache:
@ -967,27 +992,9 @@ class MIoTClient:
return return
# Unsub old # Unsub old
if from_old: if from_old:
if from_old == 'cloud': self.__unsub_from(from_old, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif from_old == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif from_old in self._mips_local:
mips = self._mips_local[from_old]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Sub new # Sub new
if from_new == 'cloud': self.__sub_from(from_new, did)
self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg)
self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg)
elif from_new == 'lan':
self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg)
self._miot_lan.sub_event(did=did, handler=self.__on_event_msg)
elif from_new in self._mips_local:
mips = self._mips_local[from_new]
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
self._sub_source_list[did] = from_new self._sub_source_list[did] = from_new
_LOGGER.info( _LOGGER.info(
'device sub changed, %s, from %s to %s', did, from_old, from_new) 'device sub changed, %s, from %s to %s', did, from_old, from_new)

View File

@ -444,6 +444,17 @@ class MIoTHttpClient:
return home_list return home_list
async def get_separated_shared_devices_async(self) -> dict[str, dict]:
separated_shared_devices: dict = {}
device_list: dict[str, dict] = await self.__get_device_list_page_async(
dids=[], start_did=None)
for did, value in device_list.items():
if value['owner'] is not None and ('userid' in value['owner']) and (
'nickname' in value['owner']
):
separated_shared_devices.setdefault(did, value['owner'])
return separated_shared_devices
async def get_homeinfos_async(self) -> dict: async def get_homeinfos_async(self) -> dict:
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/gethome', url_path='/app/v2/homeroom/gethome',
@ -499,18 +510,21 @@ class MIoTHttpClient:
): ):
more_list = await self.__get_dev_room_page_async( more_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id']) max_id=res_obj['result']['max_id'])
for device_source in ['homelist', 'share_home_list']:
for home_id, info in more_list.items(): for home_id, info in more_list.items():
if home_id not in home_infos['homelist']: if home_id not in home_infos[device_source]:
_LOGGER.info('unknown home, %s, %s', home_id, info) _LOGGER.info('unknown home, %s, %s', home_id, info)
continue continue
home_infos['homelist'][home_id]['dids'].extend(info['dids']) home_infos[device_source][home_id]['dids'].extend(
info['dids'])
for room_id, info in info['room_info'].items(): for room_id, info in info['room_info'].items():
home_infos['homelist'][home_id]['room_info'].setdefault( home_infos[device_source][home_id][
'room_info'].setdefault(
room_id, { room_id, {
'room_id': room_id, 'room_id': room_id,
'room_name': '', 'room_name': '',
'dids': []}) 'dids': []})
home_infos['homelist'][home_id]['room_info'][ home_infos[device_source][home_id]['room_info'][
room_id]['dids'].extend(info['dids']) room_id]['dids'].extend(info['dids'])
return { return {
@ -651,6 +665,25 @@ class MIoTHttpClient:
'room_name': room_name, 'room_name': room_name,
'group_id': group_id 'group_id': group_id
} for did in room_info.get('dids', [])}) } for did in room_info.get('dids', [])})
separated_shared_devices: dict = (
await self.get_separated_shared_devices_async())
if separated_shared_devices:
homes.setdefault('separated_shared_list', {})
for did, owner in separated_shared_devices.items():
owner_id = str(owner['userid'])
homes['separated_shared_list'].setdefault(owner_id,{
'home_name': owner['nickname'],
'uid': owner_id,
'group_id': 'NotSupport',
'room_info': {'shared_device': 'shared_device'}
})
devices.update({did: {
'home_id': owner_id,
'home_name': owner['nickname'],
'room_id': 'shared_device',
'room_name': 'shared_device',
'group_id': 'NotSupport'
}})
dids = sorted(list(devices.keys())) dids = sorted(list(devices.keys()))
results = await self.get_devices_with_dids_async(dids=dids) results = await self.get_devices_with_dids_async(dids=dids)
if results is None: if results is None:

View File

@ -345,10 +345,11 @@ class MIoTDevice:
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}') f'{self._model_strs[-1][:20]}')
def gen_service_entity_id(self, ha_domain: str, siid: int) -> str: def gen_service_entity_id(self, ha_domain: str, siid: int,
description: str) -> str:
return ( return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_' f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_s_{siid}') f'{self._model_strs[-1][:20]}_s_{siid}_{description}')
def gen_prop_entity_id( def gen_prop_entity_id(
self, ha_domain: str, spec_name: str, siid: int, piid: int self, ha_domain: str, spec_name: str, siid: int, piid: int
@ -549,6 +550,10 @@ class MIoTDevice:
# Optional actions # Optional actions
# Optional events # Optional events
miot_service.platform = platform miot_service.platform = platform
# entity_category
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
'entity_category', None):
miot_service.entity_category = entity_category
return entity_data return entity_data
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
@ -587,13 +592,8 @@ class MIoTDevice:
# Priority: spec_modify.unit > unit_convert > specv2entity.unit # Priority: spec_modify.unit > unit_convert > specv2entity.unit
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][ miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
prop_name]['unit_of_measurement'] prop_name]['unit_of_measurement']
if ( # Priority: default.icon when device_class is set > spec_modify.icon
not miot_prop.icon # > icon_convert
and 'icon' in SPEC_PROP_TRANS_MAP['properties'][prop_name]
):
# Priority: spec_modify.icon > icon_convert > specv2entity.icon
miot_prop.icon = SPEC_PROP_TRANS_MAP['properties'][prop_name][
'icon']
miot_prop.platform = platform miot_prop.platform = platform
return True return True
@ -779,8 +779,10 @@ class MIoTDevice:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from homeassistant.const import UnitOfConductivity # type: ignore from homeassistant.const import UnitOfConductivity # type: ignore
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
unit_map['μS/cm'] = 'μS/cm' unit_map['μS/cm'] = 'μS/cm'
unit_map['mWh'] = 'mWh'
return unit_map.get(spec_unit, None) return unit_map.get(spec_unit, None)
@ -895,10 +897,12 @@ class MIoTServiceEntity(Entity):
self._attr_name = f' {self.entity_data.spec.description_trans}' self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(self.entity_data.spec, MIoTSpecService): elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id( self.entity_id = miot_device.gen_service_entity_id(
DOMAIN, siid=self.entity_data.spec.iid) DOMAIN, siid=self.entity_data.spec.iid,
description=self.entity_data.spec.description)
self._attr_name = ( self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}' f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}') f'{self.entity_data.spec.description_trans}')
self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
self._attr_should_poll = False self._attr_should_poll = False

View File

@ -226,7 +226,7 @@ class _MIoTLanDevice:
def gen_packet( def gen_packet(
self, out_buffer: bytearray, clear_data: dict, did: str, offset: int self, out_buffer: bytearray, clear_data: dict, did: str, offset: int
) -> int: ) -> int:
clear_bytes = json.dumps(clear_data).encode('utf-8') clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode('utf-8')
padder = padding.PKCS7(algorithms.AES128.block_size).padder() padder = padding.PKCS7(algorithms.AES128.block_size).padder()
padded_data = padder.update(clear_bytes) + padder.finalize() padded_data = padder.update(clear_bytes) + padder.finalize()
if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer): if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer):

View File

@ -1213,10 +1213,13 @@ class MipsLocalClient(_MipsClient):
or 'did' not in msg or 'did' not in msg
or 'siid' not in msg or 'siid' not in msg
or 'eiid' 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}') self.log_info('unknown event msg, %s', payload)
return return
if 'arguments' not in msg:
self.log_info('wrong event msg, %s', payload)
msg['arguments'] = []
if handler: if handler:
self.log_debug('local, on event_occurred, %s', payload) self.log_debug('local, on event_occurred, %s', payload)
handler(msg, ctx) handler(msg, ctx)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
{
"urn:miot-spec-v2:device:bath-heater:0000A028:yeelink-v10": {
"en": {
"service:003:property:001:valuelist:000": "Idle",
"service:003:property:001:valuelist:001": "Dry"
}
},
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"de": {
"service:001": "Geräteinformationen",
"service:001:property:003": "Geräte-ID",
"service:001:property:005": "Seriennummer (SN)",
"service:002": "Gateway",
"service:002:event:001": "Netzwerk geändert",
"service:002:event:002": "Netzwerk geändert",
"service:002:property:001": "Zugriffsmethode",
"service:002:property:001:valuelist:000": "Kabelgebunden",
"service:002:property:001:valuelist:001": "5G Drahtlos",
"service:002:property:001:valuelist:002": "2.4G Drahtlos",
"service:002:property:002": "IP-Adresse",
"service:002:property:003": "WiFi-Netzwerkname",
"service:002:property:004": "Aktuelle Zeit",
"service:002:property:005": "DHCP-Server-MAC-Adresse",
"service:003": "Anzeigelampe",
"service:003:property:001": "Schalter",
"service:004": "Virtueller Dienst",
"service:004:action:001": "Virtuelles Ereignis erzeugen",
"service:004:event:001": "Virtuelles Ereignis aufgetreten",
"service:004:property:001": "Ereignisname"
},
"en": {
"service:001": "Device Information",
"service:001:property:003": "Device ID",
"service:001:property:005": "Serial Number (SN)",
"service:002": "Gateway",
"service:002:event:001": "Network Changed",
"service:002:event:002": "Network Changed",
"service:002:property:001": "Access Method",
"service:002:property:001:valuelist:000": "Wired",
"service:002:property:001:valuelist:001": "5G Wireless",
"service:002:property:001:valuelist:002": "2.4G Wireless",
"service:002:property:002": "IP Address",
"service:002:property:003": "WiFi Network Name",
"service:002:property:004": "Current Time",
"service:002:property:005": "DHCP Server MAC Address",
"service:003": "Indicator Light",
"service:003:property:001": "Switch",
"service:004": "Virtual Service",
"service:004:action:001": "Generate Virtual Event",
"service:004:event:001": "Virtual Event Occurred",
"service:004:property:001": "Event Name"
},
"es": {
"service:001": "Información del dispositivo",
"service:001:property:003": "ID del dispositivo",
"service:001:property:005": "Número de serie (SN)",
"service:002": "Puerta de enlace",
"service:002:event:001": "Cambio de red",
"service:002:event:002": "Cambio de red",
"service:002:property:001": "Método de acceso",
"service:002:property:001:valuelist:000": "Cableado",
"service:002:property:001:valuelist:001": "5G inalámbrico",
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
"service:002:property:002": "Dirección IP",
"service:002:property:003": "Nombre de red WiFi",
"service:002:property:004": "Hora actual",
"service:002:property:005": "Dirección MAC del servidor DHCP",
"service:003": "Luz indicadora",
"service:003:property:001": "Interruptor",
"service:004": "Servicio virtual",
"service:004:action:001": "Generar evento virtual",
"service:004:event:001": "Ocurrió un evento virtual",
"service:004:property:001": "Nombre del evento"
},
"fr": {
"service:001": "Informations sur l'appareil",
"service:001:property:003": "ID de l'appareil",
"service:001:property:005": "Numéro de série (SN)",
"service:002": "Passerelle",
"service:002:event:001": "Changement de réseau",
"service:002:event:002": "Changement de réseau",
"service:002:property:001": "Méthode d'accès",
"service:002:property:001:valuelist:000": "Câblé",
"service:002:property:001:valuelist:001": "Sans fil 5G",
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
"service:002:property:002": "Adresse IP",
"service:002:property:003": "Nom du réseau WiFi",
"service:002:property:004": "Heure actuelle",
"service:002:property:005": "Adresse MAC du serveur DHCP",
"service:003": "Voyant lumineux",
"service:003:property:001": "Interrupteur",
"service:004": "Service virtuel",
"service:004:action:001": "Générer un événement virtuel",
"service:004:event:001": "Événement virtuel survenu",
"service:004:property:001": "Nom de l'événement"
},
"ja": {
"service:001": "デバイス情報",
"service:001:property:003": "デバイスID",
"service:001:property:005": "シリアル番号 (SN)",
"service:002": "ゲートウェイ",
"service:002:event:001": "ネットワークが変更されました",
"service:002:event:002": "ネットワークが変更されました",
"service:002:property:001": "アクセス方法",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G ワイヤレス",
"service:002:property:001:valuelist:002": "2.4G ワイヤレス",
"service:002:property:002": "IPアドレス",
"service:002:property:003": "WiFiネットワーク名",
"service:002:property:004": "現在の時間",
"service:002:property:005": "DHCPサーバーMACアドレス",
"service:003": "インジケータライト",
"service:003:property:001": "スイッチ",
"service:004": "バーチャルサービス",
"service:004:action:001": "バーチャルイベントを生成",
"service:004:event:001": "バーチャルイベントが発生しました",
"service:004:property:001": "イベント名"
},
"ru": {
"service:001": "Информация об устройстве",
"service:001:property:003": "ID устройства",
"service:001:property:005": "Серийный номер (SN)",
"service:002": "Шлюз",
"service:002:event:001": "Сеть изменена",
"service:002:event:002": "Сеть изменена",
"service:002:property:001": "Метод доступа",
"service:002:property:001:valuelist:000": "Проводной",
"service:002:property:001:valuelist:001": "5G Беспроводной",
"service:002:property:001:valuelist:002": "2.4G Беспроводной",
"service:002:property:002": "IP Адрес",
"service:002:property:003": "Название WiFi сети",
"service:002:property:004": "Текущее время",
"service:002:property:005": "MAC адрес DHCP сервера",
"service:003": "Световой индикатор",
"service:003:property:001": "Переключатель",
"service:004": "Виртуальная служба",
"service:004:action:001": "Создать виртуальное событие",
"service:004:event:001": "Произошло виртуальное событие",
"service:004:property:001": "Название события"
},
"zh-Hant": {
"service:001": "設備信息",
"service:001:property:003": "設備ID",
"service:001:property:005": "序號 (SN)",
"service:002": "網關",
"service:002:event:001": "網路發生變化",
"service:002:event:002": "網路發生變化",
"service:002:property:001": "接入方式",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G 無線",
"service:002:property:001:valuelist:002": "2.4G 無線",
"service:002:property:002": "IP地址",
"service:002:property:003": "WiFi網路名稱",
"service:002:property:004": "當前時間",
"service:002:property:005": "DHCP伺服器MAC地址",
"service:003": "指示燈",
"service:003:property:001": "開關",
"service:004": "虛擬服務",
"service:004:action:001": "產生虛擬事件",
"service:004:event:001": "虛擬事件發生",
"service:004:property:001": "事件名稱"
}
},
"urn:miot-spec-v2:device:lock:0000A038:loock-t2pv1": {
"zh-Hans": {
"service:003:property:1021:valuelist:000": "已上锁",
"service:003:property:1021:valuelist:001": "已上锁(童锁)",
"service:003:property:1021:valuelist:002": "已上锁(反锁)",
"service:003:property:1021:valuelist:003": "已上锁(反锁+童锁)",
"service:003:property:1021:valuelist:004": "已开锁",
"service:003:property:1021:valuelist:008": "门未关(门超时未关)",
"service:003:property:1021:valuelist:012": "门虚掩"
}
},
"urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1": {
"en": {
"service:002:property:001": "Soil Moisture"
},
"zh-Hans": {
"service:002:property:001": "土壤湿度",
"service:002:property:003": "光照强度"
}
},
"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040": {
"en": {
"service:011": "Right Button On and Off",
"service:011:property:001": "Right Button On and Off",
"service:015:action:001": "Left Button Identify",
"service:016:action:001": "Middle Button Identify",
"service:017:action:001": "Right Button Identify"
},
"zh-Hans": {
"service:015:action:001": "左键确认",
"service:016:action:001": "中键确认",
"service:017:action:001": "右键确认"
}
}
}

View File

@ -0,0 +1,62 @@
{
"urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1": [
{
"iid": 3,
"type": "urn:miot-spec-v2:service:light:00007802:hyd-lyjpro:1",
"description": "Moon Light",
"properties": [
{
"iid": 2,
"type": "urn:miot-spec-v2:property:on:00000006:hyd-lyjpro:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:xiaomi-yms2:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:xiaomi-yms2:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:zimi-h03:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:zimi-h03:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:zimi-h03:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
]
}

View File

@ -5,6 +5,9 @@ urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
- 15.* - 15.*
services: services:
- '10' - '10'
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:
properties:
- '3.2'
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01: urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
properties: properties:
- '5.1' - '5.1'

View File

@ -1,3 +1,85 @@
urn:miot-spec-v2:device:air-condition-outlet:0000A045:lumi-mcn04:1:
prop.3.4:
format: uint8
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5:
name: voc-density
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1:
prop.2.3:
name: current-position-a
prop.2.8:
name: target-position-a
prop.2.9:
name: target-position-b
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:airer:0000A00D:mrbond-m33a:1:
prop.2.3:
name: current-position-a
prop.2.11:
name: current-position-b
urn:miot-spec-v2:device:bath-heater:0000A028:mike-2:1:
prop.3.1:
name: mode-a
prop.3.11:
name: mode-b
prop.3.12:
name: mode-c
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
urn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1:
prop.4.4:
name: fan-level-ventilation
urn:miot-spec-v2:device:fan:0000A005:dmaker-p5:1:
prop.2.4:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-sa1:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-v3:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-za4:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1:
prop.2.1:
access:
- read
- notify
prop.2.2:
icon: mdi:ip
prop.2.3:
access:
- read
- notify
prop.2.5:
access:
- read
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
prop.2.1: prop.2.1:
name: access-mode name: access-mode
@ -14,25 +96,86 @@ urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
- notify - notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1 urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:kettle:0000A009:yunmi-r3:1:
prop.3.1:
unit: ppm
urn:miot-spec-v2:device:light:0000A001:shhf-sfla10:1:
prop.8.9:
name: wind-reverse
urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:
prop.8.11:
name: on-a
urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1:
prop.2.1004:
name: contact-state
value-list:
- value: 0
description: open
- value: 1
description: closed
expr: src_value!=1
urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:
prop.3.2:
access:
- read
- notify
unit: mV
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824:
prop.2.6:
unit: cm
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-hb01:2:0000C824:
prop.3.3:
unit: m
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3:
prop.5.1: prop.5.1:
name: power-consumption expr: round(src_value*6/1000000, 3)
expr: round(src_value/1000, 3)
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
prop.2.2: prop.2.2:
name: power-consumption name: power-consumption
expr: round(src_value/1000, 3) expr: round(src_value/1000, 3)
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2:
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
prop.3.2:
expr: round(src_value/10, 1)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:
prop.11.1: prop.11.1:
name: power-consumption name: power-consumption
expr: round(src_value/100, 2) expr: round(src_value/100, 2)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1 urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1
urn:miot-spec-v2:device:outlet:0000A002:giot-v8icm:1:0000C816:
prop.4.1:
unit: mWh
urn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816:
prop.3.3:
unit: mV
prop.3.4:
unit: mA
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:1:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
prop.3.1: prop.3.1:
name: electric-power name: electric-power
expr: round(src_value/100, 2) expr: round(src_value/100, 2)
urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1:1:
prop.2.1:
name: soil-moisture
icon: mdi:watering-can
prop.2.2:
name: soil-ec
icon: mdi:sprout-outline
unit: μS/cm
urn:miot-spec-v2:device:relay:0000A03D:lumi-c2acn01:1:
prop.4.1:
unit: kWh
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1: urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
prop.2.1: prop.2.1:
name: download-speed name: download-speed
@ -42,3 +185,59 @@ urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
name: upload-speed name: upload-speed
icon: mdi:upload icon: mdi:upload
unit: B/s unit: B/s
urn:miot-spec-v2:device:safe-box:0000A042:loock-v1:1:
prop.5.1:
name: contact-state
expr: src_value!=1
urn:miot-spec-v2:device:switch:0000A003:lxzn-cbcsmj:1:0000D00D:
prop.3.1:
expr: round(src_value/100, 2)
prop.3.2:
expr: round(src_value/1000, 2)
prop.3.3:
expr: round(src_value/10, 1)
urn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:
prop.2.3:
value-list:
- value: 1
description: '1'
- value: 2
description: '2'
- value: 3
description: '3'
- value: 4
description: '4'
- value: 5
description: '5'
- value: 6
description: '6'
- value: 7
description: '7'
- value: 8
description: '8'
- value: 9
description: '9'
- value: 10
description: '10'
- value: 11
description: '11'
- value: 12
description: '12'
- value: 13
description: '13'
- value: 14
description: '14'
- value: 15
description: '15'
- value: 16
description: '16'
urn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm
urn:miot-spec-v2:device:water-purifier:0000A013:yunmi-s20:1:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm

View File

@ -48,9 +48,11 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import SensorStateClass from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX, LIGHT_LUX,
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
@ -137,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'mode', 'target-humidity'} 'properties': {'mode', 'target-humidity'}
} }
}, }
}, },
'optional': { 'optional': {
'environment': { 'environment': {
@ -163,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'continue-sweep', 'continue-sweep',
'stop-and-gocharge' 'stop-and-gocharge'
} }
}, }
} }
}, },
'optional': { 'optional': {
@ -177,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'required': { 'required': {
'properties': { 'properties': {
'battery-level': {'read'} 'battery-level': {'read'}
},
} }
}, }
}
}, },
'entity': 'vacuum' 'entity': 'vacuum'
}, },
@ -195,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-humidity'} 'properties': {'target-humidity'}
}, }
} }
}, },
'optional': { 'optional': {
@ -224,6 +225,31 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'entity': 'air-conditioner' 'entity': 'air-conditioner'
}, },
'air-condition-outlet': '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': { 'heater': {
'required': { 'required': {
'heater': { 'heater': {
@ -234,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': {'target-temperature', 'heat-level'} 'properties': {'target-temperature', 'heat-level'}
}, }
} }
}, },
'optional': { 'optional': {
@ -243,10 +269,58 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': { 'optional': {
'properties': {'temperature', 'relative-humidity'} 'properties': {'temperature', 'relative-humidity'}
} }
}, }
}, },
'entity': 'heater' 'entity': 'heater'
}, },
'bath-heater': {
'required': {
'ptc-bath-heater': {
'required': {
'properties': {
'mode':{'read', 'write'}
}
},
'optional': {
'properties': {'target-temperature', 'temperature'}
}
}
},
'optional': {
'fan-control': {
'required': {},
'optional': {
'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
}
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature'}
}
}
},
'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'
},
'speaker': { 'speaker': {
'required': { 'required': {
'speaker': { 'speaker': {
@ -322,7 +396,8 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'events': set<event instance name: str>, 'events': set<event instance name: str>,
'actions': set<action instance name: str> 'actions': set<action instance name: str>
}, },
'entity': str 'entity': str,
'entity_category'?: str
} }
} }
""" """
@ -340,10 +415,23 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'entity': 'light' 'entity': 'light'
}, },
'indicator-light': 'light',
'ambient-light': 'light', 'ambient-light': 'light',
'night-light': 'light', 'night-light': 'light',
'white-light': 'light', 'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': { 'fan': {
'required': { 'required': {
'properties': { 'properties': {
@ -358,6 +446,8 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'fan-control': 'fan', 'fan-control': 'fan',
'ceiling-fan': 'fan', 'ceiling-fan': 'fan',
'air-fresh': 'fan',
'air-purifier': 'fan',
'water-heater': { 'water-heater': {
'required': { 'required': {
'properties': { 'properties': {
@ -365,7 +455,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
} }
}, },
'optional': { 'optional': {
'properties': {'on', 'temperature', 'target-temperature', 'mode'} 'properties': {'temperature', 'target-temperature', 'mode'}
}, },
'entity': 'water_heater' 'entity': 'water_heater'
}, },
@ -377,12 +467,27 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}, },
'optional': { 'optional': {
'properties': { 'properties': {
'motor-control', 'status', 'current-position', 'target-position' 'status', 'current-position', 'target-position'
} }
}, },
'entity': 'cover' 'entity': 'cover'
}, },
'window-opener': 'curtain' 'window-opener': 'curtain',
'motor-controller': 'curtain',
'airer': 'curtain',
'air-conditioner': {
'required': {
'properties': {
'on': {'read', 'write'},
'mode': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'target-humidity'}
},
'entity': 'air-conditioner'
}
} }
"""SPEC_PROP_TRANS_MAP """SPEC_PROP_TRANS_MAP
@ -409,12 +514,28 @@ SPEC_PROP_TRANS_MAP: dict = {
'format': {'int', 'float'}, 'format': {'int', 'float'},
'access': {'read'} 'access': {'read'}
}, },
'binary_sensor': {
'format': {'bool', 'int'},
'access': {'read'}
},
'switch': { 'switch': {
'format': {'bool'}, 'format': {'bool'},
'access': {'read', 'write'} 'access': {'read', 'write'}
} }
}, },
'properties': { 'properties': {
'submersion-state': {
'device_class': BinarySensorDeviceClass.MOISTURE,
'entity': 'binary_sensor'
},
'contact-state': {
'device_class': BinarySensorDeviceClass.DOOR,
'entity': 'binary_sensor'
},
'occupancy-status': {
'device_class': BinarySensorDeviceClass.OCCUPANCY,
'entity': 'binary_sensor',
},
'temperature': { 'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE, 'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor', 'entity': 'sensor',
@ -461,7 +582,11 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor', 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT 'state_class': SensorStateClass.MEASUREMENT
}, },
'voc-density': 'tvoc-density', 'voc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
},
'battery-level': { 'battery-level': {
'device_class': SensorDeviceClass.BATTERY, 'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor', 'entity': 'sensor',
@ -515,12 +640,6 @@ SPEC_PROP_TRANS_MAP: dict = {
'entity': 'sensor', 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT, 'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT 'unit_of_measurement': UnitOfPower.WATT
},
'total-battery': {
'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor',
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
} }
} }
} }

View File

@ -88,7 +88,7 @@ class Number(MIoTPropertyEntity, NumberEntity):
if self.spec.external_unit: if self.spec.external_unit:
self._attr_native_unit_of_measurement = self.spec.external_unit self._attr_native_unit_of_measurement = self.spec.external_unit
# Set icon # Set icon
if self.spec.icon: if self.spec.icon and not self.device_class:
self._attr_icon = self.spec.icon self._attr_icon = self.spec.icon
# Set value range # Set value range
if self._value_range: if self._value_range:

View File

@ -110,13 +110,13 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_native_unit_of_measurement = list( self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None unit_sets)[0] if unit_sets else None
# Set suggested precision # Set suggested precision
if spec.format_ in {int, float}: if spec.format_ in {int, float} and spec.expr is None:
self._attr_suggested_display_precision = spec.precision self._attr_suggested_display_precision = spec.precision
# Set state_class # Set state_class
if spec.state_class: if spec.state_class:
self._attr_state_class = spec.state_class self._attr_state_class = spec.state_class
# Set icon # Set icon
if spec.icon: if spec.icon and not self.device_class:
self._attr_icon = spec.icon self._attr_icon = spec.icon
@property @property

View File

@ -52,13 +52,10 @@ from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (STATE_ON, STATE_OFF,
STATE_ON,
STATE_OFF,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
WaterHeaterEntity, WaterHeaterEntity,
WaterHeaterEntityFeature WaterHeaterEntityFeature)
)
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
@ -79,8 +76,8 @@ async def async_setup_entry(
new_entities = [] new_entities = []
for miot_device in device_list: for miot_device in device_list:
for data in miot_device.entity_list.get('water_heater', []): for data in miot_device.entity_list.get('water_heater', []):
new_entities.append(WaterHeater( new_entities.append(
miot_device=miot_device, entity_data=data)) WaterHeater(miot_device=miot_device, entity_data=data))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -95,12 +92,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
_mode_map: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Water heater.""" """Initialize the Water heater."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_temperature_unit = None # type: ignore self._attr_temperature_unit = None
self._attr_supported_features = WaterHeaterEntityFeature(0) self._attr_supported_features = WaterHeaterEntityFeature(0)
self._prop_on = None self._prop_on = None
self._prop_temp = None self._prop_temp = None
@ -117,14 +113,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
# temperature # temperature
if prop.name == 'temperature': if prop.name == 'temperature':
if not prop.value_range: if not prop.value_range:
_LOGGER.error( _LOGGER.error('invalid temperature value_range format, %s',
'invalid temperature value_range format, %s',
self.entity_id) self.entity_id)
continue continue
if prop.external_unit: if prop.external_unit:
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._prop_temp = prop self._prop_temp = prop
# target-temperature # target-temperature
if prop.name == 'target-temperature': if prop.name == 'target-temperature':
@ -133,9 +126,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
'invalid target-temperature value_range format, %s', 'invalid target-temperature value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._attr_target_temperature_low = prop.value_range.min_ self._attr_min_temp = prop.value_range.min_
self._attr_target_temperature_high = prop.value_range.max_ self._attr_max_temp = prop.value_range.max_
self._attr_precision = prop.value_range.step self._attr_target_temperature_step = prop.value_range.step
if self._attr_temperature_unit is None and prop.external_unit: if self._attr_temperature_unit is None and prop.external_unit:
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= ( self._attr_supported_features |= (
@ -144,8 +137,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
# mode # mode
if prop.name == 'mode': if prop.name == 'mode':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('mode value_list is None, %s', self.entity_id)
'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values()) self._attr_operation_list = list(self._mode_map.values())
@ -165,16 +157,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature the water heater should heat water to.""" """Set the target temperature."""
if not self._prop_target_temp: await self.set_property_async(prop=self._prop_target_temp,
return value=kwargs[ATTR_TEMPERATURE])
await self.set_property_async(
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
async def async_set_operation_mode(self, operation_mode: str) -> None: async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set the operation mode of the water heater. """Set the operation mode of the water heater."""
Must be in the operation_list.
"""
if operation_mode == STATE_OFF: if operation_mode == STATE_OFF:
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)
return return
@ -182,32 +170,32 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
await self.set_property_async(prop=self._prop_on, value=True) await self.set_property_async(prop=self._prop_on, value=True)
return return
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async( await self.set_property_async(prop=self._prop_on,
prop=self._prop_on, value=True, write_ha_state=False) value=True,
await self.set_property_async( write_ha_state=False)
prop=self._prop_mode, await self.set_property_async(prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, value=operation_mode)) map_=self._mode_map,
value=operation_mode))
@property @property
def current_temperature(self) -> Optional[float]: def current_temperature(self) -> Optional[float]:
"""Return the current temperature.""" """The current temperature."""
return self.get_prop_value(prop=self._prop_temp) return (None if self._prop_temp is None else self.get_prop_value(
prop=self._prop_temp))
@property @property
def target_temperature(self) -> Optional[float]: def target_temperature(self) -> Optional[float]:
"""Return the target temperature.""" """The target temperature."""
if not self._prop_target_temp: return (None if self._prop_target_temp is None else self.get_prop_value(
return None prop=self._prop_target_temp))
return self.get_prop_value(prop=self._prop_target_temp)
@property @property
def current_operation(self) -> Optional[str]: def current_operation(self) -> Optional[str]:
"""Return the current mode.""" """The current mode."""
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
return STATE_OFF return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on): if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON return STATE_ON
return self.get_map_value( return self.get_map_value(map_=self._mode_map,
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### 方法 2: [HACS](https://hacs.xyz/) ### 方法 2: [HACS](https://hacs.xyz/)
HACS > 右上角三个点 > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > 点击 HACS 的 New 或 Available for download 分类下的 Xiaomi Home ,进入集成详情页 > DOWNLOAD 一键从 HACS 安装米家集成:
> 米家集成暂未添加到 HACS 商店,敬请期待。 [![打开您的 Home Assistant 实例并打开 Home Assistant 社区商店内的米家集成。](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
或者HACS > 在搜索框中输入 **Xiaomi Home** > 点击 **Xiaomi Home** ,进入集成详情页 > DOWNLOAD
### 方法 3通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装 ### 方法 3通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装
@ -47,7 +49,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
[设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录 [设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home) [![打开您的 Home Assistant 实例并开始配置一个新的米家集成实例。](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
### 添加 MIoT 设备 ### 添加 MIoT 设备
@ -59,7 +61,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录 方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) [![打开您的 Home Assistant 实例并显示米家集成。](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
### 修改配置项 ### 修改配置项
@ -353,7 +355,7 @@ instance code 为 MIoT-Spec-V2 实例代码,格式如下:
``` ```
service:<siid> # 服务 service:<siid> # 服务
service:<siid>:property:<piid> # 属性 service:<siid>:property:<piid> # 属性
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的值 service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引
service:<siid>:event:<eiid> # 事件 service:<siid>:event:<eiid> # 事件
service:<siid>:action:<aiid> # 方法 service:<siid>:action:<aiid> # 方法
``` ```
@ -376,7 +378,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) > 配置 > 更新实体转换规则
## 文档 ## 文档

View File

@ -15,14 +15,15 @@ TRANS_RELATIVE_PATH: str = path.join(
MIOT_I18N_RELATIVE_PATH: str = path.join( MIOT_I18N_RELATIVE_PATH: str = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n') ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
SPEC_BOOL_TRANS_FILE = path.join( SPEC_BOOL_TRANS_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
SPEC_FILTER_FILE = path.join( SPEC_FILTER_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml') SPEC_MULTI_LANG_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/multi_lang.json')
SPEC_ADD_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_add.json')
SPEC_MODIFY_FILE = path.join( SPEC_MODIFY_FILE = path.join(
ROOT_PATH, ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
'../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
def load_json_file(file_path: str) -> Optional[dict]: def load_json_file(file_path: str) -> Optional[dict]:
@ -30,7 +31,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file: with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file) return json.load(file)
except FileNotFoundError: except FileNotFoundError:
_LOGGER.info('%s is not found.', file_path,) _LOGGER.info('%s is not found.', file_path)
return None return None
except json.JSONDecodeError: except json.JSONDecodeError:
_LOGGER.info('%s is not a valid JSON file.', file_path) _LOGGER.info('%s is not a valid JSON file.', file_path)
@ -39,7 +40,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
def save_json_file(file_path: str, data: dict) -> None: def save_json_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file: with open(file_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4) json.dump(data, file, ensure_ascii=False, indent=2)
def load_yaml_file(file_path: str) -> Optional[dict]: def load_yaml_file(file_path: str) -> Optional[dict]:
@ -56,9 +57,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
def save_yaml_file(file_path: str, data: dict) -> None: def save_yaml_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file: with open(file_path, 'w', encoding='utf-8') as file:
yaml.safe_dump( yaml.safe_dump(data,
data, file, default_flow_style=False, file,
allow_unicode=True, indent=2, sort_keys=False) default_flow_style=False,
allow_unicode=True,
indent=2,
sort_keys=False)
def dict_str_str(d: dict) -> bool: def dict_str_str(d: dict) -> bool:
@ -132,9 +136,116 @@ def bool_trans(d: dict) -> bool:
for key, trans in d['translate'].items(): for key, trans in d['translate'].items():
trans_keys: set[str] = set(trans.keys()) trans_keys: set[str] = set(trans.keys())
if set(trans.keys()) != default_keys: if set(trans.keys()) != default_keys:
_LOGGER.info( _LOGGER.info('bool trans inconsistent, %s, %s, %s', key,
'bool trans inconsistent, %s, %s, %s', default_keys, trans_keys)
key, default_keys, trans_keys) return False
return True
def multi_lang(data: dict) -> bool:
"""dict[str, dict[str, dict[str, str]]]"""
for key in data.keys():
if key.count(':') != 5:
return False
return nested_3_dict_str_str(data)
def spec_add(data: dict) -> bool:
"""dict[str, list[dict[str, int| str | list]]]"""
if not isinstance(data, dict):
return False
for urn, content in data.items():
if not isinstance(urn, str) or not isinstance(content, (list, str)):
return False
if isinstance(content, str):
continue
for service in content:
if ('iid' not in service) or ('type' not in service) or (
'description'
not in service) or (('properties' not in service) and
('actions' not in service) and
('events' not in service)):
return False
type_strs: list[str] = service['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
if 'properties' in service:
if not isinstance(service['properties'], list):
return False
for prop in service['properties']:
if ('iid' not in prop) or ('type' not in prop) or (
'description' not in prop) or (
'format' not in prop) or ('access' not in prop):
return False
if not isinstance(prop['iid'], int) or not isinstance(
prop['type'], str) or not isinstance(
prop['description'], str) or not isinstance(
prop['format'], str) or not isinstance(
prop['access'], list):
return False
type_strs = prop['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for access in prop['access']:
if access not in ['read', 'write', 'notify']:
return False
if 'value-range' in prop:
if not isinstance(prop['value-range'], list):
return False
for value in prop['value-range']:
if not isinstance(value, (int, float)):
return False
if 'value-list' in prop:
if not isinstance(prop['value-list'], list):
return False
for item in prop['value-list']:
if 'value' not in item or 'description' not in item:
return False
if not isinstance(item['value'],
int) or not isinstance(
item['description'], str):
return False
if 'actions' in service:
if not isinstance(service['actions'], list):
return False
for action in service['actions']:
if ('iid' not in action) or ('type' not in action) or (
'description' not in action) or (
'in' not in action) or ('out' not in action):
return False
if not isinstance(action['iid'], int) or not isinstance(
action['type'], str) or not isinstance(
action['description'], str) or not isinstance(
action['in'], list) or not isinstance(
action['out'], list):
return False
type_strs = action['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in action['in']:
if not isinstance(param, int):
return False
for param in action['out']:
if not isinstance(param, int):
return False
if 'events' in service:
if not isinstance(service['events'], list):
return False
for event in service['events']:
if ('iid' not in event) or ('type' not in event) or (
'description' not in event) or ('arguments'
not in event):
return False
if not isinstance(event['iid'], int) or not isinstance(
event['type'], str) or not isinstance(
event['description'], str) or not isinstance(
event['arguments'], list):
return False
type_strs = event['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in event['arguments']:
if not isinstance(param, int):
return False return False
return True return True
@ -159,25 +270,22 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
_LOGGER.info('invalid type') _LOGGER.info('invalid type')
return False return False
if dict1.keys() != dict2.keys(): if dict1.keys() != dict2.keys():
_LOGGER.info( _LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys()) dict2.keys())
return False return False
for key in dict1: for key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
if not compare_dict_structure(dict1[key], dict2[key]): if not compare_dict_structure(dict1[key], dict2[key]):
_LOGGER.info( _LOGGER.info('inconsistent key values, dict, %s', key)
'inconsistent key values, dict, %s', key)
return False return False
elif isinstance(dict1[key], list) and isinstance(dict2[key], list): elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if not all( if not all(
isinstance(i, type(j)) isinstance(i, type(j))
for i, j in zip(dict1[key], dict2[key])): for i, j in zip(dict1[key], dict2[key])):
_LOGGER.info( _LOGGER.info('inconsistent key values, list, %s', key)
'inconsistent key values, list, %s', key)
return False return False
elif not isinstance(dict1[key], type(dict2[key])): elif not isinstance(dict1[key], type(dict2[key])):
_LOGGER.info( _LOGGER.info('inconsistent key values, type, %s', key)
'inconsistent key values, type, %s', key)
return False return False
return True return True
@ -200,6 +308,16 @@ def sort_spec_filter(file_path: str):
return filter_data return filter_data
def sort_spec_add(file_path: str):
filter_data = load_json_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
return dict(sorted(filter_data.items()))
def sort_multi_lang(file_path: str):
return sort_spec_add(file_path)
def sort_spec_modify(file_path: str): def sort_spec_modify(file_path: str):
filter_data = load_yaml_file(file_path=file_path) filter_data = load_yaml_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error' assert isinstance(filter_data, dict), f'{file_path} format error'
@ -222,6 +340,22 @@ def test_spec_filter():
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error' assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_multi_lang():
data = load_json_file(SPEC_MULTI_LANG_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_MULTI_LANG_FILE} failed'
assert multi_lang(data), f'{SPEC_MULTI_LANG_FILE} format error'
@pytest.mark.github
def test_spec_add():
data = load_json_file(SPEC_ADD_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_ADD_FILE} failed'
assert spec_add(data), f'{SPEC_ADD_FILE} format error'
@pytest.mark.github @pytest.mark.github
def test_spec_modify(): def test_spec_modify():
data = load_yaml_file(SPEC_MODIFY_FILE) data = load_yaml_file(SPEC_MODIFY_FILE)
@ -255,7 +389,8 @@ def test_miot_lang_integrity():
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from miot.const import INTEGRATION_LANGUAGES from miot.const import INTEGRATION_LANGUAGES
integration_lang_list: list[str] = [ integration_lang_list: list[str] = [
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())] f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())
]
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH)) translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
assert len(translations_names) == len(integration_lang_list) assert len(translations_names) == len(integration_lang_list)
assert translations_names == set(integration_lang_list) assert translations_names == set(integration_lang_list)
@ -271,21 +406,18 @@ def test_miot_lang_integrity():
default_dict = load_json_file( default_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0])) path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]: for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file( compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))
path.join(TRANS_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info( _LOGGER.info('compare_dict_structure failed /translations, %s',
'compare_dict_structure failed /translations, %s', name) name)
assert False assert False
# Check i18n files structure # Check i18n files structure
default_dict = load_json_file( default_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0])) path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]: for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file( compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))
path.join(MIOT_I18N_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info( _LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)
'compare_dict_structure failed /miot/i18n, %s', name)
assert False assert False
@ -303,12 +435,27 @@ def test_miot_data_sort():
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path' f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
' and run the following command sorting, ', ' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py') 'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps( assert json.dumps(load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
sort_spec_filter(file_path=SPEC_FILTER_FILE)), ( sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
f'{SPEC_FILTER_FILE} not sorted, goto project root path' f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ', ' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py') 'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(
load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(
sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (
f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(
sort_spec_add(file_path=SPEC_ADD_FILE)), (
f'{SPEC_ADD_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_yaml_file(file_path=SPEC_MODIFY_FILE)) == json.dumps(
sort_spec_modify(file_path=SPEC_MODIFY_FILE)), (
f'{SPEC_MODIFY_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
@pytest.mark.update @pytest.mark.update
@ -319,6 +466,11 @@ def test_sort_spec_data():
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE) _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
sort_data = sort_spec_add(file_path=SPEC_ADD_FILE)
save_json_file(file_path=SPEC_ADD_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_ADD_FILE)
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE) sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data) save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE) _LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)