mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-07-06 11:49:05 +08:00
Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
d659d13e49 | |||
3402587b1c | |||
028399c0b1 | |||
5179e97e38 | |||
9fdbf3dff2 | |||
d0508ead25 | |||
d05bdcbba9 | |||
a4f9c29b6b | |||
62dd32a132 | |||
1bd338639b | |||
6a2534934c | |||
d06a564917 | |||
23cc1130fe | |||
a83ad60b38 | |||
00f24bd3e1 | |||
f384034854 | |||
b0204ad9b7 | |||
b4ece958ac | |||
db77af8a13 | |||
a9f1fc630d | |||
51a27a1e30 | |||
2e0ea642a4 | |||
80d962897a | |||
d17784070d | |||
218c96e5e6 | |||
eacc0d02da | |||
23f0a2d360 | |||
3abccc2491 | |||
7a459de766 | |||
2f619ff51d | |||
cb34b6ce46 | |||
d0a7940c59 | |||
899d616da4 | |||
c6be6be1ec | |||
77b0a4531b | |||
7d9250914c | |||
a09289ef90 | |||
b0428dc95a | |||
19ed04f2f5 | |||
e174a73f52 | |||
a1aa1c024f | |||
372e635681 | |||
3759aa9a1b | |||
60d054cf19 | |||
6680d9e8cb | |||
0ef8cb6370 | |||
8f0a69c611 | |||
8be0fa5d61 | |||
07cb4ed193 | |||
5c46504d0e | |||
97d89b3a04 | |||
4482d257dc | |||
d0387be15b | |||
27cf1085bd | |||
e69448f2eb | |||
7901607648 | |||
5adcb7ce00 | |||
672e5b3f5d | |||
417af787c4 | |||
6f058bf392 | |||
52485d8c7a | |||
48554ec0f7 | |||
0ce94f7316 | |||
20b0004746 | |||
57422ddf0d | |||
2e60962e94 | |||
52fd6371ab | |||
8778b00c3a | |||
3c16f0ffbb | |||
3a5b641ec7 | |||
8fb6f9065e |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,7 +1,7 @@
|
||||
name: Bug Report / 报告问题
|
||||
description: Create a report to help us improve. / 报告问题以帮助我们改进
|
||||
body:
|
||||
- type: input
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug / 描述问题
|
||||
description: |
|
||||
|
123
CHANGELOG.md
123
CHANGELOG.md
@ -1,4 +1,121 @@
|
||||
# CHANGELOG
|
||||
## v0.3.3
|
||||
### Changed
|
||||
- Change the log level of error "mips unsub internal error, 4, None". [#1135](https://github.com/XiaoMi/ha_xiaomi_home/pull/1135)
|
||||
- Add necessary logs for distinguishing the set_properties command source. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)
|
||||
|
||||
### Fixed
|
||||
- Fix tofan.airrtc.wk01 thermostat and air conditioner service. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)
|
||||
- Fix mrbond.airer.m1t closing status. [#1134](https://github.com/XiaoMi/ha_xiaomi_home/pull/1134)
|
||||
- Fix the MIoT-Spec-V2 of xiaomi.fan.p69 fan service, ainice.sensor_occupy.3b people number, cykj.hood.jyj22 ventilation switch status, xiaomi.fan.p43 fan level, zhimi.airp.ua1a pm10 density, 090615.switch.x1tpm switch status, dmaker.fan.p33 fan-level. [#1132](https://github.com/XiaoMi/ha_xiaomi_home/pull/1132)
|
||||
- Fix cubee.airrtc.th123e and cubee.airrtc.th123w MIoT-Spec-V2 instance descriptions in Russian.
|
||||
- Fix ijai.vacuum.v1 suction-state value-list descriptions in Chinese.
|
||||
- Fix the misuse of Chinese brackets in multi_lang.json.
|
||||
- The unit of the humidity-range property of xiaomi.aircondition.mt0, xiaomi.aircondition.c35, xiaomi.aircondition.c24 and xiaomi.aircondition.c20 is "none". [#1187](https://github.com/XiaoMi/ha_xiaomi_home/pull/1187)
|
||||
|
||||
## 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
|
||||
### Added
|
||||
@ -91,10 +208,10 @@ This version will cause some Xiaomi routers that do not support access (#564) to
|
||||
### Changed
|
||||
### Fixed
|
||||
- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59
|
||||
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
|
||||
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
|
||||
- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16
|
||||
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
|
||||
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
|
||||
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
|
||||
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
|
||||
|
||||
## v0.1.0
|
||||
### Added
|
||||
|
@ -98,6 +98,9 @@ footer: Optional. The footer is the place to reference GitHub issues and PRs tha
|
||||
|
||||
When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
|
||||
|
||||
|
||||
When you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.
|
||||
|
||||
## How to Get Help
|
||||
|
||||
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.
|
||||
|
12
README.md
12
README.md
@ -33,9 +33,11 @@ git checkout v1.0.0
|
||||
|
||||
### 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.
|
||||
[](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)
|
||||
|
||||
@ -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
|
||||
|
||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
|
||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
|
||||
|
||||
### 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
|
||||
|
||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
|
||||
### Update Configurations
|
||||
|
||||
@ -374,7 +376,7 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
> If you edit `specv2entity.py`, `spec_filter.json` or `multi_lang.json` in the `custom_components/xiaomi_home/miot/specs` directory in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update Entity Conversion Rule
|
||||
> If you edit any files in the `custom_components/xiaomi_home/miot/specs` directory (`spec_filter.py`, `spec_modify.json`, `multi_lang.json`, etc.) in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update entity conversion rules
|
||||
|
||||
## Documents
|
||||
|
||||
|
@ -156,7 +156,8 @@ async def async_setup_entry(
|
||||
device.entity_list[platform].remove(entity)
|
||||
entity_id = device.gen_service_entity_id(
|
||||
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):
|
||||
er.async_remove(entity_id=entity_id)
|
||||
if platform in device.prop_list:
|
||||
|
@ -89,4 +89,8 @@ class BinarySensor(MIoTPropertyEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -565,27 +565,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
home_list = {}
|
||||
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
||||
# home list
|
||||
for home_id, home_info in self._cc_home_info[
|
||||
'homes']['home_list'].items():
|
||||
# i18n
|
||||
tip_central = ''
|
||||
group_id = home_info.get('group_id', None)
|
||||
dev_list = {
|
||||
device['did']: device
|
||||
for device in list(self._cc_home_info['devices'].values())
|
||||
if device.get('home_id', None) == home_id}
|
||||
if (
|
||||
mips_list
|
||||
and group_id in mips_list
|
||||
and mips_list[group_id].get('did', None) in dev_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[
|
||||
'homes'][device_source].items():
|
||||
# i18n
|
||||
tip_central = self._miot_i18n.translate(
|
||||
key='config.other.found_central_gateway')
|
||||
home_info['central_did'] = mips_list[group_id].get('did', None)
|
||||
home_list[home_id] = (
|
||||
f'{home_info["home_name"]} '
|
||||
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
|
||||
tip_central = ''
|
||||
group_id = home_info.get('group_id', None)
|
||||
dev_list = {
|
||||
device['did']: device
|
||||
for device in list(self._cc_home_info['devices'].values())
|
||||
if device.get('home_id', None) == home_id}
|
||||
if (
|
||||
mips_list
|
||||
and group_id in mips_list
|
||||
and mips_list[group_id].get('did', None) in dev_list
|
||||
):
|
||||
# i18n
|
||||
tip_central = self._miot_i18n.translate(
|
||||
key='config.other.found_central_gateway')
|
||||
home_info['central_did'] = mips_list[group_id].get(
|
||||
'did', None)
|
||||
home_list[home_id] = (
|
||||
f'{home_info["home_name"]} '
|
||||
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
|
||||
|
||||
self._cc_home_list_show = dict(sorted(home_list.items()))
|
||||
|
||||
@ -660,10 +665,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if not home_selected:
|
||||
return await self.__show_homes_select_form(
|
||||
'no_family_selected')
|
||||
for home_id, home_info in self._cc_home_info[
|
||||
'homes']['home_list'].items():
|
||||
if home_id in home_selected:
|
||||
self._home_selected[home_id] = home_info
|
||||
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[
|
||||
'homes'][device_source].items():
|
||||
if home_id in home_selected:
|
||||
self._home_selected[home_id] = home_info
|
||||
self._area_name_rule = user_input.get(
|
||||
'area_name_rule', self._area_name_rule)
|
||||
# Storage device list
|
||||
@ -1420,27 +1429,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
home_list = {}
|
||||
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
||||
# home list
|
||||
for home_id, home_info in self._cc_home_info[
|
||||
'homes']['home_list'].items():
|
||||
# i18n
|
||||
tip_central = ''
|
||||
group_id = home_info.get('group_id', None)
|
||||
did_list = {
|
||||
device['did']: device for device in list(
|
||||
self._cc_home_info['devices'].values())
|
||||
if device.get('home_id', None) == home_id}
|
||||
if (
|
||||
group_id in mips_list
|
||||
and mips_list[group_id].get('did', None) in did_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[
|
||||
'homes'][device_source].items():
|
||||
# i18n
|
||||
tip_central = self._miot_i18n.translate(
|
||||
key='config.other.found_central_gateway')
|
||||
home_info['central_did'] = mips_list[group_id].get(
|
||||
'did', None)
|
||||
home_list[home_id] = (
|
||||
f'{home_info["home_name"]} '
|
||||
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
|
||||
tip_central = ''
|
||||
group_id = home_info.get('group_id', None)
|
||||
did_list = {
|
||||
device['did']: device for device in list(
|
||||
self._cc_home_info['devices'].values())
|
||||
if device.get('home_id', None) == home_id}
|
||||
if (
|
||||
group_id in mips_list
|
||||
and mips_list[group_id].get('did', None) in did_list
|
||||
):
|
||||
# i18n
|
||||
tip_central = self._miot_i18n.translate(
|
||||
key='config.other.found_central_gateway')
|
||||
home_info['central_did'] = mips_list[group_id].get(
|
||||
'did', None)
|
||||
home_list[home_id] = (
|
||||
f'{home_info["home_name"]} '
|
||||
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
|
||||
# Remove deleted item
|
||||
self._home_selected_list = [
|
||||
home_id for home_id in self._home_selected_list
|
||||
@ -1460,10 +1473,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
return await self.__show_homes_select_form('no_family_selected')
|
||||
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
|
||||
self._home_selected = {}
|
||||
for home_id, home_info in self._cc_home_info[
|
||||
'homes']['home_list'].items():
|
||||
if home_id in self._home_selected_list:
|
||||
self._home_selected[home_id] = home_info
|
||||
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[
|
||||
'homes'][device_source].items():
|
||||
if home_id in self._home_selected_list:
|
||||
self._home_selected[home_id] = home_info
|
||||
# Get device list
|
||||
device_list: dict = {
|
||||
did: dev_info
|
||||
|
@ -47,30 +47,24 @@ Cover entities for Xiaomi Home.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
CoverDeviceClass
|
||||
)
|
||||
from homeassistant.components.cover import (ATTR_POSITION, CoverEntity,
|
||||
CoverEntityFeature,
|
||||
CoverDeviceClass)
|
||||
|
||||
from .miot.miot_spec import MIoTSpecProperty
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback) -> None:
|
||||
"""Set up a config entry."""
|
||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||
config_entry.entry_id]
|
||||
@ -82,8 +76,12 @@ async def async_setup_entry(
|
||||
data.spec.device_class = CoverDeviceClass.CURTAIN
|
||||
elif data.spec.name == 'window-opener':
|
||||
data.spec.device_class = CoverDeviceClass.WINDOW
|
||||
new_entities.append(
|
||||
Cover(miot_device=miot_device, entity_data=data))
|
||||
elif data.spec.name == 'motor-controller':
|
||||
data.spec.device_class = CoverDeviceClass.SHUTTER
|
||||
elif data.spec.name == 'airer':
|
||||
data.spec.device_class = CoverDeviceClass.BLIND
|
||||
new_entities.append(Cover(miot_device=miot_device,
|
||||
entity_data=data))
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
@ -97,18 +95,20 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
||||
_prop_motor_value_close: Optional[int]
|
||||
_prop_motor_value_pause: Optional[int]
|
||||
_prop_status: Optional[MIoTSpecProperty]
|
||||
_prop_status_opening: Optional[int]
|
||||
_prop_status_closing: Optional[int]
|
||||
_prop_status_stop: Optional[int]
|
||||
_prop_status_opening: Optional[list[int]]
|
||||
_prop_status_closing: Optional[list[int]]
|
||||
_prop_status_stop: Optional[list[int]]
|
||||
_prop_status_closed: Optional[list[int]]
|
||||
_prop_current_position: Optional[MIoTSpecProperty]
|
||||
_prop_target_position: Optional[MIoTSpecProperty]
|
||||
_prop_position_value_min: Optional[int]
|
||||
_prop_position_value_max: Optional[int]
|
||||
_prop_position_value_range: Optional[int]
|
||||
_prop_pos_closing: bool
|
||||
_prop_pos_opening: bool
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
) -> None:
|
||||
def __init__(self, miot_device: MIoTDevice,
|
||||
entity_data: MIoTEntityData) -> None:
|
||||
"""Initialize the Cover."""
|
||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||
self._attr_device_class = entity_data.spec.device_class
|
||||
@ -120,50 +120,64 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
||||
self._prop_motor_value_close = None
|
||||
self._prop_motor_value_pause = None
|
||||
self._prop_status = None
|
||||
self._prop_status_opening = None
|
||||
self._prop_status_closing = None
|
||||
self._prop_status_stop = None
|
||||
self._prop_status_opening = []
|
||||
self._prop_status_closing = []
|
||||
self._prop_status_stop = []
|
||||
self._prop_status_closed = []
|
||||
self._prop_current_position = None
|
||||
self._prop_target_position = None
|
||||
self._prop_position_value_min = None
|
||||
self._prop_position_value_max = None
|
||||
self._prop_position_value_range = None
|
||||
self._prop_pos_closing = False
|
||||
self._prop_pos_opening = False
|
||||
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
if prop.name == 'motor-control':
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'motor-control value_list is None, %s', self.entity_id)
|
||||
_LOGGER.error('motor-control value_list is None, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'open'}:
|
||||
if item.name in {'open', 'up'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN)
|
||||
self._prop_motor_value_open = item.value
|
||||
elif item.name in {'close'}:
|
||||
elif item.name in {'close', 'down'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.CLOSE)
|
||||
self._prop_motor_value_close = item.value
|
||||
elif item.name in {'pause'}:
|
||||
elif item.name in {'pause', 'stop'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.STOP)
|
||||
self._prop_motor_value_pause = item.value
|
||||
self._prop_motor_control = prop
|
||||
elif prop.name == 'status':
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'status value_list is None, %s', self.entity_id)
|
||||
_LOGGER.error('status value_list is None, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'opening', 'open'}:
|
||||
self._prop_status_opening = item.value
|
||||
elif item.name in {'closing', 'close'}:
|
||||
self._prop_status_closing = item.value
|
||||
elif item.name in {'stop', 'pause'}:
|
||||
self._prop_status_stop = item.value
|
||||
if item.name in {'opening', 'open', 'up'}:
|
||||
self._prop_status_opening.append(item.value)
|
||||
elif item.name in {'closing', 'close', 'down', 'dowm'}:
|
||||
self._prop_status_closing.append(item.value)
|
||||
elif item.name in {'stop', 'stopped', 'pause'}:
|
||||
self._prop_status_stop.append(item.value)
|
||||
elif item.name in {'closed'}:
|
||||
self._prop_status_closed.append(item.value)
|
||||
self._prop_status = prop
|
||||
elif prop.name == 'current-position':
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid current-position value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._prop_position_value_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
|
||||
elif prop.name == 'target-position':
|
||||
if not prop.value_range:
|
||||
@ -173,35 +187,65 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
||||
continue
|
||||
self._prop_position_value_min = prop.value_range.min_
|
||||
self._prop_position_value_max = prop.value_range.max_
|
||||
self._prop_position_value_range = (
|
||||
self._prop_position_value_max -
|
||||
self._prop_position_value_min)
|
||||
self._prop_position_value_range = (prop.value_range.max_ -
|
||||
prop.value_range.min_)
|
||||
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
|
||||
self._prop_target_position = prop
|
||||
# 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:
|
||||
"""Open the cover."""
|
||||
await self.set_property_async(
|
||||
self._prop_motor_control, self._prop_motor_value_open)
|
||||
current = None if (self._prop_current_position
|
||||
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:
|
||||
"""Close the cover."""
|
||||
await self.set_property_async(
|
||||
self._prop_motor_control, self._prop_motor_value_close)
|
||||
current = None if (self._prop_current_position
|
||||
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:
|
||||
"""Stop the cover."""
|
||||
await self.set_property_async(
|
||||
self._prop_motor_control, self._prop_motor_value_pause)
|
||||
self._prop_pos_opening = False
|
||||
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:
|
||||
"""Set the position of the cover."""
|
||||
pos = kwargs.get(ATTR_POSITION, None)
|
||||
if pos is None:
|
||||
return None
|
||||
pos = round(pos*self._prop_position_value_range/100)
|
||||
return await self.set_property_async(
|
||||
prop=self._prop_target_position, value=pos)
|
||||
current = self.current_cover_position
|
||||
if current is not None:
|
||||
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
|
||||
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.
|
||||
"""
|
||||
if self._prop_current_position is None:
|
||||
# Assume that the current position is the same as the target
|
||||
# position when the current position is not defined in the device's
|
||||
# MIoT-Spec-V2.
|
||||
if self._prop_target_position is None:
|
||||
return None
|
||||
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)
|
||||
if pos is None:
|
||||
return None
|
||||
return round(pos*100/self._prop_position_value_range)
|
||||
return None if pos is None else round(pos * 100 /
|
||||
self._prop_position_value_range)
|
||||
|
||||
@property
|
||||
def is_opening(self) -> Optional[bool]:
|
||||
"""Return if the cover is opening."""
|
||||
if self._prop_status is None:
|
||||
return None
|
||||
return self.get_prop_value(
|
||||
prop=self._prop_status) == self._prop_status_opening
|
||||
if self._prop_status and self._prop_status_opening:
|
||||
return (self.get_prop_value(prop=self._prop_status)
|
||||
in self._prop_status_opening)
|
||||
# The status has higher priority when determining whether the cover
|
||||
# is opening.
|
||||
return self._prop_pos_opening
|
||||
|
||||
@property
|
||||
def is_closing(self) -> Optional[bool]:
|
||||
"""Return if the cover is closing."""
|
||||
if self._prop_status is None:
|
||||
return None
|
||||
return self.get_prop_value(
|
||||
prop=self._prop_status) == self._prop_status_closing
|
||||
if self._prop_status and self._prop_status_closing:
|
||||
return (self.get_prop_value(prop=self._prop_status)
|
||||
in self._prop_status_closing)
|
||||
# The status has higher priority when determining whether the cover
|
||||
# is closing.
|
||||
return self._prop_pos_closing
|
||||
|
||||
@property
|
||||
def is_closed(self) -> Optional[bool]:
|
||||
"""Return if the cover is closed."""
|
||||
return self.get_prop_value(prop=self._prop_current_position) == 0
|
||||
if self.current_cover_position is not None:
|
||||
return self.current_cover_position == 0
|
||||
# The current position is prior to the status when determining
|
||||
# whether the cover is closed.
|
||||
if self._prop_status and self._prop_status_closed:
|
||||
return (self.get_prop_value(prop=self._prop_status)
|
||||
in self._prop_status_closed)
|
||||
return None
|
||||
|
@ -46,6 +46,7 @@ off Xiaomi or its affiliates' products.
|
||||
Event entities for Xiaomi Home.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
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.const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -89,4 +92,5 @@ class Event(MIoTEventEntity, EventEntity):
|
||||
self, name: str, arguments: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""An event is occurred."""
|
||||
_LOGGER.debug('%s, attributes: %s', name, str(arguments))
|
||||
self._trigger_event(event_type=name, event_attributes=arguments)
|
||||
|
@ -52,7 +52,12 @@ import logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity,
|
||||
FanEntityFeature,
|
||||
DIRECTION_FORWARD,
|
||||
DIRECTION_REVERSE
|
||||
)
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
@ -167,20 +172,21 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||
self._prop_horizontal_swing = prop
|
||||
elif prop.name == 'wind-reverse':
|
||||
if prop.format_ == 'bool':
|
||||
if prop.format_ == bool:
|
||||
self._prop_wind_reverse_forward = False
|
||||
self._prop_wind_reverse_reverse = True
|
||||
elif prop.value_list:
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'foreward'}:
|
||||
if item.name in {'foreward', 'forward'}:
|
||||
self._prop_wind_reverse_forward = item.value
|
||||
elif item.name in {'reversal', 'reverse'}:
|
||||
self._prop_wind_reverse_reverse = item.value
|
||||
if (
|
||||
self._prop_wind_reverse_forward is None
|
||||
or self._prop_wind_reverse_reverse is None
|
||||
):
|
||||
# NOTICE: Value may be 0 or False
|
||||
_LOGGER.info(
|
||||
_LOGGER.error(
|
||||
'invalid wind-reverse, %s', self.entity_id)
|
||||
continue
|
||||
self._attr_supported_features |= FanEntityFeature.DIRECTION
|
||||
@ -202,9 +208,9 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
if self._speed_names:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=self.get_map_value(
|
||||
value=self.get_map_key(
|
||||
map_=self._speed_name_map,
|
||||
key=percentage_to_ordered_list_item(
|
||||
value=percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)))
|
||||
else:
|
||||
await self.set_property_async(
|
||||
@ -230,12 +236,15 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan speed."""
|
||||
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:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=self.get_map_value(
|
||||
value=self.get_map_key(
|
||||
map_=self._speed_name_map,
|
||||
key=percentage_to_ordered_list_item(
|
||||
value=percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)))
|
||||
else:
|
||||
await self.set_property_async(
|
||||
@ -243,9 +252,6 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
value=int(percentage_to_ranged_value(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
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:
|
||||
await self.set_property_async(prop=self._prop_on, value=False)
|
||||
|
||||
@ -264,7 +270,7 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
prop=self._prop_wind_reverse,
|
||||
value=(
|
||||
self._prop_wind_reverse_reverse
|
||||
if self.current_direction == 'reverse'
|
||||
if direction == DIRECTION_REVERSE
|
||||
else self._prop_wind_reverse_forward))
|
||||
|
||||
async def async_oscillate(self, oscillating: bool) -> None:
|
||||
@ -293,9 +299,9 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Return the current direction of the fan."""
|
||||
if not self._prop_wind_reverse:
|
||||
return None
|
||||
return 'reverse' if self.get_prop_value(
|
||||
return DIRECTION_REVERSE if self.get_prop_value(
|
||||
prop=self._prop_wind_reverse
|
||||
) == self._prop_wind_reverse_reverse else 'forward'
|
||||
) == self._prop_wind_reverse_reverse else DIRECTION_FORWARD
|
||||
|
||||
@property
|
||||
def percentage(self) -> Optional[int]:
|
||||
@ -303,7 +309,7 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
||||
if fan_level is None:
|
||||
return None
|
||||
if self._speed_names:
|
||||
if self._speed_names and self._speed_name_map:
|
||||
return ordered_list_item_to_percentage(
|
||||
self._speed_names, self._speed_name_map[fan_level])
|
||||
else:
|
||||
|
@ -52,23 +52,22 @@ from typing import Any, Optional
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.humidifier import (
|
||||
HumidifierEntity,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntityFeature
|
||||
)
|
||||
from homeassistant.components.humidifier import (HumidifierEntity,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntityFeature,
|
||||
HumidifierAction)
|
||||
|
||||
from .miot.miot_spec import MIoTSpecProperty
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||
@ -82,8 +81,8 @@ async def async_setup_entry(
|
||||
Humidifier(miot_device=miot_device, entity_data=data))
|
||||
for data in miot_device.entity_list.get('dehumidifier', []):
|
||||
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
|
||||
new_entities.append(Humidifier(
|
||||
miot_device=miot_device, entity_data=data))
|
||||
new_entities.append(
|
||||
Humidifier(miot_device=miot_device, entity_data=data))
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
@ -99,9 +98,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
|
||||
_mode_map: dict[Any, Any]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
) -> None:
|
||||
def __init__(self, miot_device: MIoTDevice,
|
||||
entity_data: MIoTEntityData) -> None:
|
||||
"""Initialize the Humidifier."""
|
||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||
self._attr_device_class = entity_data.device_class
|
||||
@ -130,12 +128,10 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
# mode
|
||||
elif prop.name == 'mode':
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'mode value_list is None, %s', self.entity_id)
|
||||
_LOGGER.error('mode value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_available_modes = list(
|
||||
self._mode_map.values())
|
||||
self._attr_available_modes = list(self._mode_map.values())
|
||||
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||
self._prop_mode = prop
|
||||
# relative-humidity
|
||||
@ -152,33 +148,45 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
await self.set_property_async(
|
||||
prop=self._prop_target_humidity, value=humidity)
|
||||
if self._prop_target_humidity is None:
|
||||
return
|
||||
await self.set_property_async(prop=self._prop_target_humidity,
|
||||
value=humidity)
|
||||
|
||||
async def async_set_mode(self, mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
await self.set_property_async(
|
||||
prop=self._prop_mode,
|
||||
value=self.get_map_key(map_=self._mode_map, value=mode))
|
||||
await self.set_property_async(prop=self._prop_mode,
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map, value=mode))
|
||||
|
||||
@property
|
||||
def is_on(self) -> Optional[bool]:
|
||||
"""Return if the humidifier is 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
|
||||
def current_humidity(self) -> Optional[int]:
|
||||
"""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
|
||||
def target_humidity(self) -> Optional[int]:
|
||||
"""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
|
||||
def mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode."""
|
||||
return self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
return self.get_map_value(map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
@ -96,7 +96,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
"""Light entities for Xiaomi Home."""
|
||||
# pylint: disable=unused-argument
|
||||
_VALUE_RANGE_MODE_COUNT_MAX = 30
|
||||
_prop_on: MIoTSpecProperty
|
||||
_prop_on: Optional[MIoTSpecProperty]
|
||||
_prop_brightness: Optional[MIoTSpecProperty]
|
||||
_prop_color_temp: Optional[MIoTSpecProperty]
|
||||
_prop_color: Optional[MIoTSpecProperty]
|
||||
@ -179,7 +179,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
) / prop.value_range.step)
|
||||
> self._VALUE_RANGE_MODE_COUNT_MAX
|
||||
):
|
||||
_LOGGER.info(
|
||||
_LOGGER.error(
|
||||
'too many mode values, %s, %s, %s',
|
||||
self.entity_id, prop.name, prop.value_range)
|
||||
else:
|
||||
@ -250,23 +250,25 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
|
||||
Shall set attributes in kwargs if applicable.
|
||||
"""
|
||||
result: bool = False
|
||||
# on
|
||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||
value_on = True if self._prop_on.format_ == bool else 1
|
||||
result = await self.set_property_async(
|
||||
prop=self._prop_on, value=value_on)
|
||||
if self._prop_on:
|
||||
value_on = True if self._prop_on.format_ == bool else 1
|
||||
await self.set_property_async(
|
||||
prop=self._prop_on, value=value_on)
|
||||
# brightness
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = brightness_to_value(
|
||||
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
|
||||
result = await self.set_property_async(
|
||||
prop=self._prop_brightness, value=brightness)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_brightness, value=brightness,
|
||||
write_ha_state=False)
|
||||
# color-temperature
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
result = await self.set_property_async(
|
||||
await self.set_property_async(
|
||||
prop=self._prop_color_temp,
|
||||
value=kwargs[ATTR_COLOR_TEMP_KELVIN])
|
||||
value=kwargs[ATTR_COLOR_TEMP_KELVIN],
|
||||
write_ha_state=False)
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
# rgb color
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
@ -274,19 +276,23 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
g = kwargs[ATTR_RGB_COLOR][1]
|
||||
b = kwargs[ATTR_RGB_COLOR][2]
|
||||
rgb = (r << 16) | (g << 8) | b
|
||||
result = await self.set_property_async(
|
||||
prop=self._prop_color, value=rgb)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_color, value=rgb,
|
||||
write_ha_state=False)
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
# mode
|
||||
if ATTR_EFFECT in kwargs:
|
||||
result = await self.set_property_async(
|
||||
await self.set_property_async(
|
||||
prop=self._prop_mode,
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
|
||||
return result
|
||||
map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
|
||||
write_ha_state=False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the light off."""
|
||||
if not self._prop_on:
|
||||
return
|
||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||
value_on = False if self._prop_on.format_ == bool else 0
|
||||
return await self.set_property_async(prop=self._prop_on, value=value_on)
|
||||
await self.set_property_async(prop=self._prop_on, value=value_on)
|
||||
|
@ -20,13 +20,13 @@
|
||||
],
|
||||
"requirements": [
|
||||
"construct>=2.10.56",
|
||||
"paho-mqtt<2.0.0",
|
||||
"paho-mqtt",
|
||||
"numpy",
|
||||
"cryptography",
|
||||
"psutil"
|
||||
],
|
||||
"version": "v0.1.5b2",
|
||||
"version": "v0.3.3",
|
||||
"zeroconf": [
|
||||
"_miot-central._tcp.local."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ class MIoTClient:
|
||||
# Device list update timestamp
|
||||
_device_list_update_ts: int
|
||||
|
||||
_sub_source_list: dict[str, str]
|
||||
_sub_source_list: dict[str, Optional[str]]
|
||||
_sub_tree: MIoTMatcher
|
||||
_sub_device_state: dict[str, MipsDeviceState]
|
||||
|
||||
@ -620,7 +620,7 @@ class MIoTClient:
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
# Gateway control
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
if (
|
||||
device_gw and device_gw.get('online', False)
|
||||
and device_gw.get('specv2_access', False)
|
||||
@ -629,11 +629,14 @@ class MIoTClient:
|
||||
mips = self._mips_local.get(device_gw['group_id'], None)
|
||||
if mips is None:
|
||||
_LOGGER.error(
|
||||
'no gw route, %s, try control throw cloud',
|
||||
'no gateway route, %s, try control through cloud',
|
||||
device_gw)
|
||||
else:
|
||||
result = await mips.set_prop_async(
|
||||
did=did, siid=siid, piid=piid, value=value)
|
||||
_LOGGER.debug(
|
||||
'gateway set prop, %s.%d.%d, %s -> %s',
|
||||
did, siid, piid, value, result)
|
||||
rc = (result or {}).get(
|
||||
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
||||
if rc in [0, 1]:
|
||||
@ -641,12 +644,13 @@ class MIoTClient:
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Lan control
|
||||
device_lan: dict = self._device_list_lan.get(did, None)
|
||||
device_lan = self._device_list_lan.get(did, None)
|
||||
if device_lan and device_lan.get('online', False):
|
||||
result = await self._miot_lan.set_prop_async(
|
||||
did=did, siid=siid, piid=piid, value=value)
|
||||
_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(
|
||||
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
||||
if rc in [0, 1]:
|
||||
@ -657,12 +661,12 @@ class MIoTClient:
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
result: list = await self._http.set_prop_async(
|
||||
result = await self._http.set_prop_async(
|
||||
params=[
|
||||
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
|
||||
])
|
||||
_LOGGER.debug(
|
||||
'set prop response, %s.%d.%d, %s, result, %s',
|
||||
'cloud set prop, %s.%d.%d, %s -> %s',
|
||||
did, siid, piid, value, result)
|
||||
if result and len(result) == 1:
|
||||
rc = result[0].get(
|
||||
@ -746,7 +750,7 @@ class MIoTClient:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
if (
|
||||
@ -782,7 +786,7 @@ class MIoTClient:
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud.get('online', False):
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
result: dict = await self._http.action_async(
|
||||
did=did, siid=siid, aiid=aiid, in_list=in_list)
|
||||
if result:
|
||||
@ -798,14 +802,15 @@ class MIoTClient:
|
||||
dids=[did]))
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Show error message
|
||||
# TODO: Show error message
|
||||
_LOGGER.error(
|
||||
'client action failed, %s.%d.%d', did, siid, aiid)
|
||||
return None
|
||||
return []
|
||||
|
||||
def sub_prop(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: int = None, piid: int = None, handler_ctx: Any = None
|
||||
siid: Optional[int] = None, piid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -818,7 +823,9 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub prop, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
|
||||
def unsub_prop(
|
||||
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
|
||||
) -> bool:
|
||||
topic = (
|
||||
f'{did}/p/'
|
||||
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
||||
@ -829,7 +836,8 @@ class MIoTClient:
|
||||
|
||||
def sub_event(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: int = None, eiid: int = None, handler_ctx: Any = None
|
||||
siid: Optional[int] = None, eiid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -841,7 +849,9 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub event, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
|
||||
def unsub_event(
|
||||
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
|
||||
) -> bool:
|
||||
topic = (
|
||||
f'{did}/e/'
|
||||
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
||||
@ -873,16 +883,7 @@ class MIoTClient:
|
||||
sub_from = self._sub_source_list.pop(did, None)
|
||||
# Unsub
|
||||
if sub_from:
|
||||
if sub_from == 'cloud':
|
||||
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)
|
||||
self.__unsub_from(sub_from, did)
|
||||
# Storage
|
||||
await self._storage.save_async(
|
||||
domain='miot_devices',
|
||||
@ -930,6 +931,39 @@ class MIoTClient:
|
||||
delay_sec, lambda: self._main_loop.create_task(
|
||||
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
|
||||
def __update_device_msg_sub(self, did: str) -> None:
|
||||
if did not in self._device_list_cache:
|
||||
@ -961,27 +995,9 @@ class MIoTClient:
|
||||
return
|
||||
# Unsub old
|
||||
if from_old:
|
||||
if from_old == 'cloud':
|
||||
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)
|
||||
self.__unsub_from(from_old, did)
|
||||
# Sub new
|
||||
if from_new == 'cloud':
|
||||
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_from(from_new, did)
|
||||
self._sub_source_list[did] = from_new
|
||||
_LOGGER.info(
|
||||
'device sub changed, %s, from %s to %s', did, from_old, from_new)
|
||||
@ -1081,7 +1097,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1091,8 +1107,8 @@ class MIoTClient:
|
||||
self, group_id: str, state: bool
|
||||
) -> None:
|
||||
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
if mips is None:
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
_LOGGER.error(
|
||||
'local mips state changed, mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1124,7 +1140,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1171,7 +1187,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self._device_list_lan = {}
|
||||
@ -1201,7 +1217,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1257,7 +1273,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1301,9 +1317,8 @@ class MIoTClient:
|
||||
async def __load_cache_device_async(self) -> None:
|
||||
"""Load device list from cache."""
|
||||
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
|
||||
domain='miot_devices',
|
||||
name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict)
|
||||
domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict) # type: ignore
|
||||
if not cache_list:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate(
|
||||
@ -1346,7 +1361,7 @@ class MIoTClient:
|
||||
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
|
||||
did, {}).get('online', None)
|
||||
cloud_state_new: Optional[bool] = None
|
||||
device_new: dict = cloud_list.pop(did, None)
|
||||
device_new = cloud_list.pop(did, None)
|
||||
if device_new:
|
||||
cloud_state_new = device_new.get('online', None)
|
||||
# Update cache device info
|
||||
@ -1371,7 +1386,7 @@ class MIoTClient:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
# Call device state changed callback
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1426,8 +1441,7 @@ class MIoTClient:
|
||||
self, dids: list[str]
|
||||
) -> None:
|
||||
_LOGGER.debug('refresh cloud device with dids, %s', dids)
|
||||
cloud_list: dict[str, dict] = (
|
||||
await self._http.get_devices_with_dids_async(dids=dids))
|
||||
cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
|
||||
if cloud_list is None:
|
||||
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
|
||||
return
|
||||
@ -1466,11 +1480,11 @@ class MIoTClient:
|
||||
for did, info in self._device_list_cache.items():
|
||||
if did not in filter_dids:
|
||||
continue
|
||||
device_old: dict = self._device_list_gateway.get(did, None)
|
||||
device_old = self._device_list_gateway.get(did, None)
|
||||
gw_state_old = device_old.get(
|
||||
'online', False) if device_old else False
|
||||
gw_state_new: bool = False
|
||||
device_new: dict = gw_list.pop(did, None)
|
||||
device_new = gw_list.pop(did, None)
|
||||
if device_new:
|
||||
# Update gateway device info
|
||||
self._device_list_gateway[did] = {
|
||||
@ -1493,7 +1507,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1518,7 +1532,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1533,7 +1547,7 @@ class MIoTClient:
|
||||
'refresh gw devices with group_id, %s', group_id)
|
||||
# Remove timer
|
||||
self._mips_local_state_changed_timers.pop(group_id, None)
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
_LOGGER.error('mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1900,77 +1914,73 @@ async def get_miot_instance_async(
|
||||
) -> MIoTClient:
|
||||
if entry_id is None:
|
||||
raise MIoTClientError('invalid entry_id')
|
||||
miot_client: MIoTClient = None
|
||||
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None):
|
||||
miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
|
||||
if miot_client:
|
||||
_LOGGER.info('instance exist, %s', entry_id)
|
||||
miot_client = a
|
||||
else:
|
||||
if entry_data is None:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if loop is None:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get(
|
||||
'network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get(
|
||||
'miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
network=network,
|
||||
mips_service=mips_service,
|
||||
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||
_LOGGER.info('create miot_lan instance')
|
||||
# MIoT client
|
||||
miot_client = MIoTClient(
|
||||
entry_id=entry_id,
|
||||
entry_data=entry_data,
|
||||
return miot_client
|
||||
# Create new instance
|
||||
if not entry_data:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if not loop:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get('network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
network=network,
|
||||
storage=storage,
|
||||
mips_service=mips_service,
|
||||
miot_lan=miot_lan,
|
||||
loop=loop
|
||||
)
|
||||
miot_client.persistent_notify = persistent_notify
|
||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
||||
_LOGGER.info(
|
||||
'new miot_client instance, %s, %s', entry_id, entry_data)
|
||||
await miot_client.init_async()
|
||||
|
||||
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||
_LOGGER.info('create miot_lan instance')
|
||||
# MIoT client
|
||||
miot_client = MIoTClient(
|
||||
entry_id=entry_id,
|
||||
entry_data=entry_data,
|
||||
network=network,
|
||||
storage=storage,
|
||||
mips_service=mips_service,
|
||||
miot_lan=miot_lan,
|
||||
loop=loop
|
||||
)
|
||||
miot_client.persistent_notify = persistent_notify
|
||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
||||
_LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)
|
||||
await miot_client.init_async()
|
||||
return miot_client
|
||||
|
@ -382,7 +382,7 @@ class MIoTHttpClient:
|
||||
|
||||
return res_obj['data']
|
||||
|
||||
async def get_central_cert_async(self, csr: str) -> Optional[str]:
|
||||
async def get_central_cert_async(self, csr: str) -> str:
|
||||
if not isinstance(csr, str):
|
||||
raise MIoTHttpError('invalid params')
|
||||
|
||||
@ -444,6 +444,17 @@ class MIoTHttpClient:
|
||||
|
||||
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:
|
||||
res_obj = await self.__mihome_api_post_async(
|
||||
url_path='/app/v2/homeroom/gethome',
|
||||
@ -499,19 +510,22 @@ class MIoTHttpClient:
|
||||
):
|
||||
more_list = await self.__get_dev_room_page_async(
|
||||
max_id=res_obj['result']['max_id'])
|
||||
for home_id, info in more_list.items():
|
||||
if home_id not in home_infos['homelist']:
|
||||
_LOGGER.info('unknown home, %s, %s', home_id, info)
|
||||
continue
|
||||
home_infos['homelist'][home_id]['dids'].extend(info['dids'])
|
||||
for room_id, info in info['room_info'].items():
|
||||
home_infos['homelist'][home_id]['room_info'].setdefault(
|
||||
room_id, {
|
||||
'room_id': room_id,
|
||||
'room_name': '',
|
||||
'dids': []})
|
||||
home_infos['homelist'][home_id]['room_info'][
|
||||
room_id]['dids'].extend(info['dids'])
|
||||
for device_source in ['homelist', 'share_home_list']:
|
||||
for home_id, info in more_list.items():
|
||||
if home_id not in home_infos[device_source]:
|
||||
_LOGGER.info('unknown home, %s, %s', home_id, info)
|
||||
continue
|
||||
home_infos[device_source][home_id]['dids'].extend(
|
||||
info['dids'])
|
||||
for room_id, info in info['room_info'].items():
|
||||
home_infos[device_source][home_id][
|
||||
'room_info'].setdefault(
|
||||
room_id, {
|
||||
'room_id': room_id,
|
||||
'room_name': '',
|
||||
'dids': []})
|
||||
home_infos[device_source][home_id]['room_info'][
|
||||
room_id]['dids'].extend(info['dids'])
|
||||
|
||||
return {
|
||||
'uid': uid,
|
||||
@ -651,6 +665,25 @@ class MIoTHttpClient:
|
||||
'room_name': room_name,
|
||||
'group_id': group_id
|
||||
} 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()))
|
||||
results = await self.get_devices_with_dids_async(dids=dids)
|
||||
if results is None:
|
||||
|
@ -56,6 +56,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
@ -72,6 +73,7 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
UnitOfDataRate
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
@ -243,12 +245,12 @@ class MIoTDevice:
|
||||
def sub_device_state(
|
||||
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
||||
) -> int:
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._device_state_sub_list:
|
||||
self._device_state_sub_list[key][str(self._sub_id)] = handler
|
||||
self._device_state_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._device_state_sub_list[key] = {str(self._sub_id): handler}
|
||||
return self._sub_id
|
||||
self._device_state_sub_list[key] = {str(sub_id): handler}
|
||||
return sub_id
|
||||
|
||||
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
||||
sub_list = self._device_state_sub_list.get(key, None)
|
||||
@ -266,14 +268,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self.miot_client.sub_prop(
|
||||
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
|
||||
return self._sub_id
|
||||
return sub_id
|
||||
|
||||
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
|
||||
key: str = f'p.{siid}.{piid}'
|
||||
@ -294,14 +296,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self.miot_client.sub_event(
|
||||
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
|
||||
return self._sub_id
|
||||
return sub_id
|
||||
|
||||
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
|
||||
key: str = f'e.{siid}.{eiid}'
|
||||
@ -343,10 +345,11 @@ class MIoTDevice:
|
||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||
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 (
|
||||
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(
|
||||
self, ha_domain: str, spec_name: str, siid: int, piid: int
|
||||
@ -414,10 +417,12 @@ class MIoTDevice:
|
||||
spec_name: str = spec_instance.name
|
||||
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
|
||||
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
|
||||
if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:
|
||||
return None
|
||||
# 1. The device shall have all required services.
|
||||
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
|
||||
if not {
|
||||
service.name for service in spec_instance.services
|
||||
service.name for service in spec_instance.services
|
||||
}.issuperset(required_services):
|
||||
return None
|
||||
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
|
||||
@ -427,9 +432,13 @@ class MIoTDevice:
|
||||
for service in spec_instance.services:
|
||||
if service.platform:
|
||||
continue
|
||||
required_properties: dict
|
||||
optional_properties: dict
|
||||
required_actions: set
|
||||
optional_actions: set
|
||||
# 2. The service shall have all required properties, actions.
|
||||
if service.name in required_services:
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'required'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -446,7 +455,7 @@ class MIoTDevice:
|
||||
service.name, {}
|
||||
).get('optional', {}).get('actions', set({}))
|
||||
elif service.name in optional_services:
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'optional'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -484,7 +493,7 @@ class MIoTDevice:
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# action
|
||||
@ -499,85 +508,94 @@ class MIoTDevice:
|
||||
return entity_data
|
||||
|
||||
def parse_miot_service_entity(
|
||||
self, service_instance: MIoTSpecService
|
||||
self, miot_service: MIoTSpecService
|
||||
) -> Optional[MIoTEntityData]:
|
||||
service = service_instance
|
||||
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP):
|
||||
if (
|
||||
miot_service.platform
|
||||
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
|
||||
):
|
||||
return None
|
||||
|
||||
service_name = service.name
|
||||
service_name = miot_service.name
|
||||
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
|
||||
service_name = SPEC_SERVICE_TRANS_MAP[service_name]
|
||||
# 1. The service shall have all required properties.
|
||||
if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:
|
||||
return None
|
||||
# Required properties, required access mode
|
||||
required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'required'].get('properties', {})
|
||||
if not {
|
||||
prop.name for prop in service.properties if prop.access
|
||||
prop.name for prop in miot_service.properties if prop.access
|
||||
}.issuperset(set(required_properties.keys())):
|
||||
return None
|
||||
# 2. The required property shall have all required access mode.
|
||||
for prop in service.properties:
|
||||
for prop in miot_service.properties:
|
||||
if prop.name in required_properties:
|
||||
if not set(prop.access).issuperset(
|
||||
required_properties[prop.name]):
|
||||
return None
|
||||
# Required actions
|
||||
# Required events
|
||||
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
|
||||
entity_data = MIoTEntityData(platform=platform, spec=service_instance)
|
||||
entity_data = MIoTEntityData(platform=platform, spec=miot_service)
|
||||
# Optional properties
|
||||
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'optional'].get('properties', set({}))
|
||||
for prop in service.properties:
|
||||
for prop in miot_service.properties:
|
||||
if prop.name in set.union(
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# action
|
||||
# event
|
||||
# No actions or events is in SPEC_SERVICE_TRANS_MAP now.
|
||||
service.platform = platform
|
||||
# Optional actions
|
||||
# Optional events
|
||||
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
|
||||
|
||||
def parse_miot_property_entity(
|
||||
self, property_instance: MIoTSpecProperty
|
||||
) -> Optional[dict[str, str]]:
|
||||
prop = property_instance
|
||||
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
|
||||
if (
|
||||
prop.platform
|
||||
or (prop.name not in SPEC_PROP_TRANS_MAP['properties'])
|
||||
miot_prop.platform
|
||||
or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
|
||||
):
|
||||
return None
|
||||
|
||||
prop_name = prop.name
|
||||
return False
|
||||
prop_name = miot_prop.name
|
||||
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
|
||||
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
|
||||
# Check
|
||||
prop_access: set = set({})
|
||||
if prop.readable:
|
||||
if miot_prop.readable:
|
||||
prop_access.add('read')
|
||||
if prop.writable:
|
||||
if miot_prop.writable:
|
||||
prop_access.add('write')
|
||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['access']):
|
||||
return None
|
||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
return False
|
||||
if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['format']:
|
||||
return None
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
return False
|
||||
miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
'device_class']
|
||||
result = {'platform': platform, 'device_class': device_class}
|
||||
# optional:
|
||||
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional']
|
||||
if 'state_class' in optional:
|
||||
result['state_class'] = optional['state_class']
|
||||
if not prop.unit and 'unit_of_measurement' in optional:
|
||||
result['unit_of_measurement'] = optional['unit_of_measurement']
|
||||
return result
|
||||
# Optional params
|
||||
if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['state_class']
|
||||
if (
|
||||
not miot_prop.external_unit
|
||||
and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]
|
||||
):
|
||||
# Priority: spec_modify.unit > unit_convert > specv2entity.unit
|
||||
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['unit_of_measurement']
|
||||
# Priority: default.icon when device_class is set > spec_modify.icon
|
||||
# > icon_convert
|
||||
miot_prop.platform = platform
|
||||
return True
|
||||
|
||||
def spec_transform(self) -> None:
|
||||
"""Parse service, property, event, action from device spec."""
|
||||
@ -589,7 +607,7 @@ class MIoTDevice:
|
||||
# STEP 2: service conversion
|
||||
for service in self.spec_instance.services:
|
||||
service_entity = self.parse_miot_service_entity(
|
||||
service_instance=service)
|
||||
miot_service=service)
|
||||
if service_entity:
|
||||
self.append_entity(entity_data=service_entity)
|
||||
# STEP 3.1: property conversion
|
||||
@ -598,20 +616,11 @@ class MIoTDevice:
|
||||
continue
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
prop_entity = self.parse_miot_property_entity(
|
||||
property_instance=prop)
|
||||
if prop_entity:
|
||||
prop.platform = prop_entity['platform']
|
||||
prop.device_class = prop_entity['device_class']
|
||||
if 'state_class' in prop_entity:
|
||||
prop.state_class = prop_entity['state_class']
|
||||
if 'unit_of_measurement' in prop_entity:
|
||||
prop.external_unit = self.unit_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
prop.icon = self.icon_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
# general conversion
|
||||
if not prop.icon:
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# Special conversion
|
||||
self.parse_miot_property_entity(miot_prop=prop)
|
||||
# General conversion
|
||||
if not prop.platform:
|
||||
if prop.writable:
|
||||
if prop.format_ == str:
|
||||
@ -625,7 +634,7 @@ class MIoTDevice:
|
||||
prop.platform = 'number'
|
||||
else:
|
||||
# Irregular property will not be transformed.
|
||||
pass
|
||||
continue
|
||||
elif prop.readable or prop.notifiable:
|
||||
if prop.format_ == bool:
|
||||
prop.platform = 'binary_sensor'
|
||||
@ -653,11 +662,66 @@ class MIoTDevice:
|
||||
self.append_action(action=action)
|
||||
|
||||
def unit_convert(self, spec_unit: str) -> Optional[str]:
|
||||
"""Convert MIoT unit to Home Assistant unit."""
|
||||
"""Convert MIoT unit to Home Assistant unit.
|
||||
25/01/20: All online prop unit statistical tables: unit, quantity.
|
||||
{
|
||||
"no_unit": 148499,
|
||||
"percentage": 10042,
|
||||
"kelvin": 1895,
|
||||
"rgb": 772, // color
|
||||
"celsius": 5762,
|
||||
"none": 16106,
|
||||
"hours": 1540,
|
||||
"minutes": 5061,
|
||||
"ms": 27,
|
||||
"watt": 216,
|
||||
"arcdegrees": 159,
|
||||
"ppm": 177,
|
||||
"μg/m3": 106,
|
||||
"days": 571,
|
||||
"seconds": 2749,
|
||||
"B/s": 21,
|
||||
"pascal": 110,
|
||||
"mg/m3": 339,
|
||||
"lux": 125,
|
||||
"kWh": 124,
|
||||
"mv": 2,
|
||||
"V": 38,
|
||||
"A": 29,
|
||||
"mV": 4,
|
||||
"L": 352,
|
||||
"m": 37,
|
||||
"毫摩尔每升": 2, // blood-sugar, cholesterol
|
||||
"mmol/L": 1, // urea
|
||||
"weeks": 26,
|
||||
"meter": 3,
|
||||
"dB": 26,
|
||||
"hour": 14,
|
||||
"calorie": 19, // 1 cal = 4.184 J
|
||||
"ppb": 3,
|
||||
"arcdegress": 30,
|
||||
"bpm": 4, // realtime-heartrate
|
||||
"gram": 7,
|
||||
"km/h": 9,
|
||||
"W": 1,
|
||||
"m3/h": 2,
|
||||
"kilopascal": 1,
|
||||
"mL": 4,
|
||||
"mmHg": 4,
|
||||
"w": 1,
|
||||
"liter": 1,
|
||||
"cm": 3,
|
||||
"mA": 2,
|
||||
"kilogram": 2,
|
||||
"kcal/d": 2, // basal-metabolism
|
||||
"times": 1 // exercise-count
|
||||
}
|
||||
"""
|
||||
unit_map = {
|
||||
'percentage': PERCENTAGE,
|
||||
'weeks': UnitOfTime.WEEKS,
|
||||
'days': UnitOfTime.DAYS,
|
||||
'hour': UnitOfTime.HOURS,
|
||||
'hours': UnitOfTime.HOURS,
|
||||
'minutes': UnitOfTime.MINUTES,
|
||||
'seconds': UnitOfTime.SECONDS,
|
||||
@ -672,90 +736,117 @@ class MIoTDevice:
|
||||
'ppb': CONCENTRATION_PARTS_PER_BILLION,
|
||||
'lux': LIGHT_LUX,
|
||||
'pascal': UnitOfPressure.PA,
|
||||
'kilopascal': UnitOfPressure.KPA,
|
||||
'mmHg': UnitOfPressure.MMHG,
|
||||
'bar': UnitOfPressure.BAR,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'L': UnitOfVolume.LITERS,
|
||||
'liter': UnitOfVolume.LITERS,
|
||||
'mL': UnitOfVolume.MILLILITERS,
|
||||
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
'm/s': UnitOfSpeed.METERS_PER_SECOND,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'w': UnitOfPower.WATT,
|
||||
'W': UnitOfPower.WATT,
|
||||
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
|
||||
'A': UnitOfElectricCurrent.AMPERE,
|
||||
'mA': UnitOfElectricCurrent.MILLIAMPERE,
|
||||
'V': UnitOfElectricPotential.VOLT,
|
||||
'mv': UnitOfElectricPotential.MILLIVOLT,
|
||||
'mV': UnitOfElectricPotential.MILLIVOLT,
|
||||
'cm': UnitOfLength.CENTIMETERS,
|
||||
'm': UnitOfLength.METERS,
|
||||
'meter': UnitOfLength.METERS,
|
||||
'km': UnitOfLength.KILOMETERS,
|
||||
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
'gram': UnitOfMass.GRAMS,
|
||||
'kilogram': UnitOfMass.KILOGRAMS,
|
||||
'dB': SIGNAL_STRENGTH_DECIBELS,
|
||||
'arcdegrees': DEGREE,
|
||||
'arcdegress': DEGREE,
|
||||
'kB': UnitOfInformation.KILOBYTES,
|
||||
'MB': UnitOfInformation.MEGABYTES,
|
||||
'GB': UnitOfInformation.GIGABYTES,
|
||||
'TB': UnitOfInformation.TERABYTES,
|
||||
'B/s': UnitOfDataRate.BYTES_PER_SECOND,
|
||||
'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,
|
||||
'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND
|
||||
}
|
||||
|
||||
# Handle UnitOfConductivity separately since
|
||||
# it might not be available in all HA versions
|
||||
try:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.const import UnitOfConductivity
|
||||
from homeassistant.const import UnitOfConductivity # type: ignore
|
||||
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
|
||||
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
|
||||
except Exception: # pylint: disable=broad-except
|
||||
unit_map['μS/cm'] = 'μS/cm'
|
||||
unit_map['mWh'] = 'mWh'
|
||||
|
||||
return unit_map.get(spec_unit, None)
|
||||
|
||||
def icon_convert(self, spec_unit: str) -> Optional[str]:
|
||||
if spec_unit in ['percentage']:
|
||||
if spec_unit in {'percentage'}:
|
||||
return 'mdi:percent'
|
||||
if spec_unit in [
|
||||
'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']:
|
||||
if spec_unit in {
|
||||
'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
|
||||
}:
|
||||
return 'mdi:clock'
|
||||
if spec_unit in ['celsius']:
|
||||
if spec_unit in {'celsius'}:
|
||||
return 'mdi:temperature-celsius'
|
||||
if spec_unit in ['fahrenheit']:
|
||||
if spec_unit in {'fahrenheit'}:
|
||||
return 'mdi:temperature-fahrenheit'
|
||||
if spec_unit in ['kelvin']:
|
||||
if spec_unit in {'kelvin'}:
|
||||
return 'mdi:temperature-kelvin'
|
||||
if spec_unit in ['μg/m3', 'mg/m3', 'ppm', 'ppb']:
|
||||
if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:
|
||||
return 'mdi:blur'
|
||||
if spec_unit in ['lux']:
|
||||
if spec_unit in {'lux'}:
|
||||
return 'mdi:brightness-6'
|
||||
if spec_unit in ['pascal', 'megapascal', 'bar']:
|
||||
if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
|
||||
return 'mdi:gauge'
|
||||
if spec_unit in ['watt']:
|
||||
if spec_unit in {'watt', 'w', 'W'}:
|
||||
return 'mdi:flash-triangle'
|
||||
if spec_unit in ['L', 'mL']:
|
||||
if spec_unit in {'L', 'mL'}:
|
||||
return 'mdi:gas-cylinder'
|
||||
if spec_unit in ['km/h', 'm/s']:
|
||||
if spec_unit in {'km/h', 'm/s'}:
|
||||
return 'mdi:speedometer'
|
||||
if spec_unit in ['kWh']:
|
||||
if spec_unit in {'kWh'}:
|
||||
return 'mdi:transmission-tower'
|
||||
if spec_unit in ['A', 'mA']:
|
||||
if spec_unit in {'A', 'mA'}:
|
||||
return 'mdi:current-ac'
|
||||
if spec_unit in ['V', 'mV']:
|
||||
if spec_unit in {'V', 'mv', 'mV'}:
|
||||
return 'mdi:current-dc'
|
||||
if spec_unit in ['m', 'km']:
|
||||
if spec_unit in {'cm', 'm', 'meter', 'km'}:
|
||||
return 'mdi:ruler'
|
||||
if spec_unit in ['rgb']:
|
||||
if spec_unit in {'rgb'}:
|
||||
return 'mdi:palette'
|
||||
if spec_unit in ['m3/h', 'L/s']:
|
||||
if spec_unit in {'m3/h', 'L/s'}:
|
||||
return 'mdi:pipe-leak'
|
||||
if spec_unit in ['μS/cm']:
|
||||
if spec_unit in {'μS/cm'}:
|
||||
return 'mdi:resistor-nodes'
|
||||
if spec_unit in ['gram']:
|
||||
if spec_unit in {'gram', 'kilogram'}:
|
||||
return 'mdi:weight'
|
||||
if spec_unit in ['dB']:
|
||||
if spec_unit in {'dB'}:
|
||||
return 'mdi:signal-distance-variant'
|
||||
if spec_unit in ['times']:
|
||||
if spec_unit in {'times'}:
|
||||
return 'mdi:counter'
|
||||
if spec_unit in ['mmol/L']:
|
||||
if spec_unit in {'mmol/L'}:
|
||||
return 'mdi:dots-hexagon'
|
||||
if spec_unit in ['arcdegress']:
|
||||
return 'mdi:angle-obtuse'
|
||||
if spec_unit in ['kB']:
|
||||
if spec_unit in {'kB', 'MB', 'GB'}:
|
||||
return 'mdi:network-pos'
|
||||
if spec_unit in ['calorie', 'kCal']:
|
||||
if spec_unit in {'arcdegress', 'arcdegrees'}:
|
||||
return 'mdi:angle-obtuse'
|
||||
if spec_unit in {'B/s', 'KB/s', 'MB/s', 'GB/s'}:
|
||||
return 'mdi:network'
|
||||
if spec_unit in {'calorie', 'kCal'}:
|
||||
return 'mdi:food'
|
||||
return None
|
||||
|
||||
def __gen_sub_id(self) -> int:
|
||||
self._sub_id += 1
|
||||
return self._sub_id
|
||||
|
||||
def __on_device_state_changed(
|
||||
self, did: str, state: MIoTDeviceState, ctx: Any
|
||||
) -> None:
|
||||
@ -806,10 +897,12 @@ class MIoTServiceEntity(Entity):
|
||||
self._attr_name = f' {self.entity_data.spec.description_trans}'
|
||||
elif isinstance(self.entity_data.spec, MIoTSpecService):
|
||||
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 = (
|
||||
f'{"* "if self.entity_data.spec.proprietary else " "}'
|
||||
f'{self.entity_data.spec.description_trans}')
|
||||
self._attr_entity_category = entity_data.spec.entity_category
|
||||
# Set entity attr
|
||||
self._attr_unique_id = self.entity_id
|
||||
self._attr_should_poll = False
|
||||
@ -903,14 +996,14 @@ class MIoTServiceEntity(Entity):
|
||||
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
|
||||
|
||||
def get_map_value(
|
||||
self, map_: dict[int, Any], key: int
|
||||
self, map_: Optional[dict[int, Any]], key: int
|
||||
) -> Any:
|
||||
if map_ is None:
|
||||
return None
|
||||
return map_.get(key, None)
|
||||
|
||||
def get_map_key(
|
||||
self, map_: dict[int, Any], value: Any
|
||||
self, map_: Optional[dict[int, Any]], value: Any
|
||||
) -> Optional[int]:
|
||||
if map_ is None:
|
||||
return None
|
||||
@ -919,7 +1012,7 @@ class MIoTServiceEntity(Entity):
|
||||
return key
|
||||
return None
|
||||
|
||||
def get_prop_value(self, prop: MIoTSpecProperty) -> Any:
|
||||
def get_prop_value(self, prop: Optional[MIoTSpecProperty]) -> Any:
|
||||
if not prop:
|
||||
_LOGGER.error(
|
||||
'get_prop_value error, property is None, %s, %s',
|
||||
@ -927,7 +1020,9 @@ class MIoTServiceEntity(Entity):
|
||||
return None
|
||||
return self._prop_value_map.get(prop, None)
|
||||
|
||||
def set_prop_value(self, prop: MIoTSpecProperty, value: Any) -> None:
|
||||
def set_prop_value(
|
||||
self, prop: Optional[MIoTSpecProperty], value: Any
|
||||
) -> None:
|
||||
if not prop:
|
||||
_LOGGER.error(
|
||||
'set_prop_value error, property is None, %s, %s',
|
||||
@ -936,13 +1031,14 @@ class MIoTServiceEntity(Entity):
|
||||
self._prop_value_map[prop] = value
|
||||
|
||||
async def set_property_async(
|
||||
self, prop: MIoTSpecProperty, value: Any, update: bool = True
|
||||
self, prop: Optional[MIoTSpecProperty], value: Any,
|
||||
update_value: bool = True, write_ha_state: bool = True
|
||||
) -> bool:
|
||||
value = prop.value_format(value)
|
||||
if not prop:
|
||||
raise RuntimeError(
|
||||
f'set property failed, property is None, '
|
||||
f'{self.entity_id}, {self.name}')
|
||||
value = prop.value_format(value)
|
||||
if prop not in self.entity_data.props:
|
||||
raise RuntimeError(
|
||||
f'set property failed, unknown property, '
|
||||
@ -958,8 +1054,9 @@ class MIoTServiceEntity(Entity):
|
||||
except MIoTClientError as e:
|
||||
raise RuntimeError(
|
||||
f'{e}, {self.entity_id}, {self.name}, {prop.name}') from e
|
||||
if update:
|
||||
if update_value:
|
||||
self._prop_value_map[prop] = value
|
||||
if write_ha_state:
|
||||
self.async_write_ha_state()
|
||||
return True
|
||||
|
||||
@ -1184,6 +1281,7 @@ class MIoTPropertyEntity(Entity):
|
||||
def __on_value_changed(self, params: dict, ctx: Any) -> None:
|
||||
_LOGGER.debug('property changed, %s', params)
|
||||
self._value = self.spec.value_format(params['value'])
|
||||
self._value = self.spec.eval_expr(self._value)
|
||||
if not self._pending_write_ha_state_timer:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -226,7 +226,7 @@ class _MIoTLanDevice:
|
||||
def gen_packet(
|
||||
self, out_buffer: bytearray, clear_data: dict, did: str, offset: 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()
|
||||
padded_data = padder.update(clear_bytes) + padder.finalize()
|
||||
if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer):
|
||||
|
@ -60,6 +60,7 @@ from typing import Any, Callable, Optional, final, Coroutine
|
||||
|
||||
from paho.mqtt.client import (
|
||||
MQTT_ERR_SUCCESS,
|
||||
MQTT_ERR_NO_CONN,
|
||||
MQTT_ERR_UNKNOWN,
|
||||
Client,
|
||||
MQTTv5,
|
||||
@ -533,7 +534,7 @@ class _MipsClient(ABC):
|
||||
return
|
||||
try:
|
||||
result, mid = self._mqtt.unsubscribe(topic=topic)
|
||||
if result == MQTT_ERR_SUCCESS:
|
||||
if (result == MQTT_ERR_SUCCESS) or (result == MQTT_ERR_NO_CONN):
|
||||
self.log_debug(
|
||||
f'mips unsub internal success, {result}, {mid}, {topic}')
|
||||
return
|
||||
@ -1213,10 +1214,13 @@ class MipsLocalClient(_MipsClient):
|
||||
or 'did' not in msg
|
||||
or 'siid' not in msg
|
||||
or 'eiid' not in msg
|
||||
or 'arguments' not in msg
|
||||
# or 'arguments' not in msg
|
||||
):
|
||||
# self.log_error(f'on_event_msg, recv unknown msg, {payload}')
|
||||
self.log_info('unknown event msg, %s', payload)
|
||||
return
|
||||
if 'arguments' not in msg:
|
||||
self.log_info('wrong event msg, %s', payload)
|
||||
msg['arguments'] = []
|
||||
if handler:
|
||||
self.log_debug('local, on event_occurred, %s', payload)
|
||||
handler(msg, ctx)
|
||||
@ -1414,7 +1418,7 @@ class MipsLocalClient(_MipsClient):
|
||||
@final
|
||||
@on_dev_list_changed.setter
|
||||
def on_dev_list_changed(
|
||||
self, func: Callable[[Any, list[str]], Coroutine]
|
||||
self, func: Optional[Callable[[Any, list[str]], Coroutine]]
|
||||
) -> None:
|
||||
"""run in main loop."""
|
||||
self._on_dev_list_changed = func
|
||||
|
@ -94,7 +94,7 @@ class MIoTNetwork:
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
|
||||
_ip_addr_map: dict[str, float]
|
||||
_url_addr_list: dict[str, float]
|
||||
_http_addr_map: dict[str, float]
|
||||
_http_session: aiohttp.ClientSession
|
||||
|
||||
_refresh_interval: int
|
||||
@ -283,8 +283,8 @@ class MIoTNetwork:
|
||||
[
|
||||
'ping', '-c', '1', '-w',
|
||||
str(self._DETECT_TIMEOUT), address]),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
await process.communicate()
|
||||
if process.returncode == 0:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -59,43 +59,6 @@ data:
|
||||
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
|
||||
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
||||
translate:
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
'true': Kontakt
|
||||
en:
|
||||
'false': No Contact
|
||||
'true': Contact
|
||||
es:
|
||||
'false': Sin contacto
|
||||
'true': Contacto
|
||||
fr:
|
||||
'false': Pas de contact
|
||||
'true': Contact
|
||||
it:
|
||||
'false': Nessun contatto
|
||||
'true': Contatto
|
||||
ja:
|
||||
'false': 非接触
|
||||
'true': 接触
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Нет контакта
|
||||
'true': Контакт
|
||||
zh-Hans:
|
||||
'false': 分离
|
||||
'true': 接触
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
default:
|
||||
de:
|
||||
'false': Falsch
|
||||
@ -133,6 +96,43 @@ translate:
|
||||
zh-Hant:
|
||||
'false': 假
|
||||
'true': 真
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
'true': Kontakt
|
||||
en:
|
||||
'false': No Contact
|
||||
'true': Contact
|
||||
es:
|
||||
'false': Sin contacto
|
||||
'true': Contacto
|
||||
fr:
|
||||
'false': Pas de contact
|
||||
'true': Contact
|
||||
it:
|
||||
'false': Nessun contatto
|
||||
'true': Contatto
|
||||
ja:
|
||||
'false': 非接触
|
||||
'true': 接触
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Нет контакта
|
||||
'true': Контакт
|
||||
zh-Hans:
|
||||
'false': 分离
|
||||
'true': 接触
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
motion_state:
|
||||
de:
|
||||
'false': Keine Bewegung erkannt
|
||||
|
290
custom_components/xiaomi_home/miot/specs/multi_lang.json
Normal file
290
custom_components/xiaomi_home/miot/specs/multi_lang.json
Normal file
@ -0,0 +1,290 @@
|
||||
{
|
||||
"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:090615-x1tpm": {
|
||||
"en": {
|
||||
"service:027:property:001": "Fan Switch",
|
||||
"service:027:property:003": "Light Switch",
|
||||
"service:027:property:004": "Fan and Light Switch"
|
||||
}
|
||||
},
|
||||
"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": "右键确认"
|
||||
}
|
||||
},
|
||||
"urn:miot-spec-v2:device:thermostat:0000A031:cubee-th123e": {
|
||||
"ru": {
|
||||
"service:002": "термостат",
|
||||
"service:002:property:001": "выключатель",
|
||||
"service:002:property:002": "режим нагрузки",
|
||||
"service:002:property:002:valuelist:000": "без подогрева",
|
||||
"service:002:property:002:valuelist:001": "нагрев",
|
||||
"service:002:property:003": "неисправность",
|
||||
"service:002:property:003:valuelist:000": "ошибка датчика",
|
||||
"service:002:property:003:valuelist:001": "без ошибок",
|
||||
"service:002:property:003:valuelist:002": "защита от высоких температур",
|
||||
"service:002:property:003:valuelist:003": "криогенная защита",
|
||||
"service:002:property:004": "режим",
|
||||
"service:002:property:004:valuelist:000": "ручной режим",
|
||||
"service:002:property:004:valuelist:001": "домашний режим",
|
||||
"service:002:property:004:valuelist:002": "режим выхода из дома",
|
||||
"service:002:property:004:valuelist:003": "автоматический режим",
|
||||
"service:002:property:004:valuelist:004": "Режим сна",
|
||||
"service:002:property:005": "температура цели",
|
||||
"service:002:property:007": "текущая температура",
|
||||
"service:004": "Пользовательские услуги",
|
||||
"service:004:property:001": "детский замок",
|
||||
"service:004:property:002": "тип датчика",
|
||||
"service:004:property:002:valuelist:000": "внутренний датчик",
|
||||
"service:004:property:002:valuelist:001": "выносной датчик",
|
||||
"service:004:property:002:valuelist:002": "встроенный и внешний датчик",
|
||||
"service:004:property:003": "пусковая разность температур",
|
||||
"service:004:property:004": "компенсационная температура",
|
||||
"service:004:property:005": "температура выносного датчика",
|
||||
"service:004:property:006": "максимальная температура цели",
|
||||
"service:004:property:007": "минимальная температура цели "
|
||||
}
|
||||
},
|
||||
"urn:miot-spec-v2:device:thermostat:0000A031:cubee-th123w": {
|
||||
"ru": {
|
||||
"service:002": "термостат",
|
||||
"service:002:property:001": "выключатель",
|
||||
"service:002:property:002": "режим нагрузки",
|
||||
"service:002:property:002:valuelist:000": "нагрев",
|
||||
"service:002:property:002:valuelist:001": "без подогрева",
|
||||
"service:002:property:003": "неисправность",
|
||||
"service:002:property:003:valuelist:000": "без ошибок",
|
||||
"service:002:property:003:valuelist:001": "ошибка датчика",
|
||||
"service:002:property:003:valuelist:002": "защита от высоких температур",
|
||||
"service:002:property:003:valuelist:003": "криогенная защита",
|
||||
"service:002:property:004": "режим",
|
||||
"service:002:property:004:valuelist:000": "ручной режим",
|
||||
"service:002:property:004:valuelist:001": "домашний режим",
|
||||
"service:002:property:004:valuelist:002": "режим выхода из дома",
|
||||
"service:002:property:004:valuelist:003": "автоматический режим",
|
||||
"service:002:property:004:valuelist:004": "Режим сна",
|
||||
"service:002:property:005": "температура цели",
|
||||
"service:002:property:007": "текущая температура",
|
||||
"service:004": "Пользовательские услуги",
|
||||
"service:004:property:001": "детский замок",
|
||||
"service:004:property:002": "тип датчика",
|
||||
"service:004:property:002:valuelist:000": "внутренний датчик",
|
||||
"service:004:property:002:valuelist:001": "выносной датчик",
|
||||
"service:004:property:002:valuelist:002": "встроенный и внешний датчик",
|
||||
"service:004:property:003": "пусковая разность температур",
|
||||
"service:004:property:004": "компенсационная температура",
|
||||
"service:004:property:005": "температура выносного датчика",
|
||||
"service:004:property:006": "максимальная температура цели",
|
||||
"service:004:property:007": "минимальная температура цели "
|
||||
}
|
||||
},
|
||||
"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01": {
|
||||
"en": {
|
||||
"service:002": "Thermostat",
|
||||
"service:002:property:002": "Air Conditioner Mode",
|
||||
"service:004": "Air Conditioner"
|
||||
},
|
||||
"zh_cn": {
|
||||
"service:002": "地暖",
|
||||
"service:004": "空调"
|
||||
}
|
||||
},
|
||||
"urn:miot-spec-v2:device:vacuum:0000A006:ijai-v1": {
|
||||
"zh_cn": {
|
||||
"service:007:property:005:valuelist:000": "安静",
|
||||
"service:007:property:005:valuelist:001": "标准",
|
||||
"service:007:property:005:valuelist:002": "中档",
|
||||
"service:007:property:005:valuelist:003": "强力"
|
||||
}
|
||||
}
|
||||
}
|
221
custom_components/xiaomi_home/miot/specs/spec_add.json
Normal file
221
custom_components/xiaomi_home/miot/specs/spec_add.json
Normal file
@ -0,0 +1,221 @@
|
||||
{
|
||||
"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:thermostat:0000A031:tofan-wk01:1:0000C822": [
|
||||
{
|
||||
"iid": 2,
|
||||
"type": "urn:miot-spec-v2:service:thermostat:0000784A:tofan-wk01:1",
|
||||
"description": "Thermostat",
|
||||
"properties": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
|
||||
"description": "Switch Status",
|
||||
"format": "bool",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 2,
|
||||
"type": "urn:miot-spec-v2:property:mode-a:00000008:tofan-wk01:1",
|
||||
"description": "Mode",
|
||||
"format": "uint8",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
],
|
||||
"value-list": [
|
||||
{
|
||||
"value": 0,
|
||||
"description": "Auto"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"description": "Cool"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"description": "Heat"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"description": "Fan"
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"description": "Dry"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 3,
|
||||
"type": "urn:miot-spec-v2:property:fault:00000009:tofan-wk01:1",
|
||||
"description": "Device Fault",
|
||||
"format": "uint8",
|
||||
"access": [
|
||||
"read",
|
||||
"notify"
|
||||
],
|
||||
"value-list": [
|
||||
{
|
||||
"value": 0,
|
||||
"description": "No Faults"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 4,
|
||||
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
|
||||
"description": "Target Temperature",
|
||||
"format": "uint8",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
],
|
||||
"unit": "celsius",
|
||||
"value-range": [
|
||||
16,
|
||||
35,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:action:toggle:00002811:tofan-wk01:1",
|
||||
"description": "Toggle",
|
||||
"in": [],
|
||||
"out": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 4,
|
||||
"type": "urn:miot-spec-v2:service:air-conditioner:0000780F:tofan-wk01:1",
|
||||
"description": "Air Conditioner",
|
||||
"properties": [
|
||||
{
|
||||
"iid": 1,
|
||||
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
|
||||
"description": "Switch Status",
|
||||
"format": "bool",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 2,
|
||||
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
|
||||
"description": "Target Temperature",
|
||||
"format": "uint8",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
],
|
||||
"unit": "celsius",
|
||||
"value-range": [
|
||||
16,
|
||||
32,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"iid": 3,
|
||||
"type": "urn:miot-spec-v2:property:fan-level:00000016:tofan-wk01:1",
|
||||
"description": "Fan Level",
|
||||
"format": "uint8",
|
||||
"access": [
|
||||
"read",
|
||||
"write",
|
||||
"notify"
|
||||
],
|
||||
"value-list": [
|
||||
{
|
||||
"value": 0,
|
||||
"description": "Auto"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"description": "Low"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"description": "Medium"
|
||||
},
|
||||
{
|
||||
"value": 4,
|
||||
"description": "High"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -5,6 +5,9 @@ urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
|
||||
- 15.*
|
||||
services:
|
||||
- '10'
|
||||
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:
|
||||
properties:
|
||||
- '3.2'
|
||||
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
|
||||
properties:
|
||||
- '5.1'
|
||||
@ -41,3 +44,7 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
|
||||
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
|
||||
services:
|
||||
- '*'
|
||||
urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
|
||||
services:
|
||||
- '2'
|
||||
- '4'
|
||||
|
297
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
297
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
@ -0,0 +1,297 @@
|
||||
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-c20:1:
|
||||
prop.10.6:
|
||||
unit: none
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1:
|
||||
prop.10.6:
|
||||
unit: none
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1:
|
||||
prop.10.6:
|
||||
unit: none
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1
|
||||
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-conditioner:0000A004:xiaomi-m9:7: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1:
|
||||
prop.10.6:
|
||||
unit: none
|
||||
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1
|
||||
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
|
||||
prop.2.5:
|
||||
name: voc-density
|
||||
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:1: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
|
||||
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:2: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
|
||||
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3:
|
||||
prop.3.5:
|
||||
expr: (src_value*6)
|
||||
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-m1t:1:
|
||||
prop.2.3:
|
||||
name: current-position-a
|
||||
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-p33:1:
|
||||
prop.2.2:
|
||||
name: fan-level-a
|
||||
prop.2.6:
|
||||
name: fan-level
|
||||
access:
|
||||
- read
|
||||
- write
|
||||
- notify
|
||||
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-p43:1:
|
||||
prop.2.2:
|
||||
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:xiaomi-p69:1:0000D062:
|
||||
prop.2.4:
|
||||
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:
|
||||
prop.2.1:
|
||||
name: access-mode
|
||||
access:
|
||||
- read
|
||||
- notify
|
||||
prop.2.2:
|
||||
name: ip-address
|
||||
icon: mdi:ip
|
||||
prop.2.3:
|
||||
name: wifi-ssid
|
||||
access:
|
||||
- read
|
||||
- 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:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
|
||||
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:1: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
|
||||
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:2: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
|
||||
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3:
|
||||
prop.3.1:
|
||||
name: on-ventilation
|
||||
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:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2
|
||||
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2:
|
||||
prop.2.8:
|
||||
name: people-number
|
||||
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:
|
||||
expr: round(src_value*6/1000000, 3)
|
||||
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
|
||||
prop.2.2:
|
||||
name: power-consumption
|
||||
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:
|
||||
prop.11.1:
|
||||
name: power-consumption
|
||||
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: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:
|
||||
prop.3.1:
|
||||
name: electric-power
|
||||
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:
|
||||
prop.2.1:
|
||||
name: download-speed
|
||||
icon: mdi:download
|
||||
unit: B/s
|
||||
prop.2.2:
|
||||
name: upload-speed
|
||||
icon: mdi:upload
|
||||
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:090615-x1tpm:1:0000D042:
|
||||
prop.27.3:
|
||||
name: light-on
|
||||
prop.27.4:
|
||||
name: light-fan-on
|
||||
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
|
@ -48,12 +48,19 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor import SensorStateClass
|
||||
from homeassistant.components.event import EventDeviceClass
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
EntityCategory,
|
||||
LIGHT_LUX,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfTemperature,
|
||||
UnitOfPressure,
|
||||
PERCENTAGE
|
||||
)
|
||||
|
||||
# pylint: disable=pointless-string-statement
|
||||
@ -96,7 +103,7 @@ from homeassistant.const import (
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_DEVICE_TRANS_MAP: dict = {
|
||||
'humidifier': {
|
||||
'required': {
|
||||
'humidifier': {
|
||||
@ -132,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'optional': {
|
||||
'properties': {'mode', 'target-humidity'}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'environment': {
|
||||
@ -158,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'continue-sweep',
|
||||
'stop-and-gocharge'
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
@ -172,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'required': {
|
||||
'properties': {
|
||||
'battery-level': {'read'}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
'entity': 'vacuum'
|
||||
},
|
||||
@ -190,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
},
|
||||
'optional': {
|
||||
'properties': {'target-humidity'}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
@ -219,6 +225,31 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'entity': 'air-conditioner'
|
||||
},
|
||||
'air-condition-outlet': 'air-conditioner',
|
||||
'thermostat': {
|
||||
'required': {
|
||||
'thermostat': {
|
||||
'required': {
|
||||
'properties': {
|
||||
'on': {'read', 'write'}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'properties': {
|
||||
'target-temperature', 'mode', 'fan-level',
|
||||
'temperature'}
|
||||
}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'environment': {
|
||||
'required': {},
|
||||
'optional': {
|
||||
'properties': {'temperature', 'relative-humidity'}
|
||||
}
|
||||
}
|
||||
},
|
||||
'entity': 'thermostat'
|
||||
},
|
||||
'heater': {
|
||||
'required': {
|
||||
'heater': {
|
||||
@ -229,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
},
|
||||
'optional': {
|
||||
'properties': {'target-temperature', 'heat-level'}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
@ -238,10 +269,58 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'optional': {
|
||||
'properties': {'temperature', 'relative-humidity'}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
'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'
|
||||
},
|
||||
}
|
||||
|
||||
"""SPEC_SERVICE_TRANS_MAP
|
||||
@ -259,11 +338,12 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'events': set<event instance name: str>,
|
||||
'actions': set<action instance name: str>
|
||||
},
|
||||
'entity': str
|
||||
'entity': str,
|
||||
'entity_category'?: str
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_SERVICE_TRANS_MAP: dict = {
|
||||
'light': {
|
||||
'required': {
|
||||
'properties': {
|
||||
@ -277,10 +357,23 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
},
|
||||
'entity': 'light'
|
||||
},
|
||||
'indicator-light': 'light',
|
||||
'ambient-light': 'light',
|
||||
'night-light': 'light',
|
||||
'white-light': 'light',
|
||||
'indicator-light': {
|
||||
'required': {
|
||||
'properties': {
|
||||
'on': {'read', 'write'}
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'properties': {
|
||||
'mode', 'brightness',
|
||||
}
|
||||
},
|
||||
'entity': 'light',
|
||||
'entity_category': EntityCategory.CONFIG
|
||||
},
|
||||
'fan': {
|
||||
'required': {
|
||||
'properties': {
|
||||
@ -295,6 +388,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
},
|
||||
'fan-control': 'fan',
|
||||
'ceiling-fan': 'fan',
|
||||
'air-fresh': 'fan',
|
||||
'air-purifier': 'fan',
|
||||
'water-heater': {
|
||||
'required': {
|
||||
'properties': {
|
||||
@ -302,7 +397,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'properties': {'on', 'temperature', 'target-temperature', 'mode'}
|
||||
'properties': {'temperature', 'target-temperature', 'mode'}
|
||||
},
|
||||
'entity': 'water_heater'
|
||||
},
|
||||
@ -314,12 +409,27 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
},
|
||||
'optional': {
|
||||
'properties': {
|
||||
'motor-control', 'status', 'current-position', 'target-position'
|
||||
'status', 'current-position', 'target-position'
|
||||
}
|
||||
},
|
||||
'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
|
||||
@ -334,121 +444,145 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'<property instance name>':{
|
||||
'device_class': str,
|
||||
'entity': str,
|
||||
'optional':{
|
||||
'state_class': str,
|
||||
'unit_of_measurement': str
|
||||
}
|
||||
'state_class'?: str,
|
||||
'unit_of_measurement'?: str
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_PROP_TRANS_MAP: dict = {
|
||||
'entities': {
|
||||
'sensor': {
|
||||
'format': {'int', 'float'},
|
||||
'access': {'read'}
|
||||
},
|
||||
'binary_sensor': {
|
||||
'format': {'bool', 'int'},
|
||||
'access': {'read'}
|
||||
},
|
||||
'switch': {
|
||||
'format': {'bool'},
|
||||
'access': {'read', 'write'}
|
||||
}
|
||||
},
|
||||
'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': {
|
||||
'device_class': SensorDeviceClass.TEMPERATURE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfTemperature.CELSIUS
|
||||
},
|
||||
'relative-humidity': {
|
||||
'device_class': SensorDeviceClass.HUMIDITY,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
},
|
||||
'air-quality-index': {
|
||||
'device_class': SensorDeviceClass.AQI,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
'pm2.5-density': {
|
||||
'device_class': SensorDeviceClass.PM25,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'pm10-density': {
|
||||
'device_class': SensorDeviceClass.PM10,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'pm1': {
|
||||
'device_class': SensorDeviceClass.PM1,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'atmospheric-pressure': {
|
||||
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPressure.PA
|
||||
},
|
||||
'tvoc-density': {
|
||||
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT
|
||||
},
|
||||
'voc-density': {
|
||||
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT
|
||||
},
|
||||
'voc-density': 'tvoc-density',
|
||||
'battery-level': {
|
||||
'device_class': SensorDeviceClass.BATTERY,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
},
|
||||
'voltage': {
|
||||
'device_class': SensorDeviceClass.VOLTAGE,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
},
|
||||
'electric-current': {
|
||||
'device_class': SensorDeviceClass.CURRENT,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
},
|
||||
'illumination': {
|
||||
'device_class': SensorDeviceClass.ILLUMINANCE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': LIGHT_LUX
|
||||
},
|
||||
'no-one-determine-time': {
|
||||
'device_class': SensorDeviceClass.DURATION,
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time',
|
||||
'electric-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
},
|
||||
'electric-current': {
|
||||
'device_class': SensorDeviceClass.CURRENT,
|
||||
'surge-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
},
|
||||
'power-consumption': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
},
|
||||
'power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
},
|
||||
'total-battery': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time'
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class Number(MIoTPropertyEntity, NumberEntity):
|
||||
if self.spec.external_unit:
|
||||
self._attr_native_unit_of_measurement = self.spec.external_unit
|
||||
# Set icon
|
||||
if self.spec.icon:
|
||||
if self.spec.icon and not self.device_class:
|
||||
self._attr_icon = self.spec.icon
|
||||
# Set value range
|
||||
if self._value_range:
|
||||
|
@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
# Set device_class
|
||||
if self._value_list:
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_icon = 'mdi:message-text'
|
||||
self._attr_icon = 'mdi:format-text'
|
||||
self._attr_native_unit_of_measurement = None
|
||||
self._attr_options = self._value_list.descriptions
|
||||
else:
|
||||
@ -109,11 +109,14 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
self._attr_device_class, None) # type: ignore
|
||||
self._attr_native_unit_of_measurement = list(
|
||||
unit_sets)[0] if unit_sets else None
|
||||
# Set suggested precision
|
||||
if spec.format_ in {int, float} and spec.expr is None:
|
||||
self._attr_suggested_display_precision = spec.precision
|
||||
# Set state_class
|
||||
if spec.state_class:
|
||||
self._attr_state_class = spec.state_class
|
||||
# Set icon
|
||||
if spec.icon:
|
||||
if spec.icon and not self.device_class:
|
||||
self._attr_icon = spec.icon
|
||||
|
||||
@property
|
||||
|
@ -52,25 +52,22 @@ from typing import Any, Optional
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
ATTR_TEMPERATURE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature
|
||||
)
|
||||
from homeassistant.components.water_heater import (STATE_ON, STATE_OFF,
|
||||
ATTR_TEMPERATURE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature)
|
||||
|
||||
from .miot.const import DOMAIN
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||
from .miot.miot_spec import MIoTSpecProperty
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||
@ -79,8 +76,8 @@ async def async_setup_entry(
|
||||
new_entities = []
|
||||
for miot_device in device_list:
|
||||
for data in miot_device.entity_list.get('water_heater', []):
|
||||
new_entities.append(WaterHeater(
|
||||
miot_device=miot_device, entity_data=data))
|
||||
new_entities.append(
|
||||
WaterHeater(miot_device=miot_device, entity_data=data))
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
@ -95,9 +92,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
|
||||
_mode_map: Optional[dict[Any, Any]]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
) -> None:
|
||||
def __init__(self, miot_device: MIoTDevice,
|
||||
entity_data: MIoTEntityData) -> None:
|
||||
"""Initialize the Water heater."""
|
||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||
self._attr_temperature_unit = None
|
||||
@ -112,20 +108,17 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
for prop in entity_data.props:
|
||||
# on
|
||||
if prop.name == 'on':
|
||||
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
|
||||
self._prop_on = prop
|
||||
# temperature
|
||||
if prop.name == 'temperature':
|
||||
if prop.value_range:
|
||||
if (
|
||||
self._attr_temperature_unit is None
|
||||
and prop.external_unit
|
||||
):
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._prop_temp = prop
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'invalid temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
if not prop.value_range:
|
||||
_LOGGER.error('invalid temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
if prop.external_unit:
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._prop_temp = prop
|
||||
# target-temperature
|
||||
if prop.name == 'target-temperature':
|
||||
if not prop.value_range:
|
||||
@ -135,7 +128,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
continue
|
||||
self._attr_min_temp = prop.value_range.min_
|
||||
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:
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._attr_supported_features |= (
|
||||
@ -144,8 +137,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
# mode
|
||||
if prop.name == 'mode':
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'mode value_list is None, %s', self.entity_id)
|
||||
_LOGGER.error('mode value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_operation_list = list(self._mode_map.values())
|
||||
@ -165,14 +157,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
await self.set_property_async(prop=self._prop_on, value=False)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the temperature the water heater should heat water to."""
|
||||
await self.set_property_async(
|
||||
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
|
||||
"""Set the target 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:
|
||||
"""Set the operation mode of the water heater.
|
||||
Must be in the operation_list.
|
||||
"""
|
||||
"""Set the operation mode of the water heater."""
|
||||
if operation_mode == STATE_OFF:
|
||||
await self.set_property_async(prop=self._prop_on, value=False)
|
||||
return
|
||||
@ -180,35 +170,32 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
return
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_on, value=True, update=False)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_mode,
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map,
|
||||
value=operation_mode))
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Set the water heater to away mode."""
|
||||
await self.hass.async_add_executor_job(self.turn_away_mode_on)
|
||||
await self.set_property_async(prop=self._prop_on,
|
||||
value=True,
|
||||
write_ha_state=False)
|
||||
await self.set_property_async(prop=self._prop_mode,
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map,
|
||||
value=operation_mode))
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> Optional[float]:
|
||||
"""Return the current temperature."""
|
||||
return self.get_prop_value(prop=self._prop_temp)
|
||||
"""The current temperature."""
|
||||
return (None if self._prop_temp is None else self.get_prop_value(
|
||||
prop=self._prop_temp))
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
"""Return the target temperature."""
|
||||
return self.get_prop_value(prop=self._prop_target_temp)
|
||||
"""The target temperature."""
|
||||
return (None if self._prop_target_temp is None else self.get_prop_value(
|
||||
prop=self._prop_target_temp))
|
||||
|
||||
@property
|
||||
def current_operation(self) -> Optional[str]:
|
||||
"""Return the current mode."""
|
||||
"""The current mode."""
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
return STATE_OFF
|
||||
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
|
||||
return STATE_ON
|
||||
return self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
return self.get_map_value(map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
@ -98,6 +98,8 @@ footer :(可选)关联的 issue 或 pull request 编号。
|
||||
|
||||
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
|
||||
|
||||
当您第一次提交拉取请求时,GitHub Action 会提示您签署贡献者许可协议(Contributor License Agreement,CLA)。只有签署了 CLA ,本项目才会合入您的拉取请求。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。
|
||||
|
@ -33,9 +33,11 @@ git checkout v1.0.0
|
||||
|
||||
### 方法 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 商店,敬请期待。
|
||||
[](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) 手动安装
|
||||
|
||||
@ -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/config_flow_start/?domain=xiaomi_home)
|
||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
|
||||
|
||||
### 添加 MIoT 设备
|
||||
|
||||
@ -59,7 +61,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
|
||||
|
||||
方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录
|
||||
|
||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||
|
||||
### 修改配置项
|
||||
|
||||
@ -353,7 +355,7 @@ instance code 为 MIoT-Spec-V2 实例代码,格式如下:
|
||||
```
|
||||
service:<siid> # 服务
|
||||
service:<siid>:property:<piid> # 属性
|
||||
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的值
|
||||
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引值
|
||||
service:<siid>:event:<eiid> # 事件
|
||||
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` 路径下的任何文件(`spec_filter.py`、`spec_modify.json`、`multi_lang.json`等),需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则
|
||||
|
||||
## 文档
|
||||
|
||||
|
@ -15,11 +15,15 @@ TRANS_RELATIVE_PATH: str = path.join(
|
||||
MIOT_I18N_RELATIVE_PATH: str = path.join(
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
|
||||
SPEC_BOOL_TRANS_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
|
||||
SPEC_FILTER_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
|
||||
ROOT_PATH, '../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(
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
|
||||
|
||||
|
||||
def load_json_file(file_path: str) -> Optional[dict]:
|
||||
@ -27,7 +31,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return json.load(file)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.info('%s is not found.', file_path,)
|
||||
_LOGGER.info('%s is not found.', file_path)
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
_LOGGER.info('%s is not a valid JSON file.', file_path)
|
||||
@ -36,7 +40,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
||||
|
||||
def save_json_file(file_path: str, data: dict) -> None:
|
||||
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]:
|
||||
@ -53,8 +57,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||
|
||||
def save_yaml_file(file_path: str, data: dict) -> None:
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
yaml.safe_dump(
|
||||
data, file, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
yaml.safe_dump(data,
|
||||
file,
|
||||
default_flow_style=False,
|
||||
allow_unicode=True,
|
||||
indent=2,
|
||||
sort_keys=False)
|
||||
|
||||
|
||||
def dict_str_str(d: dict) -> bool:
|
||||
@ -128,37 +136,156 @@ def bool_trans(d: dict) -> bool:
|
||||
for key, trans in d['translate'].items():
|
||||
trans_keys: set[str] = set(trans.keys())
|
||||
if set(trans.keys()) != default_keys:
|
||||
_LOGGER.info(
|
||||
'bool trans inconsistent, %s, %s, %s',
|
||||
key, default_keys, trans_keys)
|
||||
_LOGGER.info('bool trans inconsistent, %s, %s, %s', 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 True
|
||||
|
||||
|
||||
def spec_modify(data: dict) -> bool:
|
||||
"""dict[str, str | dict[str, dict]]"""
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
for urn, content in data.items():
|
||||
if not isinstance(urn, str) or not isinstance(content, (dict, str)):
|
||||
return False
|
||||
if isinstance(content, str):
|
||||
continue
|
||||
for key, value in content.items():
|
||||
if not isinstance(key, str) or not isinstance(value, dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||
_LOGGER.info('invalid type')
|
||||
return False
|
||||
if dict1.keys() != dict2.keys():
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys())
|
||||
_LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),
|
||||
dict2.keys())
|
||||
return False
|
||||
for key in dict1:
|
||||
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
||||
if not compare_dict_structure(dict1[key], dict2[key]):
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, dict, %s', key)
|
||||
_LOGGER.info('inconsistent key values, dict, %s', key)
|
||||
return False
|
||||
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
||||
if not all(
|
||||
isinstance(i, type(j))
|
||||
for i, j in zip(dict1[key], dict2[key])):
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, list, %s', key)
|
||||
_LOGGER.info('inconsistent key values, list, %s', key)
|
||||
return False
|
||||
elif not isinstance(dict1[key], type(dict2[key])):
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, type, %s', key)
|
||||
_LOGGER.info('inconsistent key values, type, %s', key)
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -181,6 +308,22 @@ def sort_spec_filter(file_path: str):
|
||||
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):
|
||||
filter_data = load_yaml_file(file_path=file_path)
|
||||
assert isinstance(filter_data, dict), f'{file_path} format error'
|
||||
return dict(sorted(filter_data.items()))
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_bool_trans():
|
||||
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
|
||||
@ -197,6 +340,30 @@ def test_spec_filter():
|
||||
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
|
||||
def test_spec_modify():
|
||||
data = load_yaml_file(SPEC_MODIFY_FILE)
|
||||
assert isinstance(data, dict)
|
||||
assert data, f'load {SPEC_MODIFY_FILE} failed'
|
||||
assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_i18n():
|
||||
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
||||
@ -222,7 +389,8 @@ def test_miot_lang_integrity():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from miot.const import INTEGRATION_LANGUAGES
|
||||
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))
|
||||
assert len(translations_names) == len(integration_lang_list)
|
||||
assert translations_names == set(integration_lang_list)
|
||||
@ -238,21 +406,18 @@ def test_miot_lang_integrity():
|
||||
default_dict = load_json_file(
|
||||
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
|
||||
for name in list(integration_lang_list)[1:]:
|
||||
compare_dict = load_json_file(
|
||||
path.join(TRANS_RELATIVE_PATH, name))
|
||||
compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
_LOGGER.info(
|
||||
'compare_dict_structure failed /translations, %s', name)
|
||||
_LOGGER.info('compare_dict_structure failed /translations, %s',
|
||||
name)
|
||||
assert False
|
||||
# Check i18n files structure
|
||||
default_dict = load_json_file(
|
||||
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
|
||||
for name in list(integration_lang_list)[1:]:
|
||||
compare_dict = load_json_file(
|
||||
path.join(MIOT_I18N_RELATIVE_PATH, name))
|
||||
compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
_LOGGER.info(
|
||||
'compare_dict_structure failed /miot/i18n, %s', name)
|
||||
_LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)
|
||||
assert False
|
||||
|
||||
|
||||
@ -270,12 +435,27 @@ def test_miot_data_sort():
|
||||
f'{SPEC_BOOL_TRANS_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_FILTER_FILE)) == json.dumps(
|
||||
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
|
||||
f'{SPEC_FILTER_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_FILTER_FILE)) == json.dumps(
|
||||
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
|
||||
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
|
||||
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
|
||||
@ -286,3 +466,11 @@ def test_sort_spec_data():
|
||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||
_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)
|
||||
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
|
||||
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)
|
||||
|
@ -56,7 +56,7 @@ async def test_lan_async(test_devices: dict):
|
||||
|
||||
# Your central hub gateway did
|
||||
test_did = '111111'
|
||||
# Your central hub gateway did
|
||||
# Your central hub gateway token
|
||||
test_token = '11223344556677d9a03d43936fc384205'
|
||||
test_model = 'xiaomi.gateway.hub1'
|
||||
# Your computer interface list, such as enp3s0, wlp5s0
|
||||
@ -152,3 +152,5 @@ async def test_lan_async(test_devices: dict):
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
await miot_lan.deinit_async()
|
||||
await mips_service.deinit_async()
|
||||
await miot_network.deinit_async()
|
||||
|
Reference in New Issue
Block a user