mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-09-18 04:03:21 +08:00
Compare commits
100 Commits
Author | SHA1 | Date | |
---|---|---|---|
7c97b85f02 | |||
3f2c2a648b | |||
f11b2f2f68 | |||
f3abbef94c | |||
dae63657d7 | |||
3cc66450bd | |||
8cbb451153 | |||
0fee02ae5c | |||
580ff87e7f | |||
94583a23d1 | |||
925cf3b90f | |||
836bd01ead | |||
4ad040d2ea | |||
981429670a | |||
a82fd86c60 | |||
df3faea257 | |||
e09676661c | |||
aebeaf0245 | |||
4c2e10038c | |||
9afc62f39a | |||
b46805b92c | |||
a43447ef61 | |||
e5165f34da | |||
9fbbb26d33 | |||
5b1d003bb2 | |||
6069eaaba8 | |||
fd57e7c565 | |||
096b33f3c9 | |||
664787ca58 | |||
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 |
6
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -1,12 +1,12 @@
|
|||||||
name: Bug Report / 报告问题
|
name: Bug Report / 报告问题
|
||||||
description: Create a report to help us improve. / 报告问题以帮助我们改进
|
description: Create a report to help us improve. / 报告问题以帮助我们改进
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the Bug / 描述问题
|
label: Describe the Bug / 描述问题
|
||||||
description: |
|
description: |
|
||||||
> A clear and concise description of what the bug is.
|
> A clear and concise description of what the bug is. Please include the device model information (Like xiaomi.gateway.hub1 which can be found in Device info page).
|
||||||
> 清晰且简明地描述问题。
|
> 清晰且简明地描述问题。请注明设备 model 信息(例如 xiaomi.gateway.hub1,可在设备详情页查询)。
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
163
CHANGELOG.md
163
CHANGELOG.md
@ -1,4 +1,161 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
## v0.4.1
|
||||||
|
### Changed
|
||||||
|
- The setting option "Cover closed position" in CONFIGURE is changed to "Cover dead zone width". [#1301](https://github.com/XiaoMi/ha_xiaomi_home/pull/1301)
|
||||||
|
- Add an alongside switch entity for 090615.aircondition.ktf and juhl.aircondition.hvac. [#1303](https://github.com/XiaoMi/ha_xiaomi_home/pull/1303)
|
||||||
|
### Fixed
|
||||||
|
- Fix the vacuum status so that the vacuum activity will not always be idle. [#1299](https://github.com/XiaoMi/ha_xiaomi_home/pull/1299)
|
||||||
|
- Set the device on when the switch status is False or None. [#1303](https://github.com/XiaoMi/ha_xiaomi_home/pull/1303)
|
||||||
|
- Hide sensitive info in printing logs. [#1328](https://github.com/XiaoMi/ha_xiaomi_home/pull/1328)
|
||||||
|
- Fix the MIoT-Spec-V2 of cuco.plug.cp2d electric current, xiaomi.fan.p45 fan level, sanmei.valve.s1 power consumption, current and voltage, xiaomi.aircondition.c17, xiaomi.aircondition.m16 and xiaomi.airc.h40h00 humidity-range unit. [#1329](https://github.com/XiaoMi/ha_xiaomi_home/pull/1329)
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
### Added
|
||||||
|
- Add the watch as the device tracker entity. [#1189](https://github.com/XiaoMi/ha_xiaomi_home/pull/1189)
|
||||||
|
- Add the wifi speaker and the television as the media player entity. [#706](https://github.com/XiaoMi/ha_xiaomi_home/pull/706)
|
||||||
|
- Add an option in CONFIGURE to set the cover closed position. [#1242](https://github.com/XiaoMi/ha_xiaomi_home/pull/1242)
|
||||||
|
- Add notifications to show the status of the local connection to the central hub gateway. [#1280](https://github.com/XiaoMi/ha_xiaomi_home/pull/1280)
|
||||||
|
- Import the device from the third party cloud. [#1258](https://github.com/XiaoMi/ha_xiaomi_home/pull/1258)
|
||||||
|
### Changed
|
||||||
|
- Add an alongside switch entity for viomi.waterheater.m1. [#1255](https://github.com/XiaoMi/ha_xiaomi_home/pull/1255)
|
||||||
|
- Do not subscribe BLE device online/offline state message. [#1264](https://github.com/XiaoMi/ha_xiaomi_home/pull/1264)
|
||||||
|
### Fixed
|
||||||
|
- Keep the first element of the discovered ip address list as the recently added address when getting mdns result. [#1250](https://github.com/XiaoMi/ha_xiaomi_home/pull/1250)
|
||||||
|
- Subscribe local topics every time when connected to the central hub gateway. [#1266](https://github.com/XiaoMi/ha_xiaomi_home/pull/1266)
|
||||||
|
- Record the "closing" and "closed" status that occur frequently in the motor-controller, the window-opener and the curtain service. [#1262](https://github.com/XiaoMi/ha_xiaomi_home/pull/1262)
|
||||||
|
- Fix xiaomi.aircondition.c24 total power consumption unit, adp.motor.adswb4 motor switch, cgllc.airm.cgd1st environment temperature, and shhf.light.sflt11 fan switch status. [#1256](https://github.com/XiaoMi/ha_xiaomi_home/pull/1256)
|
||||||
|
|
||||||
|
## v0.3.4
|
||||||
|
### Added
|
||||||
|
- Exclude the unsupported device models. [#1205](https://github.com/XiaoMi/ha_xiaomi_home/pull/1205)
|
||||||
|
### Changed
|
||||||
|
- Subscribe the BLE device upstream messages even though the device is offline. [#1207](https://github.com/XiaoMi/ha_xiaomi_home/pull/1207)
|
||||||
|
- Record "opening", "closing" and "closed" status of the airer service that occur frequently and do not record "stop" status for the cover entity. [#1235](https://github.com/XiaoMi/ha_xiaomi_home/pull/1235)
|
||||||
|
- Modify README about spec_filter.yaml and the event attributes. [#1237](https://github.com/XiaoMi/ha_xiaomi_home/pull/1237)
|
||||||
|
### Fixed
|
||||||
|
- Fix the reconnect delay time to be reset when the client is connected to the broker. [#1200](https://github.com/XiaoMi/ha_xiaomi_home/pull/1200)
|
||||||
|
- Fix the HA warning in the logs related to vacuum state setting. [#694](https://github.com/XiaoMi/ha_xiaomi_home/pull/694)
|
||||||
|
- Fix the operation mode when the device does not have a mode property. [#1199](https://github.com/XiaoMi/ha_xiaomi_home/pull/1199)
|
||||||
|
- Fix 090615.aircondition.ktf environment temperature. [#1210](https://github.com/XiaoMi/ha_xiaomi_home/pull/1210)
|
||||||
|
- Fix a missing variable in translation it.json. [#1215](https://github.com/XiaoMi/ha_xiaomi_home/pull/1215)
|
||||||
|
- Fix yutai.plug.fsov8m power consumption and ignore bjkcz.curtain.kczble curtain status. [#1236](https://github.com/XiaoMi/ha_xiaomi_home/pull/1236)
|
||||||
|
|
||||||
|
## 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
|
## v0.1.5b2
|
||||||
### Added
|
### Added
|
||||||
@ -91,10 +248,10 @@ This version will cause some Xiaomi routers that do not support access (#564) to
|
|||||||
### Changed
|
### Changed
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59
|
- 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 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
|
- 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
|
- 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
|
## v0.1.0
|
||||||
### Added
|
### 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 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
|
## 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.
|
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.
|
||||||
|
66
README.md
66
README.md
@ -33,9 +33,11 @@ git checkout v1.0.0
|
|||||||
|
|
||||||
### Method 2: [HACS](https://hacs.xyz/)
|
### Method 2: [HACS](https://hacs.xyz/)
|
||||||
|
|
||||||
HACS > Overflow Menu > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > Xiaomi Home in New or Available for download section of HACS > DOWNLOAD
|
One-click installation from HACS:
|
||||||
|
|
||||||
> Xiaomi Home has not been added to the HACS store as a default yet. It's coming soon.
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
|
||||||
|
|
||||||
|
Or, HACS > In the search box, type **Xiaomi Home** > Click **Xiaomi Home**, getting into the detail page > DOWNLOAD
|
||||||
|
|
||||||
### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)
|
### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ Download and copy `custom_components/xiaomi_home` folder to `config/custom_compo
|
|||||||
|
|
||||||
[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account
|
[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account
|
||||||
|
|
||||||
[](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
|
### Add MIoT Devices
|
||||||
|
|
||||||
@ -59,7 +61,7 @@ After a Xiaomi account login and its user configuration are completed, you can c
|
|||||||
|
|
||||||
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account
|
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||||
|
|
||||||
### Update Configurations
|
### Update Configurations
|
||||||
|
|
||||||
@ -81,9 +83,9 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
- Does Xiaomi Home Integration support all Xiaomi Home devices?
|
- Does Xiaomi Home Integration support all Xiaomi smart devices?
|
||||||
|
|
||||||
Xiaomi Home Integration currently supports most categories of Home device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.
|
Xiaomi Home Integration currently supports most categories of the smart device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.
|
||||||
|
|
||||||
- Does Xiaomi Home Integration support multiple Xiaomi accounts?
|
- Does Xiaomi Home Integration support multiple Xiaomi accounts?
|
||||||
|
|
||||||
@ -91,7 +93,7 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
|
|||||||
|
|
||||||
- Does Xiaomi Home Integration support local control?
|
- Does Xiaomi Home Integration support local control?
|
||||||
|
|
||||||
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.4.0_0000 above) or Xiaomi home devices with built-in central hub gateway (software version 0.8.0 above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
|
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with built-in central hub gateway (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
|
||||||
|
|
||||||
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
|
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
|
||||||
|
|
||||||
@ -153,6 +155,8 @@ In MIoT-Spec-V2 protocol, a product is defined as a device. A device contains se
|
|||||||
|
|
||||||
MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.
|
MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.
|
||||||
|
|
||||||
|
MIoT-Spec-V2 event's arguments field is the list of parameters of the event. The list elements represent the piid of the property in the same service. For example, the [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057) of the Xiaomi Wireless Double-key Switch contains the siid=2 Switch Sensor service. The eiid=1014 Long Press event of the service is triggered when a button is long pressed. The debug level log will print `Press and hold, attributes: {'Button Type': 1}`. This is an example log that the button type is 1, which means the right button is long pressed.
|
||||||
|
|
||||||
- Action
|
- Action
|
||||||
|
|
||||||
| in | Entity in Home Assistant |
|
| in | Entity in Home Assistant |
|
||||||
@ -287,39 +291,37 @@ The value of the event instance name indicates `_attr_device_class` of the Home
|
|||||||
|
|
||||||
### MIoT-Spec-V2 Filter
|
### MIoT-Spec-V2 Filter
|
||||||
|
|
||||||
`spec_filter.json` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.
|
`spec_filter.yaml` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.
|
||||||
|
|
||||||
The format of `spec_filter.json` is as follows.
|
The format of `spec_filter.yaml` is as follows.
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
{
|
<MIoT-Spec-V2 device instance urn without the version field>:
|
||||||
"<MIoT-Spec-V2 device instance>":{
|
services: list<service_iid: str>
|
||||||
"services": list<service_iid: str>,
|
properties: list<service_iid.property_iid: str>
|
||||||
"properties": list<service_iid.property_iid: str>,
|
events: list<service_iid.event_iid: str>
|
||||||
"events": list<service_iid.event_iid: str>,
|
actions: list<service_iid.action_iid: str>
|
||||||
"actions": list<service_iid.action_iid: str>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The key of `spec_filter.json` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.json` does not need to specify the version number of MIoT-Spec-V2 device instance.
|
The key of `spec_filter.yaml` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.yaml` does not need to specify the version number of MIoT-Spec-V2 device instance.
|
||||||
|
|
||||||
The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.
|
The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
{
|
urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
|
||||||
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{
|
services:
|
||||||
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance.
|
- '*' # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2.
|
||||||
},
|
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
|
||||||
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
services:
|
||||||
"services": ["3"], # Filter out the service whose iid=3.
|
- '3' # Filter out the siid=3 service.
|
||||||
"properties": ["4.*"] # Filter out all properties in the service whose iid=4.
|
properties:
|
||||||
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service.
|
- '4.*' # Filter out all properties in the siid=4 service.
|
||||||
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service.
|
events:
|
||||||
}
|
- '4.1' # Filter out the eiid=1 event in the siid=4 service.
|
||||||
}
|
actions:
|
||||||
|
- '4.1' # Filter out the aiid=1 action in the siid=4 service.
|
||||||
```
|
```
|
||||||
|
|
||||||
Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.
|
Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.
|
||||||
@ -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.yaml`, `spec_modify.yaml`, `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
|
## Documents
|
||||||
|
|
||||||
|
@ -156,7 +156,8 @@ async def async_setup_entry(
|
|||||||
device.entity_list[platform].remove(entity)
|
device.entity_list[platform].remove(entity)
|
||||||
entity_id = device.gen_service_entity_id(
|
entity_id = device.gen_service_entity_id(
|
||||||
ha_domain=platform,
|
ha_domain=platform,
|
||||||
siid=entity.spec.iid) # type: ignore
|
siid=entity.spec.iid,
|
||||||
|
description=entity.spec.description)
|
||||||
if er.async_get(entity_id_or_uuid=entity_id):
|
if er.async_get(entity_id_or_uuid=entity_id):
|
||||||
er.async_remove(entity_id=entity_id)
|
er.async_remove(entity_id=entity_id)
|
||||||
if platform in device.prop_list:
|
if platform in device.prop_list:
|
||||||
|
@ -89,4 +89,8 @@ class BinarySensor(MIoTPropertyEntity, BinarySensorEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""On/Off state. True if the binary sensor is on, False otherwise."""
|
"""On/Off state. True if the binary sensor is on, False otherwise."""
|
||||||
|
if self.spec.name == 'contact-state':
|
||||||
|
return self._value is False
|
||||||
|
elif self.spec.name == 'occupancy-status':
|
||||||
|
return bool(self._value)
|
||||||
return self._value is True
|
return self._value is True
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -75,6 +75,9 @@ from .miot.const import (
|
|||||||
DEFAULT_CLOUD_SERVER,
|
DEFAULT_CLOUD_SERVER,
|
||||||
DEFAULT_CTRL_MODE,
|
DEFAULT_CTRL_MODE,
|
||||||
DEFAULT_INTEGRATION_LANGUAGE,
|
DEFAULT_INTEGRATION_LANGUAGE,
|
||||||
|
DEFAULT_COVER_DEAD_ZONE_WIDTH,
|
||||||
|
MIN_COVER_DEAD_ZONE_WIDTH,
|
||||||
|
MAX_COVER_DEAD_ZONE_WIDTH,
|
||||||
DEFAULT_NICK_NAME,
|
DEFAULT_NICK_NAME,
|
||||||
DEFAULT_OAUTH2_API_HOST,
|
DEFAULT_OAUTH2_API_HOST,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -129,6 +132,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
_cloud_server: str
|
_cloud_server: str
|
||||||
_integration_language: str
|
_integration_language: str
|
||||||
|
_cover_dz_width: int
|
||||||
_auth_info: dict
|
_auth_info: dict
|
||||||
_nick_name: str
|
_nick_name: str
|
||||||
_home_selected: dict
|
_home_selected: dict
|
||||||
@ -151,6 +155,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._main_loop = asyncio.get_running_loop()
|
self._main_loop = asyncio.get_running_loop()
|
||||||
self._cloud_server = DEFAULT_CLOUD_SERVER
|
self._cloud_server = DEFAULT_CLOUD_SERVER
|
||||||
self._integration_language = DEFAULT_INTEGRATION_LANGUAGE
|
self._integration_language = DEFAULT_INTEGRATION_LANGUAGE
|
||||||
|
self._cover_dz_width = DEFAULT_COVER_DEAD_ZONE_WIDTH
|
||||||
self._storage_path = ''
|
self._storage_path = ''
|
||||||
self._virtual_did = ''
|
self._virtual_did = ''
|
||||||
self._uid = ''
|
self._uid = ''
|
||||||
@ -565,27 +570,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
home_list = {}
|
home_list = {}
|
||||||
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
||||||
# home list
|
# home list
|
||||||
for home_id, home_info in self._cc_home_info[
|
for device_source in ['home_list','share_home_list',
|
||||||
'homes']['home_list'].items():
|
'separated_shared_list']:
|
||||||
# i18n
|
if device_source not in self._cc_home_info['homes']:
|
||||||
tip_central = ''
|
continue
|
||||||
group_id = home_info.get('group_id', None)
|
for home_id, home_info in self._cc_home_info[
|
||||||
dev_list = {
|
'homes'][device_source].items():
|
||||||
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
|
# i18n
|
||||||
tip_central = self._miot_i18n.translate(
|
tip_central = ''
|
||||||
key='config.other.found_central_gateway')
|
group_id = home_info.get('group_id', None)
|
||||||
home_info['central_did'] = mips_list[group_id].get('did', None)
|
dev_list = {
|
||||||
home_list[home_id] = (
|
device['did']: device
|
||||||
f'{home_info["home_name"]} '
|
for device in list(self._cc_home_info['devices'].values())
|
||||||
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
|
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()))
|
self._cc_home_list_show = dict(sorted(home_list.items()))
|
||||||
|
|
||||||
@ -660,10 +670,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if not home_selected:
|
if not home_selected:
|
||||||
return await self.__show_homes_select_form(
|
return await self.__show_homes_select_form(
|
||||||
'no_family_selected')
|
'no_family_selected')
|
||||||
for home_id, home_info in self._cc_home_info[
|
for device_source in ['home_list','share_home_list',
|
||||||
'homes']['home_list'].items():
|
'separated_shared_list']:
|
||||||
if home_id in home_selected:
|
if device_source not in self._cc_home_info['homes']:
|
||||||
self._home_selected[home_id] = home_info
|
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(
|
self._area_name_rule = user_input.get(
|
||||||
'area_name_rule', self._area_name_rule)
|
'area_name_rule', self._area_name_rule)
|
||||||
# Storage device list
|
# Storage device list
|
||||||
@ -942,6 +956,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
'action_debug': self._action_debug,
|
'action_debug': self._action_debug,
|
||||||
'hide_non_standard_entities':
|
'hide_non_standard_entities':
|
||||||
self._hide_non_standard_entities,
|
self._hide_non_standard_entities,
|
||||||
|
'cover_dead_zone_width': self._cover_dz_width,
|
||||||
'display_binary_mode': self._display_binary_mode,
|
'display_binary_mode': self._display_binary_mode,
|
||||||
'display_devices_changed_notify':
|
'display_devices_changed_notify':
|
||||||
self._display_devices_changed_notify
|
self._display_devices_changed_notify
|
||||||
@ -986,6 +1001,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
_hide_non_standard_entities: bool
|
_hide_non_standard_entities: bool
|
||||||
_display_binary_mode: list[str]
|
_display_binary_mode: list[str]
|
||||||
_display_devs_notify: list[str]
|
_display_devs_notify: list[str]
|
||||||
|
_cover_dz_width: int
|
||||||
|
|
||||||
_oauth_redirect_url_full: str
|
_oauth_redirect_url_full: str
|
||||||
_auth_info: dict
|
_auth_info: dict
|
||||||
@ -1006,6 +1022,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
_opt_lan_ctrl_cfg: bool
|
_opt_lan_ctrl_cfg: bool
|
||||||
_opt_network_detect_cfg: bool
|
_opt_network_detect_cfg: bool
|
||||||
_opt_check_network_deps: bool
|
_opt_check_network_deps: bool
|
||||||
|
_cover_width_new: int
|
||||||
|
|
||||||
_trans_rules_count: int
|
_trans_rules_count: int
|
||||||
_trans_rules_count_success: int
|
_trans_rules_count_success: int
|
||||||
@ -1034,6 +1051,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)
|
self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)
|
||||||
self._integration_language = self._entry_data.get(
|
self._integration_language = self._entry_data.get(
|
||||||
'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
|
'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
|
||||||
|
self._cover_dz_width = self._entry_data.get(
|
||||||
|
'cover_dead_zone_width', DEFAULT_COVER_DEAD_ZONE_WIDTH)
|
||||||
self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)
|
self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)
|
||||||
self._action_debug = self._entry_data.get('action_debug', False)
|
self._action_debug = self._entry_data.get('action_debug', False)
|
||||||
self._hide_non_standard_entities = self._entry_data.get(
|
self._hide_non_standard_entities = self._entry_data.get(
|
||||||
@ -1059,6 +1078,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self._action_debug_new = False
|
self._action_debug_new = False
|
||||||
self._hide_non_standard_entities_new = False
|
self._hide_non_standard_entities_new = False
|
||||||
self._display_binary_mode_new = []
|
self._display_binary_mode_new = []
|
||||||
|
self._cover_width_new = self._cover_dz_width
|
||||||
self._update_user_info = False
|
self._update_user_info = False
|
||||||
self._update_devices = False
|
self._update_devices = False
|
||||||
self._update_trans_rules = False
|
self._update_trans_rules = False
|
||||||
@ -1331,6 +1351,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
): cv.multi_select(
|
): cv.multi_select(
|
||||||
self._miot_i18n.translate(
|
self._miot_i18n.translate(
|
||||||
'config.binary_mode')), # type: ignore
|
'config.binary_mode')), # type: ignore
|
||||||
|
vol.Optional(
|
||||||
|
'cover_dead_zone_width',
|
||||||
|
default=self._cover_dz_width # type: ignore
|
||||||
|
): vol.All(vol.Coerce(int), vol.Range(
|
||||||
|
min=MIN_COVER_DEAD_ZONE_WIDTH,
|
||||||
|
max=MAX_COVER_DEAD_ZONE_WIDTH)),
|
||||||
vol.Required(
|
vol.Required(
|
||||||
'update_trans_rules',
|
'update_trans_rules',
|
||||||
default=self._update_trans_rules # type: ignore
|
default=self._update_trans_rules # type: ignore
|
||||||
@ -1369,6 +1395,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)
|
'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)
|
||||||
self._opt_network_detect_cfg = user_input.get(
|
self._opt_network_detect_cfg = user_input.get(
|
||||||
'network_detect_config', self._opt_network_detect_cfg)
|
'network_detect_config', self._opt_network_detect_cfg)
|
||||||
|
self._cover_width_new = user_input.get(
|
||||||
|
'cover_dead_zone_width', self._cover_dz_width)
|
||||||
|
|
||||||
return await self.async_step_update_user_info()
|
return await self.async_step_update_user_info()
|
||||||
|
|
||||||
@ -1420,27 +1448,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
home_list = {}
|
home_list = {}
|
||||||
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
tip_devices = self._miot_i18n.translate(key='config.other.devices')
|
||||||
# home list
|
# home list
|
||||||
for home_id, home_info in self._cc_home_info[
|
for device_source in ['home_list','share_home_list',
|
||||||
'homes']['home_list'].items():
|
'separated_shared_list']:
|
||||||
# i18n
|
if device_source not in self._cc_home_info['homes']:
|
||||||
tip_central = ''
|
continue
|
||||||
group_id = home_info.get('group_id', None)
|
for home_id, home_info in self._cc_home_info[
|
||||||
did_list = {
|
'homes'][device_source].items():
|
||||||
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
|
# i18n
|
||||||
tip_central = self._miot_i18n.translate(
|
tip_central = ''
|
||||||
key='config.other.found_central_gateway')
|
group_id = home_info.get('group_id', None)
|
||||||
home_info['central_did'] = mips_list[group_id].get(
|
did_list = {
|
||||||
'did', None)
|
device['did']: device for device in list(
|
||||||
home_list[home_id] = (
|
self._cc_home_info['devices'].values())
|
||||||
f'{home_info["home_name"]} '
|
if device.get('home_id', None) == home_id}
|
||||||
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
|
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
|
# Remove deleted item
|
||||||
self._home_selected_list = [
|
self._home_selected_list = [
|
||||||
home_id for home_id in self._home_selected_list
|
home_id for home_id in self._home_selected_list
|
||||||
@ -1460,10 +1492,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
return await self.__show_homes_select_form('no_family_selected')
|
return await self.__show_homes_select_form('no_family_selected')
|
||||||
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
|
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
|
||||||
self._home_selected = {}
|
self._home_selected = {}
|
||||||
for home_id, home_info in self._cc_home_info[
|
for device_source in ['home_list','share_home_list',
|
||||||
'homes']['home_list'].items():
|
'separated_shared_list']:
|
||||||
if home_id in self._home_selected_list:
|
if device_source not in self._cc_home_info['homes']:
|
||||||
self._home_selected[home_id] = home_info
|
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
|
# Get device list
|
||||||
device_list: dict = {
|
device_list: dict = {
|
||||||
did: dev_info
|
did: dev_info
|
||||||
@ -1909,6 +1945,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
'nick_name': self._nick_name,
|
'nick_name': self._nick_name,
|
||||||
'lang_new': INTEGRATION_LANGUAGES[self._lang_new],
|
'lang_new': INTEGRATION_LANGUAGES[self._lang_new],
|
||||||
'nick_name_new': self._nick_name_new,
|
'nick_name_new': self._nick_name_new,
|
||||||
|
'cover_width_new': self._cover_width_new,
|
||||||
'devices_add': len(self._devices_add),
|
'devices_add': len(self._devices_add),
|
||||||
'devices_remove': len(self._devices_remove),
|
'devices_remove': len(self._devices_remove),
|
||||||
'trans_rules_count': self._trans_rules_count,
|
'trans_rules_count': self._trans_rules_count,
|
||||||
@ -1935,6 +1972,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
if self._lang_new != self._integration_language:
|
if self._lang_new != self._integration_language:
|
||||||
self._entry_data['integration_language'] = self._lang_new
|
self._entry_data['integration_language'] = self._lang_new
|
||||||
self._need_reload = True
|
self._need_reload = True
|
||||||
|
if self._cover_width_new != self._cover_dz_width:
|
||||||
|
self._entry_data['cover_dead_zone_width'] = self._cover_width_new
|
||||||
|
self._need_reload = True
|
||||||
if self._update_user_info:
|
if self._update_user_info:
|
||||||
self._entry_data['nick_name'] = self._nick_name_new
|
self._entry_data['nick_name'] = self._nick_name_new
|
||||||
if self._update_devices:
|
if self._update_devices:
|
||||||
|
@ -46,31 +46,26 @@ off Xiaomi or its affiliates' products.
|
|||||||
Cover entities for Xiaomi Home.
|
Cover entities for Xiaomi Home.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from typing import Any, Optional
|
||||||
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (ATTR_POSITION, CoverEntity,
|
||||||
ATTR_POSITION,
|
CoverEntityFeature,
|
||||||
CoverEntity,
|
CoverDeviceClass)
|
||||||
CoverEntityFeature,
|
|
||||||
CoverDeviceClass
|
|
||||||
)
|
|
||||||
|
|
||||||
from .miot.miot_spec import MIoTSpecProperty
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
from .miot.const import DOMAIN
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
|
||||||
hass: HomeAssistant,
|
async_add_entities: AddEntitiesCallback) -> None:
|
||||||
config_entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
config_entry.entry_id]
|
config_entry.entry_id]
|
||||||
@ -82,8 +77,12 @@ async def async_setup_entry(
|
|||||||
data.spec.device_class = CoverDeviceClass.CURTAIN
|
data.spec.device_class = CoverDeviceClass.CURTAIN
|
||||||
elif data.spec.name == 'window-opener':
|
elif data.spec.name == 'window-opener':
|
||||||
data.spec.device_class = CoverDeviceClass.WINDOW
|
data.spec.device_class = CoverDeviceClass.WINDOW
|
||||||
new_entities.append(
|
elif data.spec.name == 'motor-controller':
|
||||||
Cover(miot_device=miot_device, entity_data=data))
|
data.spec.device_class = CoverDeviceClass.SHUTTER
|
||||||
|
elif data.spec.name == 'airer':
|
||||||
|
data.spec.device_class = CoverDeviceClass.BLIND
|
||||||
|
new_entities.append(Cover(miot_device=miot_device,
|
||||||
|
entity_data=data))
|
||||||
|
|
||||||
if new_entities:
|
if new_entities:
|
||||||
async_add_entities(new_entities)
|
async_add_entities(new_entities)
|
||||||
@ -92,78 +91,105 @@ async def async_setup_entry(
|
|||||||
class Cover(MIoTServiceEntity, CoverEntity):
|
class Cover(MIoTServiceEntity, CoverEntity):
|
||||||
"""Cover entities for Xiaomi Home."""
|
"""Cover entities for Xiaomi Home."""
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
_cover_dead_zone_width: int
|
||||||
_prop_motor_control: Optional[MIoTSpecProperty]
|
_prop_motor_control: Optional[MIoTSpecProperty]
|
||||||
_prop_motor_value_open: Optional[int]
|
_prop_motor_value_open: Optional[int]
|
||||||
_prop_motor_value_close: Optional[int]
|
_prop_motor_value_close: Optional[int]
|
||||||
_prop_motor_value_pause: Optional[int]
|
_prop_motor_value_pause: Optional[int]
|
||||||
_prop_status: Optional[MIoTSpecProperty]
|
_prop_status: Optional[MIoTSpecProperty]
|
||||||
_prop_status_opening: Optional[int]
|
_prop_status_opening: Optional[list[int]]
|
||||||
_prop_status_closing: Optional[int]
|
_prop_status_closing: Optional[list[int]]
|
||||||
_prop_status_stop: Optional[int]
|
_prop_status_closed: Optional[list[int]]
|
||||||
_prop_current_position: Optional[MIoTSpecProperty]
|
_prop_current_position: Optional[MIoTSpecProperty]
|
||||||
_prop_target_position: Optional[MIoTSpecProperty]
|
_prop_target_position: Optional[MIoTSpecProperty]
|
||||||
_prop_position_value_min: Optional[int]
|
_prop_position_value_min: Optional[int]
|
||||||
_prop_position_value_max: Optional[int]
|
_prop_position_value_max: Optional[int]
|
||||||
_prop_position_value_range: Optional[int]
|
_prop_position_value_range: Optional[int]
|
||||||
|
_prop_pos_closing: bool
|
||||||
|
_prop_pos_opening: bool
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
entity_data: MIoTEntityData) -> None:
|
||||||
) -> None:
|
|
||||||
"""Initialize the Cover."""
|
"""Initialize the Cover."""
|
||||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
self._attr_device_class = entity_data.spec.device_class
|
self._attr_device_class = entity_data.spec.device_class
|
||||||
self._attr_supported_color_modes = set()
|
self._attr_supported_color_modes = set()
|
||||||
self._attr_supported_features = CoverEntityFeature(0)
|
self._attr_supported_features = CoverEntityFeature(0)
|
||||||
|
|
||||||
|
self._cover_dead_zone_width = (
|
||||||
|
miot_device.miot_client.cover_dead_zone_width)
|
||||||
|
|
||||||
self._prop_motor_control = None
|
self._prop_motor_control = None
|
||||||
self._prop_motor_value_open = None
|
self._prop_motor_value_open = None
|
||||||
self._prop_motor_value_close = None
|
self._prop_motor_value_close = None
|
||||||
self._prop_motor_value_pause = None
|
self._prop_motor_value_pause = None
|
||||||
self._prop_status = None
|
self._prop_status = None
|
||||||
self._prop_status_opening = None
|
self._prop_status_opening = []
|
||||||
self._prop_status_closing = None
|
self._prop_status_closing = []
|
||||||
self._prop_status_stop = None
|
self._prop_status_closed = []
|
||||||
self._prop_current_position = None
|
self._prop_current_position = None
|
||||||
self._prop_target_position = None
|
self._prop_target_position = None
|
||||||
self._prop_position_value_min = None
|
self._prop_position_value_min = None
|
||||||
self._prop_position_value_max = None
|
self._prop_position_value_max = None
|
||||||
self._prop_position_value_range = None
|
self._prop_position_value_range = None
|
||||||
|
self._prop_pos_closing = False
|
||||||
|
self._prop_pos_opening = False
|
||||||
|
|
||||||
# properties
|
# properties
|
||||||
for prop in entity_data.props:
|
for prop in entity_data.props:
|
||||||
if prop.name == 'motor-control':
|
if prop.name == 'motor-control':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('motor-control value_list is None, %s',
|
||||||
'motor-control value_list is None, %s', self.entity_id)
|
self.entity_id)
|
||||||
continue
|
continue
|
||||||
for item in prop.value_list.items:
|
for item in prop.value_list.items:
|
||||||
if item.name in {'open'}:
|
if item.name in {'open', 'up'}:
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
CoverEntityFeature.OPEN)
|
CoverEntityFeature.OPEN)
|
||||||
self._prop_motor_value_open = item.value
|
self._prop_motor_value_open = item.value
|
||||||
elif item.name in {'close'}:
|
elif item.name in {'close', 'down'}:
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
CoverEntityFeature.CLOSE)
|
CoverEntityFeature.CLOSE)
|
||||||
self._prop_motor_value_close = item.value
|
self._prop_motor_value_close = item.value
|
||||||
elif item.name in {'pause'}:
|
elif item.name in {'pause', 'stop'}:
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
CoverEntityFeature.STOP)
|
CoverEntityFeature.STOP)
|
||||||
self._prop_motor_value_pause = item.value
|
self._prop_motor_value_pause = item.value
|
||||||
self._prop_motor_control = prop
|
self._prop_motor_control = prop
|
||||||
elif prop.name == 'status':
|
elif prop.name == 'status':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('status value_list is None, %s',
|
||||||
'status value_list is None, %s', self.entity_id)
|
self.entity_id)
|
||||||
continue
|
continue
|
||||||
for item in prop.value_list.items:
|
for item in prop.value_list.items:
|
||||||
if item.name in {'opening', 'open'}:
|
item_str: str = item.name
|
||||||
self._prop_status_opening = item.value
|
item_name: str = re.sub(r'[^a-z]', '', item_str)
|
||||||
elif item.name in {'closing', 'close'}:
|
if item_name in {
|
||||||
self._prop_status_closing = item.value
|
'opening', 'open', 'up', 'uping', 'rise', 'rising'
|
||||||
elif item.name in {'stop', 'pause'}:
|
}:
|
||||||
self._prop_status_stop = item.value
|
self._prop_status_opening.append(item.value)
|
||||||
|
elif item_name in {
|
||||||
|
'closing', 'close', 'down', 'dowm', 'falling',
|
||||||
|
'fallin', 'dropping', 'downing', 'lower'
|
||||||
|
}:
|
||||||
|
self._prop_status_closing.append(item.value)
|
||||||
|
elif item_name in {
|
||||||
|
'closed', 'closeover', 'stopatlowest',
|
||||||
|
'stoplowerlimit', 'lowerlimitstop', 'floor',
|
||||||
|
'lowerlimit'
|
||||||
|
}:
|
||||||
|
self._prop_status_closed.append(item.value)
|
||||||
self._prop_status = prop
|
self._prop_status = prop
|
||||||
elif prop.name == 'current-position':
|
elif prop.name == 'current-position':
|
||||||
|
if not prop.value_range:
|
||||||
|
_LOGGER.error(
|
||||||
|
'invalid current-position value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._prop_position_value_min = prop.value_range.min_
|
||||||
|
self._prop_position_value_max = prop.value_range.max_
|
||||||
|
self._prop_position_value_range = (prop.value_range.max_ -
|
||||||
|
prop.value_range.min_)
|
||||||
self._prop_current_position = prop
|
self._prop_current_position = prop
|
||||||
elif prop.name == 'target-position':
|
elif prop.name == 'target-position':
|
||||||
if not prop.value_range:
|
if not prop.value_range:
|
||||||
@ -173,35 +199,65 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
|||||||
continue
|
continue
|
||||||
self._prop_position_value_min = prop.value_range.min_
|
self._prop_position_value_min = prop.value_range.min_
|
||||||
self._prop_position_value_max = prop.value_range.max_
|
self._prop_position_value_max = prop.value_range.max_
|
||||||
self._prop_position_value_range = (
|
self._prop_position_value_range = (prop.value_range.max_ -
|
||||||
self._prop_position_value_max -
|
prop.value_range.min_)
|
||||||
self._prop_position_value_min)
|
|
||||||
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
|
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
|
||||||
self._prop_target_position = prop
|
self._prop_target_position = prop
|
||||||
|
# For the device that has the current position property but no status
|
||||||
|
# property, the current position property will be used to determine the
|
||||||
|
# opening and the closing status.
|
||||||
|
if (self._prop_status is None) and (self._prop_current_position
|
||||||
|
is not None):
|
||||||
|
self.sub_prop_changed(self._prop_current_position,
|
||||||
|
self._position_changed_handler)
|
||||||
|
|
||||||
|
def _position_changed_handler(self, prop: MIoTSpecProperty,
|
||||||
|
ctx: Any) -> None:
|
||||||
|
self._prop_pos_closing = False
|
||||||
|
self._prop_pos_opening = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_open_cover(self, **kwargs) -> None:
|
async def async_open_cover(self, **kwargs) -> None:
|
||||||
"""Open the cover."""
|
"""Open the cover."""
|
||||||
await self.set_property_async(
|
current = None if (self._prop_current_position
|
||||||
self._prop_motor_control, self._prop_motor_value_open)
|
is None) else self.get_prop_value(
|
||||||
|
prop=self._prop_current_position)
|
||||||
|
if (current is not None) and (current < self._prop_position_value_max):
|
||||||
|
self._prop_pos_opening = True
|
||||||
|
self._prop_pos_closing = False
|
||||||
|
await self.set_property_async(self._prop_motor_control,
|
||||||
|
self._prop_motor_value_open)
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs) -> None:
|
async def async_close_cover(self, **kwargs) -> None:
|
||||||
"""Close the cover."""
|
"""Close the cover."""
|
||||||
await self.set_property_async(
|
current = None if (self._prop_current_position
|
||||||
self._prop_motor_control, self._prop_motor_value_close)
|
is None) else self.get_prop_value(
|
||||||
|
prop=self._prop_current_position)
|
||||||
|
if (current is not None) and (current > self._prop_position_value_min):
|
||||||
|
self._prop_pos_opening = False
|
||||||
|
self._prop_pos_closing = True
|
||||||
|
await self.set_property_async(self._prop_motor_control,
|
||||||
|
self._prop_motor_value_close)
|
||||||
|
|
||||||
async def async_stop_cover(self, **kwargs) -> None:
|
async def async_stop_cover(self, **kwargs) -> None:
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
await self.set_property_async(
|
self._prop_pos_opening = False
|
||||||
self._prop_motor_control, self._prop_motor_value_pause)
|
self._prop_pos_closing = False
|
||||||
|
await self.set_property_async(self._prop_motor_control,
|
||||||
|
self._prop_motor_value_pause)
|
||||||
|
|
||||||
async def async_set_cover_position(self, **kwargs) -> None:
|
async def async_set_cover_position(self, **kwargs) -> None:
|
||||||
"""Set the position of the cover."""
|
"""Set the position of the cover."""
|
||||||
pos = kwargs.get(ATTR_POSITION, None)
|
pos = kwargs.get(ATTR_POSITION, None)
|
||||||
if pos is None:
|
if pos is None:
|
||||||
return None
|
return None
|
||||||
pos = round(pos*self._prop_position_value_range/100)
|
current = self.current_cover_position
|
||||||
return await self.set_property_async(
|
if current is not None:
|
||||||
prop=self._prop_target_position, value=pos)
|
self._prop_pos_opening = pos > current
|
||||||
|
self._prop_pos_closing = pos < current
|
||||||
|
pos = round(pos * self._prop_position_value_range / 100)
|
||||||
|
await self.set_property_async(prop=self._prop_target_position,
|
||||||
|
value=pos)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_cover_position(self) -> Optional[int]:
|
def current_cover_position(self) -> Optional[int]:
|
||||||
@ -209,28 +265,53 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
|||||||
|
|
||||||
0: the cover is closed, 100: the cover is fully opened, None: unknown.
|
0: the cover is closed, 100: the cover is fully opened, None: unknown.
|
||||||
"""
|
"""
|
||||||
|
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)
|
pos = self.get_prop_value(prop=self._prop_current_position)
|
||||||
if pos is None:
|
if pos is None:
|
||||||
return None
|
return None
|
||||||
return round(pos*100/self._prop_position_value_range)
|
pos = round(pos*100/self._prop_position_value_range)
|
||||||
|
if pos <= self._cover_dead_zone_width:
|
||||||
|
pos = 0
|
||||||
|
elif pos >= (100 - self._cover_dead_zone_width):
|
||||||
|
pos = 100
|
||||||
|
return pos
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_opening(self) -> Optional[bool]:
|
def is_opening(self) -> Optional[bool]:
|
||||||
"""Return if the cover is opening."""
|
"""Return if the cover is opening."""
|
||||||
if self._prop_status is None:
|
if self._prop_status and self._prop_status_opening:
|
||||||
return None
|
return (self.get_prop_value(prop=self._prop_status)
|
||||||
return self.get_prop_value(
|
in self._prop_status_opening)
|
||||||
prop=self._prop_status) == self._prop_status_opening
|
# The status has higher priority when determining whether the cover
|
||||||
|
# is opening.
|
||||||
|
return self._prop_pos_opening
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closing(self) -> Optional[bool]:
|
def is_closing(self) -> Optional[bool]:
|
||||||
"""Return if the cover is closing."""
|
"""Return if the cover is closing."""
|
||||||
if self._prop_status is None:
|
if self._prop_status and self._prop_status_closing:
|
||||||
return None
|
return (self.get_prop_value(prop=self._prop_status)
|
||||||
return self.get_prop_value(
|
in self._prop_status_closing)
|
||||||
prop=self._prop_status) == self._prop_status_closing
|
# The status has higher priority when determining whether the cover
|
||||||
|
# is closing.
|
||||||
|
return self._prop_pos_closing
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self) -> Optional[bool]:
|
def is_closed(self) -> Optional[bool]:
|
||||||
"""Return if the cover is closed."""
|
"""Return if the cover is closed."""
|
||||||
return self.get_prop_value(prop=self._prop_current_position) == 0
|
if self.current_cover_position is not None:
|
||||||
|
return self.current_cover_position == 0
|
||||||
|
# The current position is prior to the status when determining
|
||||||
|
# whether the cover is closed.
|
||||||
|
if self._prop_status and self._prop_status_closed:
|
||||||
|
return (self.get_prop_value(prop=self._prop_status)
|
||||||
|
in self._prop_status_closed)
|
||||||
|
return None
|
||||||
|
126
custom_components/xiaomi_home/device_tracker.py
Normal file
126
custom_components/xiaomi_home/device_tracker.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Device tracker entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.device_tracker import TrackerEntity
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
|
config_entry.entry_id]
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('device_tracker', []):
|
||||||
|
new_entities.append(
|
||||||
|
DeviceTracker(miot_device=miot_device, entity_data=data))
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTracker(MIoTServiceEntity, TrackerEntity):
|
||||||
|
"""Tracker entities for Xiaomi Home."""
|
||||||
|
_prop_battery_level: Optional[MIoTSpecProperty]
|
||||||
|
_prop_latitude: Optional[MIoTSpecProperty]
|
||||||
|
_prop_longitude: Optional[MIoTSpecProperty]
|
||||||
|
_prop_area_id: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._prop_battery_level = None
|
||||||
|
self._prop_latitude = None
|
||||||
|
self._prop_longitude = None
|
||||||
|
self._prop_area_id = None
|
||||||
|
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'battery-level':
|
||||||
|
self._prop_battery_level = prop
|
||||||
|
elif prop.name == 'latitude':
|
||||||
|
self._prop_latitude = prop
|
||||||
|
elif prop.name == 'longitude':
|
||||||
|
self._prop_longitude = prop
|
||||||
|
elif prop.name == 'area-id':
|
||||||
|
self._prop_area_id = prop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self) -> Optional[int]:
|
||||||
|
"""The battery level of the device."""
|
||||||
|
return None if (self._prop_battery_level
|
||||||
|
is None) else self.get_prop_value(
|
||||||
|
prop=self._prop_battery_level)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latitude(self) -> Optional[float]:
|
||||||
|
"""The latitude coordinate of the device."""
|
||||||
|
return None if self._prop_latitude is None else self.get_prop_value(
|
||||||
|
prop=self._prop_latitude)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def longitude(self) -> Optional[float]:
|
||||||
|
"""The longitude coordinate of the device."""
|
||||||
|
return None if self._prop_longitude is None else self.get_prop_value(
|
||||||
|
prop=self._prop_longitude)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_name(self) -> Optional[str]:
|
||||||
|
"""The location name of the device."""
|
||||||
|
return None if self._prop_area_id is None else self.get_prop_value(
|
||||||
|
prop=self._prop_area_id)
|
@ -46,6 +46,7 @@ off Xiaomi or its affiliates' products.
|
|||||||
Event entities for Xiaomi Home.
|
Event entities for Xiaomi Home.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -57,6 +58,8 @@ from .miot.miot_spec import MIoTSpecEvent
|
|||||||
from .miot.miot_device import MIoTDevice, MIoTEventEntity
|
from .miot.miot_device import MIoTDevice, MIoTEventEntity
|
||||||
from .miot.const import DOMAIN
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -89,4 +92,5 @@ class Event(MIoTEventEntity, EventEntity):
|
|||||||
self, name: str, arguments: dict[str, Any] | None = None
|
self, name: str, arguments: dict[str, Any] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""An event is occurred."""
|
"""An event is occurred."""
|
||||||
|
_LOGGER.debug('%s, attributes: %s', name, str(arguments))
|
||||||
self._trigger_event(event_type=name, event_attributes=arguments)
|
self._trigger_event(event_type=name, event_attributes=arguments)
|
||||||
|
@ -52,7 +52,12 @@ import logging
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
from homeassistant.components.fan import (
|
||||||
|
FanEntity,
|
||||||
|
FanEntityFeature,
|
||||||
|
DIRECTION_FORWARD,
|
||||||
|
DIRECTION_REVERSE
|
||||||
|
)
|
||||||
from homeassistant.util.percentage import (
|
from homeassistant.util.percentage import (
|
||||||
percentage_to_ranged_value,
|
percentage_to_ranged_value,
|
||||||
ranged_value_to_percentage,
|
ranged_value_to_percentage,
|
||||||
@ -167,20 +172,21 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||||
self._prop_horizontal_swing = prop
|
self._prop_horizontal_swing = prop
|
||||||
elif prop.name == 'wind-reverse':
|
elif prop.name == 'wind-reverse':
|
||||||
if prop.format_ == 'bool':
|
if prop.format_ == bool:
|
||||||
self._prop_wind_reverse_forward = False
|
self._prop_wind_reverse_forward = False
|
||||||
self._prop_wind_reverse_reverse = True
|
self._prop_wind_reverse_reverse = True
|
||||||
elif prop.value_list:
|
elif prop.value_list:
|
||||||
for item in prop.value_list.items:
|
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
|
self._prop_wind_reverse_forward = item.value
|
||||||
|
elif item.name in {'reversal', 'reverse'}:
|
||||||
self._prop_wind_reverse_reverse = item.value
|
self._prop_wind_reverse_reverse = item.value
|
||||||
if (
|
if (
|
||||||
self._prop_wind_reverse_forward is None
|
self._prop_wind_reverse_forward is None
|
||||||
or self._prop_wind_reverse_reverse is None
|
or self._prop_wind_reverse_reverse is None
|
||||||
):
|
):
|
||||||
# NOTICE: Value may be 0 or False
|
# NOTICE: Value may be 0 or False
|
||||||
_LOGGER.info(
|
_LOGGER.error(
|
||||||
'invalid wind-reverse, %s', self.entity_id)
|
'invalid wind-reverse, %s', self.entity_id)
|
||||||
continue
|
continue
|
||||||
self._attr_supported_features |= FanEntityFeature.DIRECTION
|
self._attr_supported_features |= FanEntityFeature.DIRECTION
|
||||||
@ -202,9 +208,9 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
if self._speed_names:
|
if self._speed_names:
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_fan_level,
|
prop=self._prop_fan_level,
|
||||||
value=self.get_map_value(
|
value=self.get_map_key(
|
||||||
map_=self._speed_name_map,
|
map_=self._speed_name_map,
|
||||||
key=percentage_to_ordered_list_item(
|
value=percentage_to_ordered_list_item(
|
||||||
self._speed_names, percentage)))
|
self._speed_names, percentage)))
|
||||||
else:
|
else:
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
@ -215,7 +221,7 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
# preset_mode
|
# preset_mode
|
||||||
if preset_mode:
|
if preset_mode:
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
self._prop_mode,
|
prop=self._prop_mode,
|
||||||
value=self.get_map_key(
|
value=self.get_map_key(
|
||||||
map_=self._mode_map, value=preset_mode))
|
map_=self._mode_map, value=preset_mode))
|
||||||
|
|
||||||
@ -230,12 +236,15 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
async def async_set_percentage(self, percentage: int) -> None:
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
"""Set the percentage of the fan speed."""
|
"""Set the percentage of the fan speed."""
|
||||||
if percentage > 0:
|
if percentage > 0:
|
||||||
|
if not self.is_on:
|
||||||
|
# If the fan is off, turn it on.
|
||||||
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
if self._speed_names:
|
if self._speed_names:
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_fan_level,
|
prop=self._prop_fan_level,
|
||||||
value=self.get_map_value(
|
value=self.get_map_key(
|
||||||
map_=self._speed_name_map,
|
map_=self._speed_name_map,
|
||||||
key=percentage_to_ordered_list_item(
|
value=percentage_to_ordered_list_item(
|
||||||
self._speed_names, percentage)))
|
self._speed_names, percentage)))
|
||||||
else:
|
else:
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
@ -243,16 +252,13 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
value=int(percentage_to_ranged_value(
|
value=int(percentage_to_ranged_value(
|
||||||
low_high_range=(self._speed_min, self._speed_max),
|
low_high_range=(self._speed_min, self._speed_max),
|
||||||
percentage=percentage)))
|
percentage=percentage)))
|
||||||
if not self.is_on:
|
|
||||||
# If the fan is off, turn it on.
|
|
||||||
await self.set_property_async(prop=self._prop_on, value=True)
|
|
||||||
else:
|
else:
|
||||||
await self.set_property_async(prop=self._prop_on, value=False)
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set the preset mode."""
|
"""Set the preset mode."""
|
||||||
await self.set_property_async(
|
await self.set_property_async(
|
||||||
self._prop_mode,
|
prop=self._prop_mode,
|
||||||
value=self.get_map_key(
|
value=self.get_map_key(
|
||||||
map_=self._mode_map, value=preset_mode))
|
map_=self._mode_map, value=preset_mode))
|
||||||
|
|
||||||
@ -264,7 +270,7 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
prop=self._prop_wind_reverse,
|
prop=self._prop_wind_reverse,
|
||||||
value=(
|
value=(
|
||||||
self._prop_wind_reverse_reverse
|
self._prop_wind_reverse_reverse
|
||||||
if self.current_direction == 'reverse'
|
if direction == DIRECTION_REVERSE
|
||||||
else self._prop_wind_reverse_forward))
|
else self._prop_wind_reverse_forward))
|
||||||
|
|
||||||
async def async_oscillate(self, oscillating: bool) -> None:
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
@ -293,9 +299,9 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
"""Return the current direction of the fan."""
|
"""Return the current direction of the fan."""
|
||||||
if not self._prop_wind_reverse:
|
if not self._prop_wind_reverse:
|
||||||
return None
|
return None
|
||||||
return 'reverse' if self.get_prop_value(
|
return DIRECTION_REVERSE if self.get_prop_value(
|
||||||
prop=self._prop_wind_reverse
|
prop=self._prop_wind_reverse
|
||||||
) == self._prop_wind_reverse_reverse else 'forward'
|
) == self._prop_wind_reverse_reverse else DIRECTION_FORWARD
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percentage(self) -> Optional[int]:
|
def percentage(self) -> Optional[int]:
|
||||||
@ -303,7 +309,7 @@ class Fan(MIoTServiceEntity, FanEntity):
|
|||||||
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
||||||
if fan_level is None:
|
if fan_level is None:
|
||||||
return None
|
return None
|
||||||
if self._speed_names:
|
if self._speed_names and self._speed_name_map:
|
||||||
return ordered_list_item_to_percentage(
|
return ordered_list_item_to_percentage(
|
||||||
self._speed_names, self._speed_name_map[fan_level])
|
self._speed_names, self._speed_name_map[fan_level])
|
||||||
else:
|
else:
|
||||||
|
@ -52,23 +52,22 @@ from typing import Any, Optional
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.components.humidifier import (
|
from homeassistant.components.humidifier import (HumidifierEntity,
|
||||||
HumidifierEntity,
|
HumidifierDeviceClass,
|
||||||
HumidifierDeviceClass,
|
HumidifierEntityFeature,
|
||||||
HumidifierEntityFeature
|
HumidifierAction)
|
||||||
)
|
|
||||||
|
|
||||||
from .miot.miot_spec import MIoTSpecProperty
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
from .miot.const import DOMAIN
|
from .miot.const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
@ -82,8 +81,8 @@ async def async_setup_entry(
|
|||||||
Humidifier(miot_device=miot_device, entity_data=data))
|
Humidifier(miot_device=miot_device, entity_data=data))
|
||||||
for data in miot_device.entity_list.get('dehumidifier', []):
|
for data in miot_device.entity_list.get('dehumidifier', []):
|
||||||
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
|
data.device_class = HumidifierDeviceClass.DEHUMIDIFIER
|
||||||
new_entities.append(Humidifier(
|
new_entities.append(
|
||||||
miot_device=miot_device, entity_data=data))
|
Humidifier(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
if new_entities:
|
if new_entities:
|
||||||
async_add_entities(new_entities)
|
async_add_entities(new_entities)
|
||||||
@ -99,9 +98,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
|||||||
|
|
||||||
_mode_map: dict[Any, Any]
|
_mode_map: dict[Any, Any]
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
entity_data: MIoTEntityData) -> None:
|
||||||
) -> None:
|
|
||||||
"""Initialize the Humidifier."""
|
"""Initialize the Humidifier."""
|
||||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
self._attr_device_class = entity_data.device_class
|
self._attr_device_class = entity_data.device_class
|
||||||
@ -130,12 +128,10 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
|||||||
# mode
|
# mode
|
||||||
elif prop.name == 'mode':
|
elif prop.name == 'mode':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('mode value_list is None, %s', self.entity_id)
|
||||||
'mode value_list is None, %s', self.entity_id)
|
|
||||||
continue
|
continue
|
||||||
self._mode_map = prop.value_list.to_map()
|
self._mode_map = prop.value_list.to_map()
|
||||||
self._attr_available_modes = list(
|
self._attr_available_modes = list(self._mode_map.values())
|
||||||
self._mode_map.values())
|
|
||||||
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||||
self._prop_mode = prop
|
self._prop_mode = prop
|
||||||
# relative-humidity
|
# relative-humidity
|
||||||
@ -152,33 +148,45 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
|||||||
|
|
||||||
async def async_set_humidity(self, humidity: int) -> None:
|
async def async_set_humidity(self, humidity: int) -> None:
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
await self.set_property_async(
|
if self._prop_target_humidity is None:
|
||||||
prop=self._prop_target_humidity, value=humidity)
|
return
|
||||||
|
await self.set_property_async(prop=self._prop_target_humidity,
|
||||||
|
value=humidity)
|
||||||
|
|
||||||
async def async_set_mode(self, mode: str) -> None:
|
async def async_set_mode(self, mode: str) -> None:
|
||||||
"""Set new target preset mode."""
|
"""Set new target preset mode."""
|
||||||
await self.set_property_async(
|
await self.set_property_async(prop=self._prop_mode,
|
||||||
prop=self._prop_mode,
|
value=self.get_map_key(
|
||||||
value=self.get_map_key(map_=self._mode_map, value=mode))
|
map_=self._mode_map, value=mode))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> Optional[bool]:
|
def is_on(self) -> Optional[bool]:
|
||||||
"""Return if the humidifier is on."""
|
"""Return if the humidifier is on."""
|
||||||
return self.get_prop_value(prop=self._prop_on)
|
return self.get_prop_value(prop=self._prop_on)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def action(self) -> Optional[HumidifierAction]:
|
||||||
|
"""Return the current status of the device."""
|
||||||
|
if not self.is_on:
|
||||||
|
return HumidifierAction.OFF
|
||||||
|
if self._attr_device_class == HumidifierDeviceClass.HUMIDIFIER:
|
||||||
|
return HumidifierAction.HUMIDIFYING
|
||||||
|
return HumidifierAction.DRYING
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_humidity(self) -> Optional[int]:
|
def current_humidity(self) -> Optional[int]:
|
||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
return self.get_prop_value(prop=self._prop_humidity)
|
return (self.get_prop_value(
|
||||||
|
prop=self._prop_humidity) if self._prop_humidity else None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_humidity(self) -> Optional[int]:
|
def target_humidity(self) -> Optional[int]:
|
||||||
"""Return the target humidity."""
|
"""Return the target humidity."""
|
||||||
return self.get_prop_value(prop=self._prop_target_humidity)
|
return (self.get_prop_value(prop=self._prop_target_humidity)
|
||||||
|
if self._prop_target_humidity else None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mode(self) -> Optional[str]:
|
def mode(self) -> Optional[str]:
|
||||||
"""Return the current preset mode."""
|
"""Return the current preset mode."""
|
||||||
return self.get_map_value(
|
return self.get_map_value(map_=self._mode_map,
|
||||||
map_=self._mode_map,
|
key=self.get_prop_value(prop=self._prop_mode))
|
||||||
key=self.get_prop_value(prop=self._prop_mode))
|
|
||||||
|
@ -96,7 +96,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
|||||||
"""Light entities for Xiaomi Home."""
|
"""Light entities for Xiaomi Home."""
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
_VALUE_RANGE_MODE_COUNT_MAX = 30
|
_VALUE_RANGE_MODE_COUNT_MAX = 30
|
||||||
_prop_on: MIoTSpecProperty
|
_prop_on: Optional[MIoTSpecProperty]
|
||||||
_prop_brightness: Optional[MIoTSpecProperty]
|
_prop_brightness: Optional[MIoTSpecProperty]
|
||||||
_prop_color_temp: Optional[MIoTSpecProperty]
|
_prop_color_temp: Optional[MIoTSpecProperty]
|
||||||
_prop_color: Optional[MIoTSpecProperty]
|
_prop_color: Optional[MIoTSpecProperty]
|
||||||
@ -179,7 +179,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
|||||||
) / prop.value_range.step)
|
) / prop.value_range.step)
|
||||||
> self._VALUE_RANGE_MODE_COUNT_MAX
|
> self._VALUE_RANGE_MODE_COUNT_MAX
|
||||||
):
|
):
|
||||||
_LOGGER.info(
|
_LOGGER.error(
|
||||||
'too many mode values, %s, %s, %s',
|
'too many mode values, %s, %s, %s',
|
||||||
self.entity_id, prop.name, prop.value_range)
|
self.entity_id, prop.name, prop.value_range)
|
||||||
else:
|
else:
|
||||||
@ -250,23 +250,25 @@ class Light(MIoTServiceEntity, LightEntity):
|
|||||||
|
|
||||||
Shall set attributes in kwargs if applicable.
|
Shall set attributes in kwargs if applicable.
|
||||||
"""
|
"""
|
||||||
result: bool = False
|
|
||||||
# on
|
# on
|
||||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||||
value_on = True if self._prop_on.format_ == bool else 1
|
if self._prop_on:
|
||||||
result = await self.set_property_async(
|
value_on = True if self._prop_on.format_ == bool else 1
|
||||||
prop=self._prop_on, value=value_on)
|
await self.set_property_async(
|
||||||
|
prop=self._prop_on, value=value_on)
|
||||||
# brightness
|
# brightness
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
brightness = brightness_to_value(
|
brightness = brightness_to_value(
|
||||||
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
|
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
|
||||||
result = await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_brightness, value=brightness)
|
prop=self._prop_brightness, value=brightness,
|
||||||
|
write_ha_state=False)
|
||||||
# color-temperature
|
# color-temperature
|
||||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||||
result = await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_color_temp,
|
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
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
# rgb color
|
# rgb color
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
@ -274,19 +276,23 @@ class Light(MIoTServiceEntity, LightEntity):
|
|||||||
g = kwargs[ATTR_RGB_COLOR][1]
|
g = kwargs[ATTR_RGB_COLOR][1]
|
||||||
b = kwargs[ATTR_RGB_COLOR][2]
|
b = kwargs[ATTR_RGB_COLOR][2]
|
||||||
rgb = (r << 16) | (g << 8) | b
|
rgb = (r << 16) | (g << 8) | b
|
||||||
result = await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_color, value=rgb)
|
prop=self._prop_color, value=rgb,
|
||||||
|
write_ha_state=False)
|
||||||
self._attr_color_mode = ColorMode.RGB
|
self._attr_color_mode = ColorMode.RGB
|
||||||
# mode
|
# mode
|
||||||
if ATTR_EFFECT in kwargs:
|
if ATTR_EFFECT in kwargs:
|
||||||
result = await self.set_property_async(
|
await self.set_property_async(
|
||||||
prop=self._prop_mode,
|
prop=self._prop_mode,
|
||||||
value=self.get_map_key(
|
value=self.get_map_key(
|
||||||
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
|
map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
|
||||||
return result
|
write_ha_state=False)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs) -> None:
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
|
if not self._prop_on:
|
||||||
|
return
|
||||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||||
value_on = False if self._prop_on.format_ == bool else 0
|
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": [
|
"requirements": [
|
||||||
"construct>=2.10.56",
|
"construct>=2.10.56",
|
||||||
"paho-mqtt<2.0.0",
|
"paho-mqtt",
|
||||||
"numpy",
|
"numpy",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
"psutil"
|
"psutil"
|
||||||
],
|
],
|
||||||
"version": "v0.1.5b2",
|
"version": "v0.4.1",
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
"_miot-central._tcp.local."
|
"_miot-central._tcp.local."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
470
custom_components/xiaomi_home/media_player.py
Normal file
470
custom_components/xiaomi_home/media_player.py
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Copyright (C) 2024 Xiaomi Corporation.
|
||||||
|
|
||||||
|
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||||
|
Integration and related Xiaomi cloud service API interface provided under this
|
||||||
|
license, including source code and object code (collectively, "Licensed Work"),
|
||||||
|
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||||
|
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||||
|
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||||
|
distribute the Licensed Work only for your use of Home Assistant for
|
||||||
|
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||||
|
you to use the Licensed Work for any other purpose, including but not limited
|
||||||
|
to use Licensed Work to develop applications (APP), Web services, and other
|
||||||
|
forms of software.
|
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||||
|
modifications, whether in source or object form, provided that you must give
|
||||||
|
any other recipients of the Licensed Work a copy of this License and retain all
|
||||||
|
copyright and disclaimers.
|
||||||
|
|
||||||
|
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||||
|
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||||
|
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||||
|
for any direct, indirect, special, incidental, or consequential damages or
|
||||||
|
losses arising from the use or inability to use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||||
|
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||||
|
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||||
|
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||||
|
without limitation, without obtaining other written permission from Xiaomi, you
|
||||||
|
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||||
|
may make the public associate with Xiaomi in any form to publicize or promote
|
||||||
|
the software or hardware devices that use the Licensed Work.
|
||||||
|
|
||||||
|
Xiaomi has the right to immediately terminate all your authorization under this
|
||||||
|
License in the event:
|
||||||
|
1. You assert patent invalidation, litigation, or other claims against patents
|
||||||
|
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||||
|
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||||
|
off Xiaomi or its affiliates' products.
|
||||||
|
|
||||||
|
Media player entities for Xiaomi Home.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.components.media_player import (MediaPlayerEntity,
|
||||||
|
MediaPlayerEntityFeature,
|
||||||
|
MediaPlayerDeviceClass,
|
||||||
|
MediaPlayerState, MediaType)
|
||||||
|
|
||||||
|
from .miot.const import DOMAIN
|
||||||
|
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
||||||
|
from .miot.miot_spec import MIoTSpecProperty, MIoTSpecAction
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
new_entities = []
|
||||||
|
for miot_device in device_list:
|
||||||
|
for data in miot_device.entity_list.get('wifi-speaker', []):
|
||||||
|
new_entities.append(
|
||||||
|
WifiSpeaker(miot_device=miot_device, entity_data=data))
|
||||||
|
for data in miot_device.entity_list.get('television', []):
|
||||||
|
new_entities.append(
|
||||||
|
Television(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
|
if new_entities:
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureVolumeMute(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""VOLUME_MUTE feature of the media player entity."""
|
||||||
|
_prop_mute: Optional[MIoTSpecProperty]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._prop_mute = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'mute':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.VOLUME_MUTE)
|
||||||
|
self._prop_mute = prop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self) -> Optional[bool]:
|
||||||
|
"""True if volume is currently muted."""
|
||||||
|
return self.get_prop_value(
|
||||||
|
prop=self._prop_mute) if self._prop_mute else None
|
||||||
|
|
||||||
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
|
"""Mute the volume."""
|
||||||
|
await self.set_property_async(prop=self._prop_mute, value=mute)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureVolumeSet(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""VOLUME_SET feature of the media player entity."""
|
||||||
|
_prop_volume: Optional[MIoTSpecProperty]
|
||||||
|
_volume_value_min: Optional[float]
|
||||||
|
_volume_value_max: Optional[float]
|
||||||
|
_volume_value_range: Optional[float]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._prop_volume = None
|
||||||
|
self._volume_value_min = None
|
||||||
|
self._volume_value_max = None
|
||||||
|
self._volume_value_range = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'volume':
|
||||||
|
if not prop.value_range:
|
||||||
|
_LOGGER.error('invalid volume value_range format, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._volume_value_min = prop.value_range.min_
|
||||||
|
self._volume_value_max = prop.value_range.max_
|
||||||
|
self._volume_value_range = (prop.value_range.max_ -
|
||||||
|
prop.value_range.min_)
|
||||||
|
self._attr_volume_step = (prop.value_range.step /
|
||||||
|
self._volume_value_range)
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.VOLUME_SET |
|
||||||
|
MediaPlayerEntityFeature.VOLUME_STEP)
|
||||||
|
self._prop_volume = prop
|
||||||
|
|
||||||
|
async def async_set_volume_level(self, volume: float) -> None:
|
||||||
|
"""Set volume level."""
|
||||||
|
value = volume * self._volume_value_range + self._volume_value_min
|
||||||
|
if value > self._volume_value_max:
|
||||||
|
value = self._volume_value_max
|
||||||
|
elif value < self._volume_value_min:
|
||||||
|
value = self._volume_value_min
|
||||||
|
await self.set_property_async(prop=self._prop_volume, value=value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self) -> Optional[float]:
|
||||||
|
"""The current volume level, range [0, 1]."""
|
||||||
|
value = self.get_prop_value(
|
||||||
|
prop=self._prop_volume) if self._prop_volume else None
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return (value - self._volume_value_min) / self._volume_value_range
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturePlay(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""PLAY feature of the media player entity."""
|
||||||
|
_action_play: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_play = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'play':
|
||||||
|
self._attr_supported_features |= (MediaPlayerEntityFeature.PLAY)
|
||||||
|
self._action_play = act
|
||||||
|
|
||||||
|
async def async_media_play(self) -> None:
|
||||||
|
"""Send play command."""
|
||||||
|
await self.action_async(action=self._action_play)
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturePause(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""PAUSE feature of the media player entity."""
|
||||||
|
_action_pause: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_pause = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'pause':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.PAUSE)
|
||||||
|
self._action_pause = act
|
||||||
|
|
||||||
|
async def async_media_pause(self) -> None:
|
||||||
|
"""Send pause command."""
|
||||||
|
await self.action_async(action=self._action_pause)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureStop(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""STOP feature of the media player entity."""
|
||||||
|
_action_stop: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_stop = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'stop':
|
||||||
|
self._attr_supported_features |= (MediaPlayerEntityFeature.STOP)
|
||||||
|
self._action_stop = act
|
||||||
|
|
||||||
|
async def async_media_stop(self) -> None:
|
||||||
|
"""Send stop command."""
|
||||||
|
await self.action_async(action=self._action_stop)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureNextTrack(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""NEXT_TRACK feature of the media player entity."""
|
||||||
|
_action_next: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_next = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'next':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.NEXT_TRACK)
|
||||||
|
self._action_next = act
|
||||||
|
|
||||||
|
async def async_media_next_track(self) -> None:
|
||||||
|
"""Send next track command."""
|
||||||
|
await self.action_async(action=self._action_next)
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturePreviousTrack(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""PREVIOUS_TRACK feature of the media player entity."""
|
||||||
|
_action_previous: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_previous = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'previous':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.PREVIOUS_TRACK)
|
||||||
|
self._action_previous = act
|
||||||
|
|
||||||
|
async def async_media_previous_track(self) -> None:
|
||||||
|
"""Send previous track command."""
|
||||||
|
await self.action_async(action=self._action_previous)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureSoundMode(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""SELECT_SOUND_MODE feature of the media player entity."""
|
||||||
|
_prop_play_loop_mode: Optional[MIoTSpecProperty]
|
||||||
|
_sound_mode_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._prop_play_loop_mode = None
|
||||||
|
self._sound_mode_map = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'play-loop-mode':
|
||||||
|
if not prop.value_list:
|
||||||
|
_LOGGER.error('invalid play-loop-mode value_list, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._sound_mode_map = prop.value_list.to_map()
|
||||||
|
self._attr_sound_mode_list = list(self._sound_mode_map.values())
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.SELECT_SOUND_MODE)
|
||||||
|
self._prop_play_loop_mode = prop
|
||||||
|
|
||||||
|
async def async_select_sound_mode(self, sound_mode: str):
|
||||||
|
"""Switch the sound mode of the entity."""
|
||||||
|
await self.set_property_async(prop=self._prop_play_loop_mode,
|
||||||
|
value=self.get_map_key(
|
||||||
|
map_=self._sound_mode_map,
|
||||||
|
value=sound_mode))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sound_mode(self) -> Optional[str]:
|
||||||
|
"""The current sound mode."""
|
||||||
|
return (self.get_map_value(map_=self._sound_mode_map,
|
||||||
|
key=self.get_prop_value(
|
||||||
|
prop=self._prop_play_loop_mode))
|
||||||
|
if self._prop_play_loop_mode else None)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureTurnOn(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""TURN_ON feature of the media player entity."""
|
||||||
|
_action_turn_on: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_turn_on = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'turn-on':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.TURN_ON)
|
||||||
|
self._action_turn_on = act
|
||||||
|
|
||||||
|
async def async_turn_on(self) -> None:
|
||||||
|
"""Turn the media player on."""
|
||||||
|
await self.action_async(action=self._action_turn_on)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureTurnOff(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""TURN_OFF feature of the media player entity."""
|
||||||
|
_action_turn_off: Optional[MIoTSpecAction]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._action_turn_off = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# actions
|
||||||
|
for act in entity_data.actions:
|
||||||
|
if act.name == 'turn-off':
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.TURN_OFF)
|
||||||
|
self._action_turn_off = act
|
||||||
|
|
||||||
|
async def async_turn_off(self) -> None:
|
||||||
|
"""Turn the media player off."""
|
||||||
|
await self.action_async(action=self._action_turn_off)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureSource(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""SELECT_SOURCE feature of the media player entity."""
|
||||||
|
_prop_input_control: Optional[MIoTSpecProperty]
|
||||||
|
_input_source_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._prop_input_control = None
|
||||||
|
self._input_source_map = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'input-control':
|
||||||
|
if not prop.value_list:
|
||||||
|
_LOGGER.error('invalid input-control value_list, %s',
|
||||||
|
self.entity_id)
|
||||||
|
continue
|
||||||
|
self._input_source_map = prop.value_list.to_map()
|
||||||
|
self._attr_source_list = list(self._input_source_map.values())
|
||||||
|
self._attr_supported_features |= (
|
||||||
|
MediaPlayerEntityFeature.SELECT_SOURCE)
|
||||||
|
self._prop_input_control = prop
|
||||||
|
|
||||||
|
async def async_select_source(self, source: str) -> None:
|
||||||
|
"""Select input source."""
|
||||||
|
await self.set_property_async(prop=self._prop_input_control,
|
||||||
|
value=self.get_map_key(
|
||||||
|
map_=self._input_source_map,
|
||||||
|
value=source))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self) -> Optional[str]:
|
||||||
|
"""The current input source."""
|
||||||
|
return (self.get_map_value(map_=self._input_source_map,
|
||||||
|
key=self.get_prop_value(
|
||||||
|
prop=self._prop_input_control))
|
||||||
|
if self._prop_input_control else None)
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureState(MIoTServiceEntity, MediaPlayerEntity):
|
||||||
|
"""States feature of the media player entity."""
|
||||||
|
_prop_playing_state: Optional[MIoTSpecProperty]
|
||||||
|
_playing_state_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the feature class."""
|
||||||
|
self._prop_playing_state = None
|
||||||
|
self._playing_state_map = None
|
||||||
|
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
# properties
|
||||||
|
for prop in entity_data.props:
|
||||||
|
if prop.name == 'playing-state':
|
||||||
|
if not prop.value_list:
|
||||||
|
_LOGGER.error('invalid mode value_list, %s', self.entity_id)
|
||||||
|
continue
|
||||||
|
self._playing_state_map = {}
|
||||||
|
for item in prop.value_list.items:
|
||||||
|
if item.name in {'off'}:
|
||||||
|
self._playing_state_map[
|
||||||
|
item.value] = MediaPlayerState.OFF
|
||||||
|
elif item.name in {'idle', 'stop', 'stopped'}:
|
||||||
|
self._playing_state_map[
|
||||||
|
item.value] = MediaPlayerState.IDLE
|
||||||
|
elif item.name in {'playing'}:
|
||||||
|
self._playing_state_map[
|
||||||
|
item.value] = MediaPlayerState.PLAYING
|
||||||
|
elif item.name in {'pause', 'paused'}:
|
||||||
|
self._playing_state_map[
|
||||||
|
item.value] = MediaPlayerState.PAUSED
|
||||||
|
self._prop_playing_state = prop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[MediaPlayerState]:
|
||||||
|
"""The current state."""
|
||||||
|
return (self.get_map_value(map_=self._playing_state_map,
|
||||||
|
key=self.get_prop_value(
|
||||||
|
prop=self._prop_playing_state))
|
||||||
|
if self._prop_playing_state else MediaPlayerState.ON)
|
||||||
|
|
||||||
|
|
||||||
|
class WifiSpeaker(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay,
|
||||||
|
FeaturePause, FeatureStop, FeatureNextTrack,
|
||||||
|
FeaturePreviousTrack, FeatureSoundMode, FeatureState):
|
||||||
|
"""WiFi speaker, aka XiaoAI sound speaker."""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the device."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
|
||||||
|
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||||
|
self._attr_media_content_type = MediaType.MUSIC
|
||||||
|
|
||||||
|
|
||||||
|
class Television(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay, FeaturePause,
|
||||||
|
FeatureStop, FeatureNextTrack, FeaturePreviousTrack,
|
||||||
|
FeatureSoundMode, FeatureState, FeatureSource, FeatureTurnOn,
|
||||||
|
FeatureTurnOff):
|
||||||
|
"""Television"""
|
||||||
|
|
||||||
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
|
"""Initialize the device."""
|
||||||
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
|
||||||
|
self._attr_device_class = MediaPlayerDeviceClass.TV
|
||||||
|
self._attr_media_content_type = MediaType.VIDEO
|
@ -71,10 +71,12 @@ SUPPORTED_PLATFORMS: list = [
|
|||||||
'button',
|
'button',
|
||||||
'climate',
|
'climate',
|
||||||
'cover',
|
'cover',
|
||||||
|
'device_tracker',
|
||||||
'event',
|
'event',
|
||||||
'fan',
|
'fan',
|
||||||
'humidifier',
|
'humidifier',
|
||||||
'light',
|
'light',
|
||||||
|
'media_player',
|
||||||
'notify',
|
'notify',
|
||||||
'number',
|
'number',
|
||||||
'select',
|
'select',
|
||||||
@ -85,6 +87,11 @@ SUPPORTED_PLATFORMS: list = [
|
|||||||
'water_heater',
|
'water_heater',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
UNSUPPORTED_MODELS: list = [
|
||||||
|
'chuangmi.ir.v2',
|
||||||
|
'xiaomi.router.rd03'
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_CLOUD_SERVER: str = 'cn'
|
DEFAULT_CLOUD_SERVER: str = 'cn'
|
||||||
CLOUD_SERVERS: dict = {
|
CLOUD_SERVERS: dict = {
|
||||||
'cn': '中国大陆',
|
'cn': '中国大陆',
|
||||||
@ -113,6 +120,10 @@ INTEGRATION_LANGUAGES = {
|
|||||||
'zh-Hant': '繁體中文'
|
'zh-Hant': '繁體中文'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFAULT_COVER_DEAD_ZONE_WIDTH: int = 0
|
||||||
|
MIN_COVER_DEAD_ZONE_WIDTH: int = 0
|
||||||
|
MAX_COVER_DEAD_ZONE_WIDTH: int = 5
|
||||||
|
|
||||||
DEFAULT_CTRL_MODE: str = 'auto'
|
DEFAULT_CTRL_MODE: str = 'auto'
|
||||||
|
|
||||||
# Registered in Xiaomi OAuth 2.0 Service
|
# Registered in Xiaomi OAuth 2.0 Service
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} Geräte offline:** \n{message}",
|
"device_list_offline": "\n**{count} Geräte offline:** \n{message}",
|
||||||
"network_status_online": "Online",
|
"network_status_online": "Online",
|
||||||
"network_status_offline": "Offline",
|
"network_status_offline": "Offline",
|
||||||
|
"central_state_changed_title": "Verbindungsstatus des Zentral-Gateways",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** Lokale Verbindungsstrecke des Zentral-Gateways: {conn_status}",
|
||||||
|
"central_state_connected": "verbunden",
|
||||||
|
"central_state_disconnected": "getrennt",
|
||||||
"device_exec_error": "Fehler bei der Ausführung"
|
"device_exec_error": "Fehler bei der Ausführung"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} devices offline:** \n{message}",
|
"device_list_offline": "\n**{count} devices offline:** \n{message}",
|
||||||
"network_status_online": "Online",
|
"network_status_online": "Online",
|
||||||
"network_status_offline": "Offline",
|
"network_status_offline": "Offline",
|
||||||
|
"central_state_changed_title": "Central Hub Gateway Connection Status",
|
||||||
|
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** local connection to Xiaomi central hub gateway: {conn_status}",
|
||||||
|
"central_state_connected": "Connected",
|
||||||
|
"central_state_disconnected": "Disconnected",
|
||||||
"device_exec_error": "Execution error"
|
"device_exec_error": "Execution error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}",
|
"device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}",
|
||||||
"network_status_online": "En línea",
|
"network_status_online": "En línea",
|
||||||
"network_status_offline": "Desconectado",
|
"network_status_offline": "Desconectado",
|
||||||
|
"central_state_changed_title": "Estado de conexión de la puerta de enlace central",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** enlace de conexión local de la puerta de enlace central: {conn_status}",
|
||||||
|
"central_state_connected": "conectado",
|
||||||
|
"central_state_disconnected": "desconectado",
|
||||||
"device_exec_error": "Error de ejecución"
|
"device_exec_error": "Error de ejecución"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} appareils hors ligne :** \n{message}",
|
"device_list_offline": "\n**{count} appareils hors ligne :** \n{message}",
|
||||||
"network_status_online": "En ligne",
|
"network_status_online": "En ligne",
|
||||||
"network_status_offline": "Hors ligne",
|
"network_status_offline": "Hors ligne",
|
||||||
|
"central_state_changed_title": "État de connexion de la passerelle centrale",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** liaison de connexion locale de la passerelle centrale : {conn_status}",
|
||||||
|
"central_state_connected": "connecté",
|
||||||
|
"central_state_disconnected": "déconnecté",
|
||||||
"device_exec_error": "Erreur d'exécution"
|
"device_exec_error": "Erreur d'exécution"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} dispositivi offline:** \n{message}",
|
"device_list_offline": "\n**{count} dispositivi offline:** \n{message}",
|
||||||
"network_status_online": "Online",
|
"network_status_online": "Online",
|
||||||
"network_status_offline": "Offline",
|
"network_status_offline": "Offline",
|
||||||
|
"central_state_changed_title": "Stato di connessione del gateway centrale",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** collegamento locale del gateway centrale: {conn_status}",
|
||||||
|
"central_state_connected": "connesso",
|
||||||
|
"central_state_disconnected": "disconnesso",
|
||||||
"device_exec_error": "Errore di esecuzione"
|
"device_exec_error": "Errore di esecuzione"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} デバイスがオフライン:** \n{message}",
|
"device_list_offline": "\n**{count} デバイスがオフライン:** \n{message}",
|
||||||
"network_status_online": "オンライン",
|
"network_status_online": "オンライン",
|
||||||
"network_status_offline": "オフライン",
|
"network_status_offline": "オフライン",
|
||||||
|
"central_state_changed_title": "中枢ゲートウェイ接続ステータス",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** 中枢ゲートウェイのローカル接続リンク: {conn_status}",
|
||||||
|
"central_state_connected": "接続済み",
|
||||||
|
"central_state_disconnected": "切断されました",
|
||||||
"device_exec_error": "実行エラー"
|
"device_exec_error": "実行エラー"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} apparaten offline:** \n{message}",
|
"device_list_offline": "\n**{count} apparaten offline:** \n{message}",
|
||||||
"network_status_online": "Online",
|
"network_status_online": "Online",
|
||||||
"network_status_offline": "Offline",
|
"network_status_offline": "Offline",
|
||||||
|
"central_state_changed_title": "Verbindingsstatus van centrale gateway",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** Lokale verbinding van centrale gateway: {conn_status}",
|
||||||
|
"central_state_connected": "verbonden",
|
||||||
|
"central_state_disconnected": "verbinding verbroken",
|
||||||
"device_exec_error": "Uitvoeringsfout"
|
"device_exec_error": "Uitvoeringsfout"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
|
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
|
||||||
"network_status_online": "online",
|
"network_status_online": "online",
|
||||||
"network_status_offline": "offline",
|
"network_status_offline": "offline",
|
||||||
|
"central_state_changed_title": "Status de conexão do gateway central",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** conexão local do gateway central: {conn_status}",
|
||||||
|
"central_state_connected": "conectado",
|
||||||
|
"central_state_disconnected": "desconectado",
|
||||||
"device_exec_error": "Erro na execução"
|
"device_exec_error": "Erro na execução"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
|
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
|
||||||
"network_status_online": "Online",
|
"network_status_online": "Online",
|
||||||
"network_status_offline": "Offline",
|
"network_status_offline": "Offline",
|
||||||
|
"central_state_changed_title": "Estado da ligação do gateway central",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** ligação local do gateway central: {conn_status}",
|
||||||
|
"central_state_connected": "ligado",
|
||||||
|
"central_state_disconnected": "desligado",
|
||||||
"device_exec_error": "Erro de execução"
|
"device_exec_error": "Erro de execução"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} устройств недоступно:** \n{message}",
|
"device_list_offline": "\n**{count} устройств недоступно:** \n{message}",
|
||||||
"network_status_online": "В сети",
|
"network_status_online": "В сети",
|
||||||
"network_status_offline": "Не в сети",
|
"network_status_offline": "Не в сети",
|
||||||
|
"central_state_changed_title": "Статус подключения центрального шлюза",
|
||||||
|
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** локальное подключение центрального шлюза: {conn_status}",
|
||||||
|
"central_state_connected": "подключено",
|
||||||
|
"central_state_disconnected": "разъединено",
|
||||||
"device_exec_error": "Ошибка выполнения"
|
"device_exec_error": "Ошибка выполнения"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} 个设备离线**: \n{message}",
|
"device_list_offline": "\n**{count} 个设备离线**: \n{message}",
|
||||||
"network_status_online": "在线",
|
"network_status_online": "在线",
|
||||||
"network_status_offline": "离线",
|
"network_status_offline": "离线",
|
||||||
|
"central_state_changed_title": "中枢网关连接状态",
|
||||||
|
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** 中枢网关本地连接链路: {conn_status}",
|
||||||
|
"central_state_connected": "已连接",
|
||||||
|
"central_state_disconnected": "断连",
|
||||||
"device_exec_error": "执行错误"
|
"device_exec_error": "执行错误"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,6 +99,10 @@
|
|||||||
"device_list_offline": "\n**{count} 個設備離線:** \n{message}",
|
"device_list_offline": "\n**{count} 個設備離線:** \n{message}",
|
||||||
"network_status_online": "在線",
|
"network_status_online": "在線",
|
||||||
"network_status_offline": "離線",
|
"network_status_offline": "離線",
|
||||||
|
"central_state_changed_title": "中枢網關連接狀態",
|
||||||
|
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** 中枢網關本地連接鏈路: {conn_status}",
|
||||||
|
"central_state_connected": "已連接",
|
||||||
|
"central_state_disconnected": "断連",
|
||||||
"device_exec_error": "執行錯誤"
|
"device_exec_error": "執行錯誤"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -63,7 +63,8 @@ from .common import MIoTMatcher, slugify_did
|
|||||||
from .const import (
|
from .const import (
|
||||||
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
|
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
|
||||||
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
|
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
|
||||||
OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL)
|
OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL,
|
||||||
|
DEFAULT_COVER_DEAD_ZONE_WIDTH)
|
||||||
from .miot_cloud import MIoTHttpClient, MIoTOauthClient
|
from .miot_cloud import MIoTHttpClient, MIoTOauthClient
|
||||||
from .miot_error import MIoTClientError, MIoTErrorCode
|
from .miot_error import MIoTClientError, MIoTErrorCode
|
||||||
from .miot_mips import (
|
from .miot_mips import (
|
||||||
@ -150,7 +151,7 @@ class MIoTClient:
|
|||||||
# Device list update timestamp
|
# Device list update timestamp
|
||||||
_device_list_update_ts: int
|
_device_list_update_ts: int
|
||||||
|
|
||||||
_sub_source_list: dict[str, str]
|
_sub_source_list: dict[str, Optional[str]]
|
||||||
_sub_tree: MIoTMatcher
|
_sub_tree: MIoTMatcher
|
||||||
_sub_device_state: dict[str, MipsDeviceState]
|
_sub_device_state: dict[str, MipsDeviceState]
|
||||||
|
|
||||||
@ -252,7 +253,18 @@ class MIoTClient:
|
|||||||
if not self._user_config:
|
if not self._user_config:
|
||||||
# Integration need to be add again
|
# Integration need to be add again
|
||||||
raise MIoTClientError('load_user_config_async error')
|
raise MIoTClientError('load_user_config_async error')
|
||||||
_LOGGER.debug('user config, %s', json.dumps(self._user_config))
|
# Hide sensitive info in printing
|
||||||
|
p_user_config: dict = deepcopy(self._user_config)
|
||||||
|
p_access_token: str = p_user_config['auth_info']['access_token']
|
||||||
|
p_refresh_token: str = p_user_config['auth_info']['refresh_token']
|
||||||
|
p_mac_key: str = p_user_config['auth_info']['mac_key']
|
||||||
|
p_user_config['auth_info'][
|
||||||
|
'access_token'] = f"{p_access_token[:5]}***{p_access_token[-5:]}"
|
||||||
|
p_user_config['auth_info'][
|
||||||
|
'refresh_token'] = f"{p_refresh_token[:5]}***{p_refresh_token[-5:]}"
|
||||||
|
p_user_config['auth_info'][
|
||||||
|
'mac_key'] = f"{p_mac_key[:5]}***{p_mac_key[-5:]}"
|
||||||
|
_LOGGER.debug('user config, %s', json.dumps(p_user_config))
|
||||||
# MIoT i18n client
|
# MIoT i18n client
|
||||||
self._i18n = MIoTI18n(
|
self._i18n = MIoTI18n(
|
||||||
lang=self._entry_data.get(
|
lang=self._entry_data.get(
|
||||||
@ -486,6 +498,11 @@ class MIoTClient:
|
|||||||
def display_binary_bool(self) -> bool:
|
def display_binary_bool(self) -> bool:
|
||||||
return self._display_binary_bool
|
return self._display_binary_bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cover_dead_zone_width(self) -> int:
|
||||||
|
return self._entry_data.get('cover_dead_zone_width',
|
||||||
|
DEFAULT_COVER_DEAD_ZONE_WIDTH)
|
||||||
|
|
||||||
@display_devices_changed_notify.setter
|
@display_devices_changed_notify.setter
|
||||||
def display_devices_changed_notify(self, value: list[str]) -> None:
|
def display_devices_changed_notify(self, value: list[str]) -> None:
|
||||||
if set(value) == set(self._display_devs_notify):
|
if set(value) == set(self._display_devs_notify):
|
||||||
@ -620,7 +637,7 @@ class MIoTClient:
|
|||||||
# Priority local control
|
# Priority local control
|
||||||
if self._ctrl_mode == CtrlMode.AUTO:
|
if self._ctrl_mode == CtrlMode.AUTO:
|
||||||
# Gateway control
|
# Gateway control
|
||||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
device_gw = self._device_list_gateway.get(did, None)
|
||||||
if (
|
if (
|
||||||
device_gw and device_gw.get('online', False)
|
device_gw and device_gw.get('online', False)
|
||||||
and device_gw.get('specv2_access', False)
|
and device_gw.get('specv2_access', False)
|
||||||
@ -629,11 +646,14 @@ class MIoTClient:
|
|||||||
mips = self._mips_local.get(device_gw['group_id'], None)
|
mips = self._mips_local.get(device_gw['group_id'], None)
|
||||||
if mips is None:
|
if mips is None:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'no gw route, %s, try control throw cloud',
|
'no gateway route, %s, try control through cloud',
|
||||||
device_gw)
|
device_gw)
|
||||||
else:
|
else:
|
||||||
result = await mips.set_prop_async(
|
result = await mips.set_prop_async(
|
||||||
did=did, siid=siid, piid=piid, value=value)
|
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(
|
rc = (result or {}).get(
|
||||||
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
||||||
if rc in [0, 1]:
|
if rc in [0, 1]:
|
||||||
@ -641,12 +661,13 @@ class MIoTClient:
|
|||||||
raise MIoTClientError(
|
raise MIoTClientError(
|
||||||
self.__get_exec_error_with_rc(rc=rc))
|
self.__get_exec_error_with_rc(rc=rc))
|
||||||
# Lan control
|
# 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):
|
if device_lan and device_lan.get('online', False):
|
||||||
result = await self._miot_lan.set_prop_async(
|
result = await self._miot_lan.set_prop_async(
|
||||||
did=did, siid=siid, piid=piid, value=value)
|
did=did, siid=siid, piid=piid, value=value)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
'lan set prop, %s, %s, %s -> %s', did, siid, piid, result)
|
'lan set prop, %s.%d.%d, %s -> %s',
|
||||||
|
did, siid, piid, value, result)
|
||||||
rc = (result or {}).get(
|
rc = (result or {}).get(
|
||||||
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
|
||||||
if rc in [0, 1]:
|
if rc in [0, 1]:
|
||||||
@ -657,12 +678,12 @@ class MIoTClient:
|
|||||||
# Cloud control
|
# Cloud control
|
||||||
device_cloud = self._device_list_cloud.get(did, None)
|
device_cloud = self._device_list_cloud.get(did, None)
|
||||||
if device_cloud and device_cloud.get('online', False):
|
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=[
|
params=[
|
||||||
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
|
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
|
||||||
])
|
])
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
'set prop response, %s.%d.%d, %s, result, %s',
|
'cloud set prop, %s.%d.%d, %s -> %s',
|
||||||
did, siid, piid, value, result)
|
did, siid, piid, value, result)
|
||||||
if result and len(result) == 1:
|
if result and len(result) == 1:
|
||||||
rc = result[0].get(
|
rc = result[0].get(
|
||||||
@ -746,7 +767,7 @@ class MIoTClient:
|
|||||||
if did not in self._device_list_cache:
|
if did not in self._device_list_cache:
|
||||||
raise MIoTClientError(f'did not exist, {did}')
|
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
|
# Priority local control
|
||||||
if self._ctrl_mode == CtrlMode.AUTO:
|
if self._ctrl_mode == CtrlMode.AUTO:
|
||||||
if (
|
if (
|
||||||
@ -782,7 +803,7 @@ class MIoTClient:
|
|||||||
self.__get_exec_error_with_rc(rc=rc))
|
self.__get_exec_error_with_rc(rc=rc))
|
||||||
# Cloud control
|
# Cloud control
|
||||||
device_cloud = self._device_list_cloud.get(did, None)
|
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(
|
result: dict = await self._http.action_async(
|
||||||
did=did, siid=siid, aiid=aiid, in_list=in_list)
|
did=did, siid=siid, aiid=aiid, in_list=in_list)
|
||||||
if result:
|
if result:
|
||||||
@ -798,14 +819,15 @@ class MIoTClient:
|
|||||||
dids=[did]))
|
dids=[did]))
|
||||||
raise MIoTClientError(
|
raise MIoTClientError(
|
||||||
self.__get_exec_error_with_rc(rc=rc))
|
self.__get_exec_error_with_rc(rc=rc))
|
||||||
# Show error message
|
# TODO: Show error message
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'client action failed, %s.%d.%d', did, siid, aiid)
|
'client action failed, %s.%d.%d', did, siid, aiid)
|
||||||
return None
|
return []
|
||||||
|
|
||||||
def sub_prop(
|
def sub_prop(
|
||||||
self, did: str, handler: Callable[[dict, Any], None],
|
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:
|
) -> bool:
|
||||||
if did not in self._device_list_cache:
|
if did not in self._device_list_cache:
|
||||||
raise MIoTClientError(f'did not exist, {did}')
|
raise MIoTClientError(f'did not exist, {did}')
|
||||||
@ -818,7 +840,9 @@ class MIoTClient:
|
|||||||
_LOGGER.debug('client sub prop, %s', topic)
|
_LOGGER.debug('client sub prop, %s', topic)
|
||||||
return True
|
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 = (
|
topic = (
|
||||||
f'{did}/p/'
|
f'{did}/p/'
|
||||||
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
||||||
@ -829,7 +853,8 @@ class MIoTClient:
|
|||||||
|
|
||||||
def sub_event(
|
def sub_event(
|
||||||
self, did: str, handler: Callable[[dict, Any], None],
|
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:
|
) -> bool:
|
||||||
if did not in self._device_list_cache:
|
if did not in self._device_list_cache:
|
||||||
raise MIoTClientError(f'did not exist, {did}')
|
raise MIoTClientError(f'did not exist, {did}')
|
||||||
@ -841,7 +866,9 @@ class MIoTClient:
|
|||||||
_LOGGER.debug('client sub event, %s', topic)
|
_LOGGER.debug('client sub event, %s', topic)
|
||||||
return True
|
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 = (
|
topic = (
|
||||||
f'{did}/e/'
|
f'{did}/e/'
|
||||||
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
||||||
@ -873,16 +900,7 @@ class MIoTClient:
|
|||||||
sub_from = self._sub_source_list.pop(did, None)
|
sub_from = self._sub_source_list.pop(did, None)
|
||||||
# Unsub
|
# Unsub
|
||||||
if sub_from:
|
if sub_from:
|
||||||
if sub_from == 'cloud':
|
self.__unsub_from(sub_from, did)
|
||||||
self._mips_cloud.unsub_prop(did=did)
|
|
||||||
self._mips_cloud.unsub_event(did=did)
|
|
||||||
elif sub_from == 'lan':
|
|
||||||
self._miot_lan.unsub_prop(did=did)
|
|
||||||
self._miot_lan.unsub_event(did=did)
|
|
||||||
elif sub_from in self._mips_local:
|
|
||||||
mips = self._mips_local[sub_from]
|
|
||||||
mips.unsub_prop(did=did)
|
|
||||||
mips.unsub_event(did=did)
|
|
||||||
# Storage
|
# Storage
|
||||||
await self._storage.save_async(
|
await self._storage.save_async(
|
||||||
domain='miot_devices',
|
domain='miot_devices',
|
||||||
@ -930,6 +948,39 @@ class MIoTClient:
|
|||||||
delay_sec, lambda: self._main_loop.create_task(
|
delay_sec, lambda: self._main_loop.create_task(
|
||||||
self.refresh_user_cert_async()))
|
self.refresh_user_cert_async()))
|
||||||
|
|
||||||
|
@final
|
||||||
|
def __unsub_from(self, sub_from: str, did: str) -> None:
|
||||||
|
mips: Any = None
|
||||||
|
if sub_from == 'cloud':
|
||||||
|
mips = self._mips_cloud
|
||||||
|
elif sub_from == 'lan':
|
||||||
|
mips = self._miot_lan
|
||||||
|
elif sub_from in self._mips_local:
|
||||||
|
mips = self._mips_local[sub_from]
|
||||||
|
if mips is not None:
|
||||||
|
try:
|
||||||
|
mips.unsub_prop(did=did)
|
||||||
|
mips.unsub_event(did=did)
|
||||||
|
except RuntimeError as e:
|
||||||
|
if 'Event loop is closed' in str(e):
|
||||||
|
# Ignore unsub exception when loop is closed
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
@final
|
||||||
|
def __sub_from(self, sub_from: str, did: str) -> None:
|
||||||
|
mips = None
|
||||||
|
if sub_from == 'cloud':
|
||||||
|
mips = self._mips_cloud
|
||||||
|
elif sub_from == 'lan':
|
||||||
|
mips = self._miot_lan
|
||||||
|
elif sub_from in self._mips_local:
|
||||||
|
mips = self._mips_local[sub_from]
|
||||||
|
if mips is not None:
|
||||||
|
mips.sub_prop(did=did, handler=self.__on_prop_msg)
|
||||||
|
mips.sub_event(did=did, handler=self.__on_event_msg)
|
||||||
|
|
||||||
@final
|
@final
|
||||||
def __update_device_msg_sub(self, did: str) -> None:
|
def __update_device_msg_sub(self, did: str) -> None:
|
||||||
if did not in self._device_list_cache:
|
if did not in self._device_list_cache:
|
||||||
@ -956,32 +1007,14 @@ class MIoTClient:
|
|||||||
and self._device_list_cloud[did].get('online', False)
|
and self._device_list_cloud[did].get('online', False)
|
||||||
):
|
):
|
||||||
from_new = 'cloud'
|
from_new = 'cloud'
|
||||||
if from_new == from_old:
|
if (from_new == from_old) and (from_new=='cloud' or from_new=='lan'):
|
||||||
# No need to update
|
# No need to update
|
||||||
return
|
return
|
||||||
# Unsub old
|
# Unsub old
|
||||||
if from_old:
|
if from_old:
|
||||||
if from_old == 'cloud':
|
self.__unsub_from(from_old, did)
|
||||||
self._mips_cloud.unsub_prop(did=did)
|
|
||||||
self._mips_cloud.unsub_event(did=did)
|
|
||||||
elif from_old == 'lan':
|
|
||||||
self._miot_lan.unsub_prop(did=did)
|
|
||||||
self._miot_lan.unsub_event(did=did)
|
|
||||||
elif from_old in self._mips_local:
|
|
||||||
mips = self._mips_local[from_old]
|
|
||||||
mips.unsub_prop(did=did)
|
|
||||||
mips.unsub_event(did=did)
|
|
||||||
# Sub new
|
# Sub new
|
||||||
if from_new == 'cloud':
|
self.__sub_from(from_new, did)
|
||||||
self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg)
|
|
||||||
self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg)
|
|
||||||
elif from_new == 'lan':
|
|
||||||
self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg)
|
|
||||||
self._miot_lan.sub_event(did=did, handler=self.__on_event_msg)
|
|
||||||
elif from_new in self._mips_local:
|
|
||||||
mips = self._mips_local[from_new]
|
|
||||||
mips.sub_prop(did=did, handler=self.__on_prop_msg)
|
|
||||||
mips.sub_event(did=did, handler=self.__on_event_msg)
|
|
||||||
self._sub_source_list[did] = from_new
|
self._sub_source_list[did] = from_new
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
'device sub changed, %s, from %s to %s', did, from_old, from_new)
|
'device sub changed, %s, from %s to %s', did, from_old, from_new)
|
||||||
@ -1081,7 +1114,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
continue
|
continue
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||||
self.__request_show_devices_changed_notify()
|
self.__request_show_devices_changed_notify()
|
||||||
@ -1091,10 +1124,12 @@ class MIoTClient:
|
|||||||
self, group_id: str, state: bool
|
self, group_id: str, state: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
|
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
|
||||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
mips = self._mips_local.get(group_id, None)
|
||||||
if mips is None:
|
if not mips:
|
||||||
_LOGGER.error(
|
_LOGGER.info(
|
||||||
'local mips state changed, mips not exist, %s', group_id)
|
'local mips state changed, mips not exist, %s', group_id)
|
||||||
|
# The connection to the central hub gateway is definitely broken.
|
||||||
|
self.__show_central_state_changed_notify(False)
|
||||||
return
|
return
|
||||||
if state:
|
if state:
|
||||||
# Connected
|
# Connected
|
||||||
@ -1124,10 +1159,11 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
continue
|
continue
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||||
self.__request_show_devices_changed_notify()
|
self.__request_show_devices_changed_notify()
|
||||||
|
self.__show_central_state_changed_notify(state)
|
||||||
|
|
||||||
@final
|
@final
|
||||||
async def __on_miot_lan_state_change(self, state: bool) -> None:
|
async def __on_miot_lan_state_change(self, state: bool) -> None:
|
||||||
@ -1171,7 +1207,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
continue
|
continue
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||||
self._device_list_lan = {}
|
self._device_list_lan = {}
|
||||||
@ -1201,7 +1237,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
return
|
return
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(
|
sub.handler(
|
||||||
did, MIoTDeviceState.ONLINE if state_new
|
did, MIoTDeviceState.ONLINE if state_new
|
||||||
@ -1257,7 +1293,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
return
|
return
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(
|
sub.handler(
|
||||||
did, MIoTDeviceState.ONLINE if state_new
|
did, MIoTDeviceState.ONLINE if state_new
|
||||||
@ -1301,9 +1337,8 @@ class MIoTClient:
|
|||||||
async def __load_cache_device_async(self) -> None:
|
async def __load_cache_device_async(self) -> None:
|
||||||
"""Load device list from cache."""
|
"""Load device list from cache."""
|
||||||
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
|
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
|
||||||
domain='miot_devices',
|
domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
|
||||||
name=f'{self._uid}_{self._cloud_server}',
|
type_=dict) # type: ignore
|
||||||
type_=dict)
|
|
||||||
if not cache_list:
|
if not cache_list:
|
||||||
self.__show_client_error_notify(
|
self.__show_client_error_notify(
|
||||||
message=self._i18n.translate(
|
message=self._i18n.translate(
|
||||||
@ -1339,6 +1374,11 @@ class MIoTClient:
|
|||||||
"""Update cloud devices.
|
"""Update cloud devices.
|
||||||
NOTICE: This function will operate the cloud_list
|
NOTICE: This function will operate the cloud_list
|
||||||
"""
|
"""
|
||||||
|
# MIoT cloud service may not publish the online state updating message
|
||||||
|
# for the BLE device. Assume that all BLE devices are online.
|
||||||
|
for did, info in cloud_list.items():
|
||||||
|
if did.startswith('blt.'):
|
||||||
|
info['online'] = True
|
||||||
for did, info in self._device_list_cache.items():
|
for did, info in self._device_list_cache.items():
|
||||||
if filter_dids and did not in filter_dids:
|
if filter_dids and did not in filter_dids:
|
||||||
continue
|
continue
|
||||||
@ -1346,7 +1386,7 @@ class MIoTClient:
|
|||||||
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
|
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
|
||||||
did, {}).get('online', None)
|
did, {}).get('online', None)
|
||||||
cloud_state_new: Optional[bool] = 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:
|
if device_new:
|
||||||
cloud_state_new = device_new.get('online', None)
|
cloud_state_new = device_new.get('online', None)
|
||||||
# Update cache device info
|
# Update cache device info
|
||||||
@ -1371,7 +1411,7 @@ class MIoTClient:
|
|||||||
continue
|
continue
|
||||||
info['online'] = state_new
|
info['online'] = state_new
|
||||||
# Call device state changed callback
|
# 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:
|
if sub and sub.handler:
|
||||||
sub.handler(
|
sub.handler(
|
||||||
did, MIoTDeviceState.ONLINE if state_new
|
did, MIoTDeviceState.ONLINE if state_new
|
||||||
@ -1426,8 +1466,7 @@ class MIoTClient:
|
|||||||
self, dids: list[str]
|
self, dids: list[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
_LOGGER.debug('refresh cloud device with dids, %s', dids)
|
_LOGGER.debug('refresh cloud device with dids, %s', dids)
|
||||||
cloud_list: dict[str, dict] = (
|
cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
|
||||||
await self._http.get_devices_with_dids_async(dids=dids))
|
|
||||||
if cloud_list is None:
|
if cloud_list is None:
|
||||||
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
|
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
|
||||||
return
|
return
|
||||||
@ -1466,11 +1505,9 @@ class MIoTClient:
|
|||||||
for did, info in self._device_list_cache.items():
|
for did, info in self._device_list_cache.items():
|
||||||
if did not in filter_dids:
|
if did not in filter_dids:
|
||||||
continue
|
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
|
gw_state_new: bool = False
|
||||||
device_new: dict = gw_list.pop(did, None)
|
device_new = gw_list.pop(did, None)
|
||||||
if device_new:
|
if device_new:
|
||||||
# Update gateway device info
|
# Update gateway device info
|
||||||
self._device_list_gateway[did] = {
|
self._device_list_gateway[did] = {
|
||||||
@ -1482,7 +1519,7 @@ class MIoTClient:
|
|||||||
device_old['online'] = False
|
device_old['online'] = False
|
||||||
# Update cache group_id
|
# Update cache group_id
|
||||||
info['group_id'] = group_id
|
info['group_id'] = group_id
|
||||||
if gw_state_old == gw_state_new:
|
if not gw_state_new:
|
||||||
continue
|
continue
|
||||||
self.__update_device_msg_sub(did=did)
|
self.__update_device_msg_sub(did=did)
|
||||||
state_old: Optional[bool] = info.get('online', None)
|
state_old: Optional[bool] = info.get('online', None)
|
||||||
@ -1493,7 +1530,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
continue
|
continue
|
||||||
info['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(
|
sub.handler(
|
||||||
did, MIoTDeviceState.ONLINE if state_new
|
did, MIoTDeviceState.ONLINE if state_new
|
||||||
@ -1518,7 +1555,7 @@ class MIoTClient:
|
|||||||
if state_old == state_new:
|
if state_old == state_new:
|
||||||
continue
|
continue
|
||||||
self._device_list_cache[did]['online'] = state_new
|
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:
|
if sub and sub.handler:
|
||||||
sub.handler(
|
sub.handler(
|
||||||
did, MIoTDeviceState.ONLINE if state_new
|
did, MIoTDeviceState.ONLINE if state_new
|
||||||
@ -1533,7 +1570,7 @@ class MIoTClient:
|
|||||||
'refresh gw devices with group_id, %s', group_id)
|
'refresh gw devices with group_id, %s', group_id)
|
||||||
# Remove timer
|
# Remove timer
|
||||||
self._mips_local_state_changed_timers.pop(group_id, None)
|
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:
|
if not mips:
|
||||||
_LOGGER.error('mips not exist, %s', group_id)
|
_LOGGER.error('mips not exist, %s', group_id)
|
||||||
return
|
return
|
||||||
@ -1892,6 +1929,23 @@ class MIoTClient:
|
|||||||
self._show_devices_changed_notify_timer = self._main_loop.call_later(
|
self._show_devices_changed_notify_timer = self._main_loop.call_later(
|
||||||
delay_sec, self.__show_devices_changed_notify)
|
delay_sec, self.__show_devices_changed_notify)
|
||||||
|
|
||||||
|
@final
|
||||||
|
def __show_central_state_changed_notify(self, connected: bool) -> None:
|
||||||
|
conn_status: str = (
|
||||||
|
self._i18n.translate('miot.client.central_state_connected')
|
||||||
|
if connected else
|
||||||
|
self._i18n.translate('miot.client.central_state_disconnected'))
|
||||||
|
self._persistence_notify(
|
||||||
|
self.__gen_notify_key('central_state_changed'),
|
||||||
|
self._i18n.translate('miot.client.central_state_changed_title'),
|
||||||
|
self._i18n.translate(key='miot.client.central_state_changed',
|
||||||
|
replace={
|
||||||
|
'nick_name': self._entry_data.get(
|
||||||
|
'nick_name', DEFAULT_NICK_NAME),
|
||||||
|
'uid': self._uid,
|
||||||
|
'cloud_server': self._cloud_server,
|
||||||
|
'conn_status': conn_status
|
||||||
|
}))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_miot_instance_async(
|
async def get_miot_instance_async(
|
||||||
@ -1900,77 +1954,73 @@ async def get_miot_instance_async(
|
|||||||
) -> MIoTClient:
|
) -> MIoTClient:
|
||||||
if entry_id is None:
|
if entry_id is None:
|
||||||
raise MIoTClientError('invalid entry_id')
|
raise MIoTClientError('invalid entry_id')
|
||||||
miot_client: MIoTClient = None
|
miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
|
||||||
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None):
|
if miot_client:
|
||||||
_LOGGER.info('instance exist, %s', entry_id)
|
_LOGGER.info('instance exist, %s', entry_id)
|
||||||
miot_client = a
|
return miot_client
|
||||||
else:
|
# Create new instance
|
||||||
if entry_data is None:
|
if not entry_data:
|
||||||
raise MIoTClientError('entry data is None')
|
raise MIoTClientError('entry data is None')
|
||||||
# Get running loop
|
# Get running loop
|
||||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||||
if loop is None:
|
if not loop:
|
||||||
raise MIoTClientError('loop is None')
|
raise MIoTClientError('loop is None')
|
||||||
# MIoT storage
|
# MIoT storage
|
||||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||||
'miot_storage', None)
|
'miot_storage', None)
|
||||||
if not storage:
|
if not storage:
|
||||||
storage = MIoTStorage(
|
storage = MIoTStorage(
|
||||||
root_path=entry_data['storage_path'], loop=loop)
|
root_path=entry_data['storage_path'], loop=loop)
|
||||||
hass.data[DOMAIN]['miot_storage'] = storage
|
hass.data[DOMAIN]['miot_storage'] = storage
|
||||||
_LOGGER.info('create miot_storage instance')
|
_LOGGER.info('create miot_storage instance')
|
||||||
global_config: dict = await storage.load_user_config_async(
|
global_config: dict = await storage.load_user_config_async(
|
||||||
uid='global_config', cloud_server='all',
|
uid='global_config', cloud_server='all',
|
||||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||||
# MIoT network
|
# MIoT network
|
||||||
network_detect_addr: dict = global_config.get(
|
network_detect_addr: dict = global_config.get('network_detect_addr', {})
|
||||||
'network_detect_addr', {})
|
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
'miot_network', None)
|
||||||
'miot_network', None)
|
if not network:
|
||||||
if not network:
|
network = MIoTNetwork(
|
||||||
network = MIoTNetwork(
|
ip_addr_list=network_detect_addr.get('ip', []),
|
||||||
ip_addr_list=network_detect_addr.get('ip', []),
|
url_addr_list=network_detect_addr.get('url', []),
|
||||||
url_addr_list=network_detect_addr.get('url', []),
|
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
loop=loop)
|
||||||
loop=loop)
|
hass.data[DOMAIN]['miot_network'] = network
|
||||||
hass.data[DOMAIN]['miot_network'] = network
|
await network.init_async()
|
||||||
await network.init_async()
|
_LOGGER.info('create miot_network instance')
|
||||||
_LOGGER.info('create miot_network instance')
|
# MIoT service
|
||||||
# MIoT service
|
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
'mips_service', None)
|
||||||
'mips_service', None)
|
if not mips_service:
|
||||||
if not mips_service:
|
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
await mips_service.init_async()
|
||||||
await mips_service.init_async()
|
_LOGGER.info('create mips_service instance')
|
||||||
_LOGGER.info('create mips_service instance')
|
# MIoT lan
|
||||||
# MIoT lan
|
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
|
||||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get(
|
if not miot_lan:
|
||||||
'miot_lan', None)
|
miot_lan = MIoTLan(
|
||||||
if not miot_lan:
|
net_ifs=global_config.get('net_interfaces', []),
|
||||||
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,
|
|
||||||
network=network,
|
network=network,
|
||||||
storage=storage,
|
|
||||||
mips_service=mips_service,
|
mips_service=mips_service,
|
||||||
miot_lan=miot_lan,
|
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||||
loop=loop
|
loop=loop)
|
||||||
)
|
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||||
miot_client.persistent_notify = persistent_notify
|
_LOGGER.info('create miot_lan instance')
|
||||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
# MIoT client
|
||||||
_LOGGER.info(
|
miot_client = MIoTClient(
|
||||||
'new miot_client instance, %s, %s', entry_id, entry_data)
|
entry_id=entry_id,
|
||||||
await miot_client.init_async()
|
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
|
return miot_client
|
||||||
|
@ -59,6 +59,7 @@ import aiohttp
|
|||||||
# pylint: disable=relative-beyond-top-level
|
# pylint: disable=relative-beyond-top-level
|
||||||
from .common import calc_group_id
|
from .common import calc_group_id
|
||||||
from .const import (
|
from .const import (
|
||||||
|
UNSUPPORTED_MODELS,
|
||||||
DEFAULT_OAUTH2_API_HOST,
|
DEFAULT_OAUTH2_API_HOST,
|
||||||
MIHOME_HTTP_API_TIMEOUT,
|
MIHOME_HTTP_API_TIMEOUT,
|
||||||
OAUTH2_AUTH_URL)
|
OAUTH2_AUTH_URL)
|
||||||
@ -382,7 +383,7 @@ class MIoTHttpClient:
|
|||||||
|
|
||||||
return res_obj['data']
|
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):
|
if not isinstance(csr, str):
|
||||||
raise MIoTHttpError('invalid params')
|
raise MIoTHttpError('invalid params')
|
||||||
|
|
||||||
@ -444,6 +445,17 @@ class MIoTHttpClient:
|
|||||||
|
|
||||||
return home_list
|
return home_list
|
||||||
|
|
||||||
|
async def get_separated_shared_devices_async(self) -> dict[str, dict]:
|
||||||
|
separated_shared_devices: dict = {}
|
||||||
|
device_list: dict[str, dict] = await self.__get_device_list_page_async(
|
||||||
|
dids=[], start_did=None)
|
||||||
|
for did, value in device_list.items():
|
||||||
|
if value['owner'] is not None and ('userid' in value['owner']) and (
|
||||||
|
'nickname' in value['owner']
|
||||||
|
):
|
||||||
|
separated_shared_devices.setdefault(did, value['owner'])
|
||||||
|
return separated_shared_devices
|
||||||
|
|
||||||
async def get_homeinfos_async(self) -> dict:
|
async def get_homeinfos_async(self) -> dict:
|
||||||
res_obj = await self.__mihome_api_post_async(
|
res_obj = await self.__mihome_api_post_async(
|
||||||
url_path='/app/v2/homeroom/gethome',
|
url_path='/app/v2/homeroom/gethome',
|
||||||
@ -499,19 +511,22 @@ class MIoTHttpClient:
|
|||||||
):
|
):
|
||||||
more_list = await self.__get_dev_room_page_async(
|
more_list = await self.__get_dev_room_page_async(
|
||||||
max_id=res_obj['result']['max_id'])
|
max_id=res_obj['result']['max_id'])
|
||||||
for home_id, info in more_list.items():
|
for device_source in ['homelist', 'share_home_list']:
|
||||||
if home_id not in home_infos['homelist']:
|
for home_id, info in more_list.items():
|
||||||
_LOGGER.info('unknown home, %s, %s', home_id, info)
|
if home_id not in home_infos[device_source]:
|
||||||
continue
|
_LOGGER.info('unknown home, %s, %s', home_id, info)
|
||||||
home_infos['homelist'][home_id]['dids'].extend(info['dids'])
|
continue
|
||||||
for room_id, info in info['room_info'].items():
|
home_infos[device_source][home_id]['dids'].extend(
|
||||||
home_infos['homelist'][home_id]['room_info'].setdefault(
|
info['dids'])
|
||||||
room_id, {
|
for room_id, info in info['room_info'].items():
|
||||||
'room_id': room_id,
|
home_infos[device_source][home_id][
|
||||||
'room_name': '',
|
'room_info'].setdefault(
|
||||||
'dids': []})
|
room_id, {
|
||||||
home_infos['homelist'][home_id]['room_info'][
|
'room_id': room_id,
|
||||||
room_id]['dids'].extend(info['dids'])
|
'room_name': '',
|
||||||
|
'dids': []})
|
||||||
|
home_infos[device_source][home_id]['room_info'][
|
||||||
|
room_id]['dids'].extend(info['dids'])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'uid': uid,
|
'uid': uid,
|
||||||
@ -528,6 +543,7 @@ class MIoTHttpClient:
|
|||||||
req_data: dict = {
|
req_data: dict = {
|
||||||
'limit': 200,
|
'limit': 200,
|
||||||
'get_split_device': True,
|
'get_split_device': True,
|
||||||
|
'get_third_device': True,
|
||||||
'dids': dids
|
'dids': dids
|
||||||
}
|
}
|
||||||
if start_did:
|
if start_did:
|
||||||
@ -559,6 +575,10 @@ class MIoTHttpClient:
|
|||||||
# were implemented.
|
# were implemented.
|
||||||
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
|
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
|
||||||
continue
|
continue
|
||||||
|
if model in UNSUPPORTED_MODELS:
|
||||||
|
_LOGGER.info('ignore unsupported model %s, cloud, %s',
|
||||||
|
model, did)
|
||||||
|
continue
|
||||||
device_infos[did] = {
|
device_infos[did] = {
|
||||||
'did': did,
|
'did': did,
|
||||||
'uid': device.get('uid', None),
|
'uid': device.get('uid', None),
|
||||||
@ -651,6 +671,25 @@ class MIoTHttpClient:
|
|||||||
'room_name': room_name,
|
'room_name': room_name,
|
||||||
'group_id': group_id
|
'group_id': group_id
|
||||||
} for did in room_info.get('dids', [])})
|
} for did in room_info.get('dids', [])})
|
||||||
|
separated_shared_devices: dict = (
|
||||||
|
await self.get_separated_shared_devices_async())
|
||||||
|
if separated_shared_devices:
|
||||||
|
homes.setdefault('separated_shared_list', {})
|
||||||
|
for did, owner in separated_shared_devices.items():
|
||||||
|
owner_id = str(owner['userid'])
|
||||||
|
homes['separated_shared_list'].setdefault(owner_id,{
|
||||||
|
'home_name': owner['nickname'],
|
||||||
|
'uid': owner_id,
|
||||||
|
'group_id': 'NotSupport',
|
||||||
|
'room_info': {'shared_device': 'shared_device'}
|
||||||
|
})
|
||||||
|
devices.update({did: {
|
||||||
|
'home_id': owner_id,
|
||||||
|
'home_name': owner['nickname'],
|
||||||
|
'room_id': 'shared_device',
|
||||||
|
'room_name': 'shared_device',
|
||||||
|
'group_id': 'NotSupport'
|
||||||
|
}})
|
||||||
dids = sorted(list(devices.keys()))
|
dids = sorted(list(devices.keys()))
|
||||||
results = await self.get_devices_with_dids_async(dids=dids)
|
results = await self.get_devices_with_dids_async(dids=dids)
|
||||||
if results is None:
|
if results is None:
|
||||||
|
@ -56,6 +56,7 @@ from homeassistant.const import (
|
|||||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
DEGREE,
|
||||||
LIGHT_LUX,
|
LIGHT_LUX,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS,
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
@ -72,6 +73,7 @@ from homeassistant.const import (
|
|||||||
UnitOfPower,
|
UnitOfPower,
|
||||||
UnitOfVolume,
|
UnitOfVolume,
|
||||||
UnitOfVolumeFlowRate,
|
UnitOfVolumeFlowRate,
|
||||||
|
UnitOfDataRate
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.components.switch import SwitchDeviceClass
|
from homeassistant.components.switch import SwitchDeviceClass
|
||||||
@ -243,12 +245,12 @@ class MIoTDevice:
|
|||||||
def sub_device_state(
|
def sub_device_state(
|
||||||
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
||||||
) -> int:
|
) -> int:
|
||||||
self._sub_id += 1
|
sub_id = self.__gen_sub_id()
|
||||||
if key in self._device_state_sub_list:
|
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:
|
else:
|
||||||
self._device_state_sub_list[key] = {str(self._sub_id): handler}
|
self._device_state_sub_list[key] = {str(sub_id): handler}
|
||||||
return self._sub_id
|
return sub_id
|
||||||
|
|
||||||
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
||||||
sub_list = self._device_state_sub_list.get(key, 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():
|
for handler in self._value_sub_list[key].values():
|
||||||
handler(params, ctx)
|
handler(params, ctx)
|
||||||
|
|
||||||
self._sub_id += 1
|
sub_id = self.__gen_sub_id()
|
||||||
if key in self._value_sub_list:
|
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:
|
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(
|
self.miot_client.sub_prop(
|
||||||
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
|
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:
|
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
|
||||||
key: str = f'p.{siid}.{piid}'
|
key: str = f'p.{siid}.{piid}'
|
||||||
@ -294,14 +296,14 @@ class MIoTDevice:
|
|||||||
for handler in self._value_sub_list[key].values():
|
for handler in self._value_sub_list[key].values():
|
||||||
handler(params, ctx)
|
handler(params, ctx)
|
||||||
|
|
||||||
self._sub_id += 1
|
sub_id = self.__gen_sub_id()
|
||||||
if key in self._value_sub_list:
|
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:
|
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(
|
self.miot_client.sub_event(
|
||||||
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
|
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:
|
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
|
||||||
key: str = f'e.{siid}.{eiid}'
|
key: str = f'e.{siid}.{eiid}'
|
||||||
@ -343,10 +345,11 @@ class MIoTDevice:
|
|||||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||||
f'{self._model_strs[-1][:20]}')
|
f'{self._model_strs[-1][:20]}')
|
||||||
|
|
||||||
def gen_service_entity_id(self, ha_domain: str, siid: int) -> str:
|
def gen_service_entity_id(self, ha_domain: str, siid: int,
|
||||||
|
description: str) -> str:
|
||||||
return (
|
return (
|
||||||
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
|
||||||
f'{self._model_strs[-1][:20]}_s_{siid}')
|
f'{self._model_strs[-1][:20]}_s_{siid}_{description}')
|
||||||
|
|
||||||
def gen_prop_entity_id(
|
def gen_prop_entity_id(
|
||||||
self, ha_domain: str, spec_name: str, siid: int, piid: int
|
self, ha_domain: str, spec_name: str, siid: int, piid: int
|
||||||
@ -414,10 +417,12 @@ class MIoTDevice:
|
|||||||
spec_name: str = spec_instance.name
|
spec_name: str = spec_instance.name
|
||||||
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
|
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
|
||||||
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
|
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.
|
# 1. The device shall have all required services.
|
||||||
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
|
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
|
||||||
if not {
|
if not {
|
||||||
service.name for service in spec_instance.services
|
service.name for service in spec_instance.services
|
||||||
}.issuperset(required_services):
|
}.issuperset(required_services):
|
||||||
return None
|
return None
|
||||||
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
|
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
|
||||||
@ -427,9 +432,13 @@ class MIoTDevice:
|
|||||||
for service in spec_instance.services:
|
for service in spec_instance.services:
|
||||||
if service.platform:
|
if service.platform:
|
||||||
continue
|
continue
|
||||||
|
required_properties: dict
|
||||||
|
optional_properties: dict
|
||||||
|
required_actions: set
|
||||||
|
optional_actions: set
|
||||||
# 2. The service shall have all required properties, actions.
|
# 2. The service shall have all required properties, actions.
|
||||||
if service.name in required_services:
|
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(
|
'required'].get(
|
||||||
service.name, {}
|
service.name, {}
|
||||||
).get('required', {}).get('properties', {})
|
).get('required', {}).get('properties', {})
|
||||||
@ -446,7 +455,7 @@ class MIoTDevice:
|
|||||||
service.name, {}
|
service.name, {}
|
||||||
).get('optional', {}).get('actions', set({}))
|
).get('optional', {}).get('actions', set({}))
|
||||||
elif service.name in optional_services:
|
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(
|
'optional'].get(
|
||||||
service.name, {}
|
service.name, {}
|
||||||
).get('required', {}).get('properties', {})
|
).get('required', {}).get('properties', {})
|
||||||
@ -484,7 +493,7 @@ class MIoTDevice:
|
|||||||
set(required_properties.keys()), optional_properties):
|
set(required_properties.keys()), optional_properties):
|
||||||
if prop.unit:
|
if prop.unit:
|
||||||
prop.external_unit = self.unit_convert(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
|
prop.platform = platform
|
||||||
entity_data.props.add(prop)
|
entity_data.props.add(prop)
|
||||||
# action
|
# action
|
||||||
@ -499,85 +508,94 @@ class MIoTDevice:
|
|||||||
return entity_data
|
return entity_data
|
||||||
|
|
||||||
def parse_miot_service_entity(
|
def parse_miot_service_entity(
|
||||||
self, service_instance: MIoTSpecService
|
self, miot_service: MIoTSpecService
|
||||||
) -> Optional[MIoTEntityData]:
|
) -> Optional[MIoTEntityData]:
|
||||||
service = service_instance
|
if (
|
||||||
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP):
|
miot_service.platform
|
||||||
|
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
service_name = miot_service.name
|
||||||
service_name = service.name
|
|
||||||
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
|
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
|
||||||
service_name = SPEC_SERVICE_TRANS_MAP[service_name]
|
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_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||||
'required'].get('properties', {})
|
'required'].get('properties', {})
|
||||||
if not {
|
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())):
|
}.issuperset(set(required_properties.keys())):
|
||||||
return None
|
return None
|
||||||
# 2. The required property shall have all required access mode.
|
for prop in miot_service.properties:
|
||||||
for prop in service.properties:
|
|
||||||
if prop.name in required_properties:
|
if prop.name in required_properties:
|
||||||
if not set(prop.access).issuperset(
|
if not set(prop.access).issuperset(
|
||||||
required_properties[prop.name]):
|
required_properties[prop.name]):
|
||||||
return None
|
return None
|
||||||
|
# Required actions
|
||||||
|
# Required events
|
||||||
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
|
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_properties = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||||
'optional'].get('properties', set({}))
|
'optional'].get('properties', set({}))
|
||||||
for prop in service.properties:
|
for prop in miot_service.properties:
|
||||||
if prop.name in set.union(
|
if prop.name in set.union(
|
||||||
set(required_properties.keys()), optional_properties):
|
set(required_properties.keys()), optional_properties):
|
||||||
if prop.unit:
|
if prop.unit:
|
||||||
prop.external_unit = self.unit_convert(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
|
prop.platform = platform
|
||||||
entity_data.props.add(prop)
|
entity_data.props.add(prop)
|
||||||
# action
|
# Optional actions
|
||||||
# event
|
# Optional events
|
||||||
# No actions or events is in SPEC_SERVICE_TRANS_MAP now.
|
miot_service.platform = platform
|
||||||
service.platform = platform
|
# entity_category
|
||||||
|
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
|
||||||
|
'entity_category', None):
|
||||||
|
miot_service.entity_category = entity_category
|
||||||
return entity_data
|
return entity_data
|
||||||
|
|
||||||
def parse_miot_property_entity(
|
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
|
||||||
self, property_instance: MIoTSpecProperty
|
|
||||||
) -> Optional[dict[str, str]]:
|
|
||||||
prop = property_instance
|
|
||||||
if (
|
if (
|
||||||
prop.platform
|
miot_prop.platform
|
||||||
or (prop.name not in SPEC_PROP_TRANS_MAP['properties'])
|
or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
|
||||||
):
|
):
|
||||||
return None
|
return False
|
||||||
|
prop_name = miot_prop.name
|
||||||
prop_name = prop.name
|
|
||||||
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
|
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
|
||||||
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||||
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
|
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
|
||||||
|
# Check
|
||||||
prop_access: set = set({})
|
prop_access: set = set({})
|
||||||
if prop.readable:
|
if miot_prop.readable:
|
||||||
prop_access.add('read')
|
prop_access.add('read')
|
||||||
if prop.writable:
|
if miot_prop.writable:
|
||||||
prop_access.add('write')
|
prop_access.add('write')
|
||||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||||
'entities'][platform]['access']):
|
'entities'][platform]['access']):
|
||||||
return None
|
return False
|
||||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||||
'entities'][platform]['format']:
|
'entities'][platform]['format']:
|
||||||
return None
|
return False
|
||||||
if prop.unit:
|
miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||||
prop.external_unit = self.unit_convert(prop.unit)
|
|
||||||
prop.icon = self.icon_convert(prop.unit)
|
|
||||||
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
|
||||||
'device_class']
|
'device_class']
|
||||||
result = {'platform': platform, 'device_class': device_class}
|
# Optional params
|
||||||
# optional:
|
if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||||
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
|
||||||
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional']
|
prop_name]['state_class']
|
||||||
if 'state_class' in optional:
|
if (
|
||||||
result['state_class'] = optional['state_class']
|
not miot_prop.external_unit
|
||||||
if not prop.unit and 'unit_of_measurement' in optional:
|
and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
|
||||||
result['unit_of_measurement'] = optional['unit_of_measurement']
|
prop_name]
|
||||||
return result
|
):
|
||||||
|
# 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:
|
def spec_transform(self) -> None:
|
||||||
"""Parse service, property, event, action from device spec."""
|
"""Parse service, property, event, action from device spec."""
|
||||||
@ -589,7 +607,7 @@ class MIoTDevice:
|
|||||||
# STEP 2: service conversion
|
# STEP 2: service conversion
|
||||||
for service in self.spec_instance.services:
|
for service in self.spec_instance.services:
|
||||||
service_entity = self.parse_miot_service_entity(
|
service_entity = self.parse_miot_service_entity(
|
||||||
service_instance=service)
|
miot_service=service)
|
||||||
if service_entity:
|
if service_entity:
|
||||||
self.append_entity(entity_data=service_entity)
|
self.append_entity(entity_data=service_entity)
|
||||||
# STEP 3.1: property conversion
|
# STEP 3.1: property conversion
|
||||||
@ -598,20 +616,11 @@ class MIoTDevice:
|
|||||||
continue
|
continue
|
||||||
if prop.unit:
|
if prop.unit:
|
||||||
prop.external_unit = self.unit_convert(prop.unit)
|
prop.external_unit = self.unit_convert(prop.unit)
|
||||||
prop.icon = self.icon_convert(prop.unit)
|
if not prop.icon:
|
||||||
prop_entity = self.parse_miot_property_entity(
|
prop.icon = self.icon_convert(prop.unit)
|
||||||
property_instance=prop)
|
# Special conversion
|
||||||
if prop_entity:
|
self.parse_miot_property_entity(miot_prop=prop)
|
||||||
prop.platform = prop_entity['platform']
|
# General conversion
|
||||||
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.platform:
|
if not prop.platform:
|
||||||
if prop.writable:
|
if prop.writable:
|
||||||
if prop.format_ == str:
|
if prop.format_ == str:
|
||||||
@ -625,7 +634,7 @@ class MIoTDevice:
|
|||||||
prop.platform = 'number'
|
prop.platform = 'number'
|
||||||
else:
|
else:
|
||||||
# Irregular property will not be transformed.
|
# Irregular property will not be transformed.
|
||||||
pass
|
continue
|
||||||
elif prop.readable or prop.notifiable:
|
elif prop.readable or prop.notifiable:
|
||||||
if prop.format_ == bool:
|
if prop.format_ == bool:
|
||||||
prop.platform = 'binary_sensor'
|
prop.platform = 'binary_sensor'
|
||||||
@ -653,11 +662,66 @@ class MIoTDevice:
|
|||||||
self.append_action(action=action)
|
self.append_action(action=action)
|
||||||
|
|
||||||
def unit_convert(self, spec_unit: str) -> Optional[str]:
|
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 = {
|
unit_map = {
|
||||||
'percentage': PERCENTAGE,
|
'percentage': PERCENTAGE,
|
||||||
'weeks': UnitOfTime.WEEKS,
|
'weeks': UnitOfTime.WEEKS,
|
||||||
'days': UnitOfTime.DAYS,
|
'days': UnitOfTime.DAYS,
|
||||||
|
'hour': UnitOfTime.HOURS,
|
||||||
'hours': UnitOfTime.HOURS,
|
'hours': UnitOfTime.HOURS,
|
||||||
'minutes': UnitOfTime.MINUTES,
|
'minutes': UnitOfTime.MINUTES,
|
||||||
'seconds': UnitOfTime.SECONDS,
|
'seconds': UnitOfTime.SECONDS,
|
||||||
@ -672,90 +736,117 @@ class MIoTDevice:
|
|||||||
'ppb': CONCENTRATION_PARTS_PER_BILLION,
|
'ppb': CONCENTRATION_PARTS_PER_BILLION,
|
||||||
'lux': LIGHT_LUX,
|
'lux': LIGHT_LUX,
|
||||||
'pascal': UnitOfPressure.PA,
|
'pascal': UnitOfPressure.PA,
|
||||||
|
'kilopascal': UnitOfPressure.KPA,
|
||||||
|
'mmHg': UnitOfPressure.MMHG,
|
||||||
'bar': UnitOfPressure.BAR,
|
'bar': UnitOfPressure.BAR,
|
||||||
'watt': UnitOfPower.WATT,
|
|
||||||
'L': UnitOfVolume.LITERS,
|
'L': UnitOfVolume.LITERS,
|
||||||
|
'liter': UnitOfVolume.LITERS,
|
||||||
'mL': UnitOfVolume.MILLILITERS,
|
'mL': UnitOfVolume.MILLILITERS,
|
||||||
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
|
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
'm/s': UnitOfSpeed.METERS_PER_SECOND,
|
'm/s': UnitOfSpeed.METERS_PER_SECOND,
|
||||||
|
'watt': UnitOfPower.WATT,
|
||||||
|
'w': UnitOfPower.WATT,
|
||||||
|
'W': UnitOfPower.WATT,
|
||||||
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
|
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
'A': UnitOfElectricCurrent.AMPERE,
|
'A': UnitOfElectricCurrent.AMPERE,
|
||||||
'mA': UnitOfElectricCurrent.MILLIAMPERE,
|
'mA': UnitOfElectricCurrent.MILLIAMPERE,
|
||||||
'V': UnitOfElectricPotential.VOLT,
|
'V': UnitOfElectricPotential.VOLT,
|
||||||
|
'mv': UnitOfElectricPotential.MILLIVOLT,
|
||||||
'mV': UnitOfElectricPotential.MILLIVOLT,
|
'mV': UnitOfElectricPotential.MILLIVOLT,
|
||||||
|
'cm': UnitOfLength.CENTIMETERS,
|
||||||
'm': UnitOfLength.METERS,
|
'm': UnitOfLength.METERS,
|
||||||
|
'meter': UnitOfLength.METERS,
|
||||||
'km': UnitOfLength.KILOMETERS,
|
'km': UnitOfLength.KILOMETERS,
|
||||||
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||||
'gram': UnitOfMass.GRAMS,
|
'gram': UnitOfMass.GRAMS,
|
||||||
|
'kilogram': UnitOfMass.KILOGRAMS,
|
||||||
'dB': SIGNAL_STRENGTH_DECIBELS,
|
'dB': SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
'arcdegrees': DEGREE,
|
||||||
|
'arcdegress': DEGREE,
|
||||||
'kB': UnitOfInformation.KILOBYTES,
|
'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
|
# Handle UnitOfConductivity separately since
|
||||||
# it might not be available in all HA versions
|
# it might not be available in all HA versions
|
||||||
try:
|
try:
|
||||||
# pylint: disable=import-outside-toplevel
|
# 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['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
|
||||||
|
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
unit_map['μS/cm'] = 'μS/cm'
|
unit_map['μS/cm'] = 'μS/cm'
|
||||||
|
unit_map['mWh'] = 'mWh'
|
||||||
|
|
||||||
return unit_map.get(spec_unit, None)
|
return unit_map.get(spec_unit, None)
|
||||||
|
|
||||||
def icon_convert(self, spec_unit: str) -> Optional[str]:
|
def icon_convert(self, spec_unit: str) -> Optional[str]:
|
||||||
if spec_unit in ['percentage']:
|
if spec_unit in {'percentage'}:
|
||||||
return 'mdi:percent'
|
return 'mdi:percent'
|
||||||
if spec_unit in [
|
if spec_unit in {
|
||||||
'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']:
|
'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
|
||||||
|
}:
|
||||||
return 'mdi:clock'
|
return 'mdi:clock'
|
||||||
if spec_unit in ['celsius']:
|
if spec_unit in {'celsius'}:
|
||||||
return 'mdi:temperature-celsius'
|
return 'mdi:temperature-celsius'
|
||||||
if spec_unit in ['fahrenheit']:
|
if spec_unit in {'fahrenheit'}:
|
||||||
return 'mdi:temperature-fahrenheit'
|
return 'mdi:temperature-fahrenheit'
|
||||||
if spec_unit in ['kelvin']:
|
if spec_unit in {'kelvin'}:
|
||||||
return 'mdi:temperature-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'
|
return 'mdi:blur'
|
||||||
if spec_unit in ['lux']:
|
if spec_unit in {'lux'}:
|
||||||
return 'mdi:brightness-6'
|
return 'mdi:brightness-6'
|
||||||
if spec_unit in ['pascal', 'megapascal', 'bar']:
|
if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
|
||||||
return 'mdi:gauge'
|
return 'mdi:gauge'
|
||||||
if spec_unit in ['watt']:
|
if spec_unit in {'watt', 'w', 'W'}:
|
||||||
return 'mdi:flash-triangle'
|
return 'mdi:flash-triangle'
|
||||||
if spec_unit in ['L', 'mL']:
|
if spec_unit in {'L', 'mL'}:
|
||||||
return 'mdi:gas-cylinder'
|
return 'mdi:gas-cylinder'
|
||||||
if spec_unit in ['km/h', 'm/s']:
|
if spec_unit in {'km/h', 'm/s'}:
|
||||||
return 'mdi:speedometer'
|
return 'mdi:speedometer'
|
||||||
if spec_unit in ['kWh']:
|
if spec_unit in {'kWh'}:
|
||||||
return 'mdi:transmission-tower'
|
return 'mdi:transmission-tower'
|
||||||
if spec_unit in ['A', 'mA']:
|
if spec_unit in {'A', 'mA'}:
|
||||||
return 'mdi:current-ac'
|
return 'mdi:current-ac'
|
||||||
if spec_unit in ['V', 'mV']:
|
if spec_unit in {'V', 'mv', 'mV'}:
|
||||||
return 'mdi:current-dc'
|
return 'mdi:current-dc'
|
||||||
if spec_unit in ['m', 'km']:
|
if spec_unit in {'cm', 'm', 'meter', 'km'}:
|
||||||
return 'mdi:ruler'
|
return 'mdi:ruler'
|
||||||
if spec_unit in ['rgb']:
|
if spec_unit in {'rgb'}:
|
||||||
return 'mdi:palette'
|
return 'mdi:palette'
|
||||||
if spec_unit in ['m3/h', 'L/s']:
|
if spec_unit in {'m3/h', 'L/s'}:
|
||||||
return 'mdi:pipe-leak'
|
return 'mdi:pipe-leak'
|
||||||
if spec_unit in ['μS/cm']:
|
if spec_unit in {'μS/cm'}:
|
||||||
return 'mdi:resistor-nodes'
|
return 'mdi:resistor-nodes'
|
||||||
if spec_unit in ['gram']:
|
if spec_unit in {'gram', 'kilogram'}:
|
||||||
return 'mdi:weight'
|
return 'mdi:weight'
|
||||||
if spec_unit in ['dB']:
|
if spec_unit in {'dB'}:
|
||||||
return 'mdi:signal-distance-variant'
|
return 'mdi:signal-distance-variant'
|
||||||
if spec_unit in ['times']:
|
if spec_unit in {'times'}:
|
||||||
return 'mdi:counter'
|
return 'mdi:counter'
|
||||||
if spec_unit in ['mmol/L']:
|
if spec_unit in {'mmol/L'}:
|
||||||
return 'mdi:dots-hexagon'
|
return 'mdi:dots-hexagon'
|
||||||
if spec_unit in ['arcdegress']:
|
if spec_unit in {'kB', 'MB', 'GB'}:
|
||||||
return 'mdi:angle-obtuse'
|
|
||||||
if spec_unit in ['kB']:
|
|
||||||
return 'mdi:network-pos'
|
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 'mdi:food'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def __gen_sub_id(self) -> int:
|
||||||
|
self._sub_id += 1
|
||||||
|
return self._sub_id
|
||||||
|
|
||||||
def __on_device_state_changed(
|
def __on_device_state_changed(
|
||||||
self, did: str, state: MIoTDeviceState, ctx: Any
|
self, did: str, state: MIoTDeviceState, ctx: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -806,10 +897,12 @@ class MIoTServiceEntity(Entity):
|
|||||||
self._attr_name = f' {self.entity_data.spec.description_trans}'
|
self._attr_name = f' {self.entity_data.spec.description_trans}'
|
||||||
elif isinstance(self.entity_data.spec, MIoTSpecService):
|
elif isinstance(self.entity_data.spec, MIoTSpecService):
|
||||||
self.entity_id = miot_device.gen_service_entity_id(
|
self.entity_id = miot_device.gen_service_entity_id(
|
||||||
DOMAIN, siid=self.entity_data.spec.iid)
|
DOMAIN, siid=self.entity_data.spec.iid,
|
||||||
|
description=self.entity_data.spec.description)
|
||||||
self._attr_name = (
|
self._attr_name = (
|
||||||
f'{"* "if self.entity_data.spec.proprietary else " "}'
|
f'{"* "if self.entity_data.spec.proprietary else " "}'
|
||||||
f'{self.entity_data.spec.description_trans}')
|
f'{self.entity_data.spec.description_trans}')
|
||||||
|
self._attr_entity_category = entity_data.spec.entity_category
|
||||||
# Set entity attr
|
# Set entity attr
|
||||||
self._attr_unique_id = self.entity_id
|
self._attr_unique_id = self.entity_id
|
||||||
self._attr_should_poll = False
|
self._attr_should_poll = False
|
||||||
@ -903,14 +996,14 @@ class MIoTServiceEntity(Entity):
|
|||||||
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
|
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
|
||||||
|
|
||||||
def get_map_value(
|
def get_map_value(
|
||||||
self, map_: dict[int, Any], key: int
|
self, map_: Optional[dict[int, Any]], key: int
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if map_ is None:
|
if map_ is None:
|
||||||
return None
|
return None
|
||||||
return map_.get(key, None)
|
return map_.get(key, None)
|
||||||
|
|
||||||
def get_map_key(
|
def get_map_key(
|
||||||
self, map_: dict[int, Any], value: Any
|
self, map_: Optional[dict[int, Any]], value: Any
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
if map_ is None:
|
if map_ is None:
|
||||||
return None
|
return None
|
||||||
@ -919,7 +1012,7 @@ class MIoTServiceEntity(Entity):
|
|||||||
return key
|
return key
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prop_value(self, prop: MIoTSpecProperty) -> Any:
|
def get_prop_value(self, prop: Optional[MIoTSpecProperty]) -> Any:
|
||||||
if not prop:
|
if not prop:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'get_prop_value error, property is None, %s, %s',
|
'get_prop_value error, property is None, %s, %s',
|
||||||
@ -927,7 +1020,9 @@ class MIoTServiceEntity(Entity):
|
|||||||
return None
|
return None
|
||||||
return self._prop_value_map.get(prop, 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:
|
if not prop:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'set_prop_value error, property is None, %s, %s',
|
'set_prop_value error, property is None, %s, %s',
|
||||||
@ -936,13 +1031,14 @@ class MIoTServiceEntity(Entity):
|
|||||||
self._prop_value_map[prop] = value
|
self._prop_value_map[prop] = value
|
||||||
|
|
||||||
async def set_property_async(
|
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:
|
) -> bool:
|
||||||
value = prop.value_format(value)
|
|
||||||
if not prop:
|
if not prop:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'set property failed, property is None, '
|
f'set property failed, property is None, '
|
||||||
f'{self.entity_id}, {self.name}')
|
f'{self.entity_id}, {self.name}')
|
||||||
|
value = prop.value_format(value)
|
||||||
if prop not in self.entity_data.props:
|
if prop not in self.entity_data.props:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'set property failed, unknown property, '
|
f'set property failed, unknown property, '
|
||||||
@ -958,8 +1054,9 @@ class MIoTServiceEntity(Entity):
|
|||||||
except MIoTClientError as e:
|
except MIoTClientError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'{e}, {self.entity_id}, {self.name}, {prop.name}') from e
|
f'{e}, {self.entity_id}, {self.name}, {prop.name}') from e
|
||||||
if update:
|
if update_value:
|
||||||
self._prop_value_map[prop] = value
|
self._prop_value_map[prop] = value
|
||||||
|
if write_ha_state:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1110,10 +1207,9 @@ class MIoTPropertyEntity(Entity):
|
|||||||
self._attr_available = miot_device.online
|
self._attr_available = miot_device.online
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
'new miot property entity, %s, %s, %s, %s, %s, %s, %s',
|
'new miot property entity, %s, %s, %s, %s, %s',
|
||||||
self.miot_device.name, self._attr_name, spec.platform,
|
self.miot_device.name, self._attr_name, spec.platform,
|
||||||
spec.device_class, self.entity_id, self._value_range,
|
spec.device_class, self.entity_id)
|
||||||
self._value_list)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> Optional[DeviceInfo]:
|
def device_info(self) -> Optional[DeviceInfo]:
|
||||||
@ -1184,6 +1280,7 @@ class MIoTPropertyEntity(Entity):
|
|||||||
def __on_value_changed(self, params: dict, ctx: Any) -> None:
|
def __on_value_changed(self, params: dict, ctx: Any) -> None:
|
||||||
_LOGGER.debug('property changed, %s', params)
|
_LOGGER.debug('property changed, %s', params)
|
||||||
self._value = self.spec.value_format(params['value'])
|
self._value = self.spec.value_format(params['value'])
|
||||||
|
self._value = self.spec.eval_expr(self._value)
|
||||||
if not self._pending_write_ha_state_timer:
|
if not self._pending_write_ha_state_timer:
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ class _MIoTLanDevice:
|
|||||||
def gen_packet(
|
def gen_packet(
|
||||||
self, out_buffer: bytearray, clear_data: dict, did: str, offset: int
|
self, out_buffer: bytearray, clear_data: dict, did: str, offset: int
|
||||||
) -> int:
|
) -> int:
|
||||||
clear_bytes = json.dumps(clear_data).encode('utf-8')
|
clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode('utf-8')
|
||||||
padder = padding.PKCS7(algorithms.AES128.block_size).padder()
|
padder = padding.PKCS7(algorithms.AES128.block_size).padder()
|
||||||
padded_data = padder.update(clear_bytes) + padder.finalize()
|
padded_data = padder.update(clear_bytes) + padder.finalize()
|
||||||
if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer):
|
if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer):
|
||||||
|
@ -110,7 +110,6 @@ class MipsServiceData:
|
|||||||
version=IPVersion.V4Only)
|
version=IPVersion.V4Only)
|
||||||
if not self.addresses:
|
if not self.addresses:
|
||||||
raise MipsServiceError('invalid addresses')
|
raise MipsServiceError('invalid addresses')
|
||||||
self.addresses.sort()
|
|
||||||
if not service_info.port:
|
if not service_info.port:
|
||||||
raise MipsServiceError('invalid port')
|
raise MipsServiceError('invalid port')
|
||||||
self.port = service_info.port
|
self.port = service_info.port
|
||||||
@ -226,7 +225,7 @@ class MipsService:
|
|||||||
state_change: ServiceStateChange
|
state_change: ServiceStateChange
|
||||||
) -> None:
|
) -> None:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
'mips service state changed, %s, %s, %s',
|
'mdns discovery changed, %s, %s, %s',
|
||||||
state_change, name, service_type)
|
state_change, name, service_type)
|
||||||
|
|
||||||
if state_change is ServiceStateChange.Removed:
|
if state_change is ServiceStateChange.Removed:
|
||||||
|
@ -60,6 +60,7 @@ from typing import Any, Callable, Optional, final, Coroutine
|
|||||||
|
|
||||||
from paho.mqtt.client import (
|
from paho.mqtt.client import (
|
||||||
MQTT_ERR_SUCCESS,
|
MQTT_ERR_SUCCESS,
|
||||||
|
MQTT_ERR_NO_CONN,
|
||||||
MQTT_ERR_UNKNOWN,
|
MQTT_ERR_UNKNOWN,
|
||||||
Client,
|
Client,
|
||||||
MQTTv5,
|
MQTTv5,
|
||||||
@ -67,7 +68,7 @@ from paho.mqtt.client import (
|
|||||||
|
|
||||||
# pylint: disable=relative-beyond-top-level
|
# pylint: disable=relative-beyond-top-level
|
||||||
from .common import MIoTMatcher
|
from .common import MIoTMatcher
|
||||||
from .const import MIHOME_MQTT_KEEPALIVE
|
from .const import UNSUPPORTED_MODELS, MIHOME_MQTT_KEEPALIVE
|
||||||
from .miot_error import MIoTErrorCode, MIoTMipsError
|
from .miot_error import MIoTErrorCode, MIoTMipsError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -215,7 +216,7 @@ class _MipsClient(ABC):
|
|||||||
MQTT_INTERVAL_S = 1
|
MQTT_INTERVAL_S = 1
|
||||||
MIPS_QOS: int = 2
|
MIPS_QOS: int = 2
|
||||||
UINT32_MAX: int = 0xFFFFFFFF
|
UINT32_MAX: int = 0xFFFFFFFF
|
||||||
MIPS_RECONNECT_INTERVAL_MIN: float = 30
|
MIPS_RECONNECT_INTERVAL_MIN: float = 10
|
||||||
MIPS_RECONNECT_INTERVAL_MAX: float = 600
|
MIPS_RECONNECT_INTERVAL_MAX: float = 600
|
||||||
MIPS_SUB_PATCH: int = 300
|
MIPS_SUB_PATCH: int = 300
|
||||||
MIPS_SUB_INTERVAL: float = 1
|
MIPS_SUB_INTERVAL: float = 1
|
||||||
@ -533,7 +534,7 @@ class _MipsClient(ABC):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
result, mid = self._mqtt.unsubscribe(topic=topic)
|
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(
|
self.log_debug(
|
||||||
f'mips unsub internal success, {result}, {mid}, {topic}')
|
f'mips unsub internal success, {result}, {mid}, {topic}')
|
||||||
return
|
return
|
||||||
@ -640,6 +641,7 @@ class _MipsClient(ABC):
|
|||||||
if not self._mqtt.is_connected():
|
if not self._mqtt.is_connected():
|
||||||
return
|
return
|
||||||
self.log_info(f'mips connect, {flags}, {rc}, {props}')
|
self.log_info(f'mips connect, {flags}, {rc}, {props}')
|
||||||
|
self.__reset_reconnect_time()
|
||||||
self._mqtt_state = True
|
self._mqtt_state = True
|
||||||
self._internal_loop.call_soon(
|
self._internal_loop.call_soon(
|
||||||
self._on_mips_connect, rc, props)
|
self._on_mips_connect, rc, props)
|
||||||
@ -821,7 +823,7 @@ class _MipsClient(ABC):
|
|||||||
self._internal_loop.stop()
|
self._internal_loop.stop()
|
||||||
|
|
||||||
def __get_next_reconnect_time(self) -> float:
|
def __get_next_reconnect_time(self) -> float:
|
||||||
if self._mips_reconnect_interval == 0:
|
if self._mips_reconnect_interval < self.MIPS_RECONNECT_INTERVAL_MIN:
|
||||||
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
|
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
|
||||||
else:
|
else:
|
||||||
self._mips_reconnect_interval = min(
|
self._mips_reconnect_interval = min(
|
||||||
@ -829,6 +831,9 @@ class _MipsClient(ABC):
|
|||||||
self.MIPS_RECONNECT_INTERVAL_MAX)
|
self.MIPS_RECONNECT_INTERVAL_MAX)
|
||||||
return self._mips_reconnect_interval
|
return self._mips_reconnect_interval
|
||||||
|
|
||||||
|
def __reset_reconnect_time(self) -> None:
|
||||||
|
self._mips_reconnect_interval = 0
|
||||||
|
|
||||||
|
|
||||||
class MipsCloudClient(_MipsClient):
|
class MipsCloudClient(_MipsClient):
|
||||||
"""MIoT Pub/Sub Cloud Client."""
|
"""MIoT Pub/Sub Cloud Client."""
|
||||||
@ -989,6 +994,11 @@ class MipsCloudClient(_MipsClient):
|
|||||||
handler(
|
handler(
|
||||||
did, MIoTDeviceState.ONLINE if msg['event'] == 'online'
|
did, MIoTDeviceState.ONLINE if msg['event'] == 'online'
|
||||||
else MIoTDeviceState.OFFLINE, ctx)
|
else MIoTDeviceState.OFFLINE, ctx)
|
||||||
|
|
||||||
|
if did.startswith('blt.'):
|
||||||
|
# MIoT cloud may not publish BLE device online/offline state message.
|
||||||
|
# Do not subscribe BLE device online/offline state.
|
||||||
|
return True
|
||||||
return self.__reg_broadcast_external(
|
return self.__reg_broadcast_external(
|
||||||
topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)
|
topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)
|
||||||
|
|
||||||
@ -1173,7 +1183,7 @@ class MipsLocalClient(_MipsClient):
|
|||||||
or 'piid' not in msg
|
or 'piid' not in msg
|
||||||
or 'value' not in msg
|
or 'value' not in msg
|
||||||
):
|
):
|
||||||
# self.log_error(f'on_prop_msg, recv unknown msg, {payload}')
|
self.log_info('unknown prop msg, %s', payload)
|
||||||
return
|
return
|
||||||
if handler:
|
if handler:
|
||||||
self.log_debug('local, on properties_changed, %s', payload)
|
self.log_debug('local, on properties_changed, %s', payload)
|
||||||
@ -1213,10 +1223,13 @@ class MipsLocalClient(_MipsClient):
|
|||||||
or 'did' not in msg
|
or 'did' not in msg
|
||||||
or 'siid' not in msg
|
or 'siid' not in msg
|
||||||
or 'eiid' not in msg
|
or 'eiid' not in msg
|
||||||
or 'arguments' not in msg
|
# or 'arguments' not in msg
|
||||||
):
|
):
|
||||||
# self.log_error(f'on_event_msg, recv unknown msg, {payload}')
|
self.log_info('unknown event msg, %s', payload)
|
||||||
return
|
return
|
||||||
|
if 'arguments' not in msg:
|
||||||
|
self.log_info('wrong event msg, %s', payload)
|
||||||
|
msg['arguments'] = []
|
||||||
if handler:
|
if handler:
|
||||||
self.log_debug('local, on event_occurred, %s', payload)
|
self.log_debug('local, on event_occurred, %s', payload)
|
||||||
handler(msg, ctx)
|
handler(msg, ctx)
|
||||||
@ -1357,6 +1370,9 @@ class MipsLocalClient(_MipsClient):
|
|||||||
if name is None or urn is None or model is None:
|
if name is None or urn is None or model is None:
|
||||||
self.log_error(f'invalid device info, {did}, {info}')
|
self.log_error(f'invalid device info, {did}, {info}')
|
||||||
continue
|
continue
|
||||||
|
if model in UNSUPPORTED_MODELS:
|
||||||
|
self.log_info(f'unsupported model, {model}, {did}')
|
||||||
|
continue
|
||||||
device_list[did] = {
|
device_list[did] = {
|
||||||
'did': did,
|
'did': did,
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -1414,7 +1430,7 @@ class MipsLocalClient(_MipsClient):
|
|||||||
@final
|
@final
|
||||||
@on_dev_list_changed.setter
|
@on_dev_list_changed.setter
|
||||||
def on_dev_list_changed(
|
def on_dev_list_changed(
|
||||||
self, func: Callable[[Any, list[str]], Coroutine]
|
self, func: Optional[Callable[[Any, list[str]], Coroutine]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""run in main loop."""
|
"""run in main loop."""
|
||||||
self._on_dev_list_changed = func
|
self._on_dev_list_changed = func
|
||||||
|
@ -94,7 +94,7 @@ class MIoTNetwork:
|
|||||||
_main_loop: asyncio.AbstractEventLoop
|
_main_loop: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
_ip_addr_map: dict[str, float]
|
_ip_addr_map: dict[str, float]
|
||||||
_url_addr_list: dict[str, float]
|
_http_addr_map: dict[str, float]
|
||||||
_http_session: aiohttp.ClientSession
|
_http_session: aiohttp.ClientSession
|
||||||
|
|
||||||
_refresh_interval: int
|
_refresh_interval: int
|
||||||
@ -283,8 +283,8 @@ class MIoTNetwork:
|
|||||||
[
|
[
|
||||||
'ping', '-c', '1', '-w',
|
'ping', '-c', '1', '-w',
|
||||||
str(self._DETECT_TIMEOUT), address]),
|
str(self._DETECT_TIMEOUT), address]),
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE
|
stderr=subprocess.DEVNULL
|
||||||
)
|
)
|
||||||
await process.communicate()
|
await process.communicate()
|
||||||
if process.returncode == 0:
|
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:wifi-ssid-hidden:000000E3: yes_no
|
||||||
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
||||||
translate:
|
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:
|
default:
|
||||||
de:
|
de:
|
||||||
'false': Falsch
|
'false': Falsch
|
||||||
@ -133,6 +96,43 @@ translate:
|
|||||||
zh-Hant:
|
zh-Hant:
|
||||||
'false': 假
|
'false': 假
|
||||||
'true': 真
|
'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:
|
motion_state:
|
||||||
de:
|
de:
|
||||||
'false': Keine Bewegung erkannt
|
'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": "强力"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
304
custom_components/xiaomi_home/miot/specs/spec_add.json
Normal file
304
custom_components/xiaomi_home/miot/specs/spec_add.json
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
{
|
||||||
|
"urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:1": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:service:switch:0000780C:090615-ktf:1",
|
||||||
|
"description": "AC Switch",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 1,
|
||||||
|
"type": "urn:miot-spec-v2:property:on:00000006:090615-ktf:1",
|
||||||
|
"description": "Switch Status",
|
||||||
|
"format": "bool",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"notify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"iid": 4,
|
||||||
|
"type": "urn:miot-spec-v2:service:environment:0000780A:090615-ktf:1",
|
||||||
|
"description": "Environment",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:property:temperature:00000020:090615-ktf:1",
|
||||||
|
"description": "Temperature",
|
||||||
|
"format": "float",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"notify"
|
||||||
|
],
|
||||||
|
"unit": "celsius",
|
||||||
|
"value-range": [
|
||||||
|
-30,
|
||||||
|
100,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"urn:miot-spec-v2:device:air-conditioner:0000A004:juhl-hvac:1": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:service:switch:0000780C:juhl-hvac:1",
|
||||||
|
"description": "AC Switch",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 1,
|
||||||
|
"type": "urn:miot-spec-v2:property:on:00000006:juhl-hvac:1",
|
||||||
|
"description": "Switch Status",
|
||||||
|
"format": "bool",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"notify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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:viomi-m1:2": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:service:switch:0000780C:viomi-m1:1",
|
||||||
|
"description": "Water Heater",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 6,
|
||||||
|
"type": "urn:miot-spec-v2:property:on:00000006:viomi-m1:1",
|
||||||
|
"description": "Switch Status",
|
||||||
|
"format": "bool",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"notify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:service:switch:0000780C:xiaomi-yms2:1",
|
||||||
|
"description": "Switch",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 6,
|
||||||
|
"type": "urn:miot-spec-v2:property:on:00000006:xiaomi-yms2:1",
|
||||||
|
"description": "Switch Status",
|
||||||
|
"format": "bool",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"notify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"urn:miot-spec-v2:device:water-heater:0000A02A:zimi-h03:1": [
|
||||||
|
{
|
||||||
|
"iid": 2,
|
||||||
|
"type": "urn:miot-spec-v2:service:switch:0000780C:zimi-h03:1",
|
||||||
|
"description": "Switch",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"iid": 6,
|
||||||
|
"type": "urn:miot-spec-v2:property:on:00000006:zimi-h03:1",
|
||||||
|
"description": "Switch Status",
|
||||||
|
"format": "bool",
|
||||||
|
"access": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"notify"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:
|
||||||
|
services:
|
||||||
|
- '4'
|
||||||
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
|
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
|
||||||
properties:
|
properties:
|
||||||
- 9.*
|
- 9.*
|
||||||
@ -5,6 +8,9 @@ urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
|
|||||||
- 15.*
|
- 15.*
|
||||||
services:
|
services:
|
||||||
- '10'
|
- '10'
|
||||||
|
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:
|
||||||
|
properties:
|
||||||
|
- '3.2'
|
||||||
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
|
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
|
||||||
properties:
|
properties:
|
||||||
- '5.1'
|
- '5.1'
|
||||||
@ -38,6 +44,7 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
|
|||||||
services:
|
services:
|
||||||
- '1'
|
- '1'
|
||||||
- '5'
|
- '5'
|
||||||
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
|
urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
|
||||||
services:
|
services:
|
||||||
- '*'
|
- '2'
|
||||||
|
- '4'
|
||||||
|
343
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
343
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
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-c17:1:
|
||||||
|
prop.10.6:
|
||||||
|
unit: none
|
||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c17:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c17:1
|
||||||
|
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.8.6:
|
||||||
|
unit: kWh
|
||||||
|
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-h40h00:1:
|
||||||
|
prop.10.6:
|
||||||
|
unit: none
|
||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1:
|
||||||
|
prop.10.6:
|
||||||
|
unit: none
|
||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1
|
||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1
|
||||||
|
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16: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-cgd1st:1:
|
||||||
|
prop.3.7:
|
||||||
|
value-range:
|
||||||
|
- -30
|
||||||
|
- 100
|
||||||
|
- 0.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:curtain:0000A00C:bjkcz-kczble:1:0000D031:
|
||||||
|
prop.2.2:
|
||||||
|
name: status-a
|
||||||
|
urn:miot-spec-v2:device:electronic-valve:0000A0A7:sanmei-s1:1:
|
||||||
|
prop.3.1:
|
||||||
|
expr: round(src_value/100, 2)
|
||||||
|
prop.3.2:
|
||||||
|
expr: round(src_value/100, 2)
|
||||||
|
prop.3.3:
|
||||||
|
expr: round(src_value/10, 1)
|
||||||
|
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-p45:1:0000D062:
|
||||||
|
prop.2.4:
|
||||||
|
name: fan-level-a
|
||||||
|
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
|
||||||
|
prop.2.2:
|
||||||
|
name: fan-level-a
|
||||||
|
urn:miot-spec-v2:device:fan:0000A005: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:light:0000A001:shhf-sflt11:1:0000C802:
|
||||||
|
prop.11.14:
|
||||||
|
name: on-power
|
||||||
|
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:motor-controller:0000A01D:adp-adswb4:1:0000C837:
|
||||||
|
prop.2.1:
|
||||||
|
name: motor-switch
|
||||||
|
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-cp2d:1:
|
||||||
|
prop.3.2:
|
||||||
|
expr: round(src_value/1000, 2)
|
||||||
|
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:yutai-fsov8m:1:0000C816:
|
||||||
|
prop.4.1:
|
||||||
|
expr: round(src_value/10000, 2)
|
||||||
|
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,13 +48,13 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
|
|||||||
from homeassistant.components.sensor import SensorDeviceClass
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
from homeassistant.components.sensor import SensorStateClass
|
from homeassistant.components.sensor import SensorStateClass
|
||||||
from homeassistant.components.event import EventDeviceClass
|
from homeassistant.components.event import EventDeviceClass
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
UnitOfEnergy,
|
EntityCategory, LIGHT_LUX, UnitOfEnergy,
|
||||||
UnitOfPower,
|
UnitOfPower, UnitOfElectricCurrent,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricPotential, UnitOfTemperature,
|
||||||
UnitOfElectricPotential,
|
UnitOfPressure, PERCENTAGE)
|
||||||
)
|
|
||||||
|
|
||||||
# pylint: disable=pointless-string-statement
|
# pylint: disable=pointless-string-statement
|
||||||
"""SPEC_DEVICE_TRANS_MAP
|
"""SPEC_DEVICE_TRANS_MAP
|
||||||
@ -96,11 +96,11 @@ from homeassistant.const import (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
SPEC_DEVICE_TRANS_MAP: dict = {
|
||||||
'humidifier': {
|
'humidifier': {
|
||||||
'required': {
|
'required': {
|
||||||
'humidifier': {
|
'humidifier': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'on': {'read', 'write'}
|
'on': {'read', 'write'}
|
||||||
}
|
}
|
||||||
@ -112,7 +112,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'environment': {
|
'environment': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'relative-humidity': {'read'}
|
'relative-humidity': {'read'}
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'dehumidifier': {
|
'dehumidifier': {
|
||||||
'required': {
|
'required': {
|
||||||
'dehumidifier': {
|
'dehumidifier': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'on': {'read', 'write'}
|
'on': {'read', 'write'}
|
||||||
}
|
}
|
||||||
@ -132,11 +132,11 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'mode', 'target-humidity'}
|
'properties': {'mode', 'target-humidity'}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'environment': {
|
'environment': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'relative-humidity': {'read'}
|
'relative-humidity': {'read'}
|
||||||
}
|
}
|
||||||
@ -148,33 +148,30 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'vacuum': {
|
'vacuum': {
|
||||||
'required': {
|
'required': {
|
||||||
'vacuum': {
|
'vacuum': {
|
||||||
'required': {
|
'required': {
|
||||||
'actions': {'start-sweep', 'stop-sweeping'},
|
'actions': {'start-sweep', 'stop-sweeping'},
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'status', 'fan-level'},
|
'properties': {'status', 'fan-level'},
|
||||||
'actions': {
|
'actions': {
|
||||||
'pause-sweeping',
|
'pause-sweeping', 'continue-sweep', 'stop-and-gocharge'
|
||||||
'continue-sweep',
|
|
||||||
'stop-and-gocharge'
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'identify': {
|
'identify': {
|
||||||
'required': {
|
'required': {
|
||||||
'actions': {'identify'}
|
'actions': {'identify'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'battery': {
|
'battery': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'battery-level': {'read'}
|
'battery-level': {'read'}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
'entity': 'vacuum'
|
'entity': 'vacuum'
|
||||||
},
|
},
|
||||||
@ -190,7 +187,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'target-humidity'}
|
'properties': {'target-humidity'}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
@ -198,10 +195,9 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'required': {},
|
'required': {},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'on',
|
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
|
||||||
'fan-level',
|
}
|
||||||
'horizontal-swing',
|
}
|
||||||
'vertical-swing'}}
|
|
||||||
},
|
},
|
||||||
'environment': {
|
'environment': {
|
||||||
'required': {},
|
'required': {},
|
||||||
@ -219,6 +215,31 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'entity': 'air-conditioner'
|
'entity': 'air-conditioner'
|
||||||
},
|
},
|
||||||
'air-condition-outlet': 'air-conditioner',
|
'air-condition-outlet': 'air-conditioner',
|
||||||
|
'thermostat': {
|
||||||
|
'required': {
|
||||||
|
'thermostat': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'target-temperature', 'mode', 'fan-level', 'temperature'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'environment': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'temperature', 'relative-humidity'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'thermostat'
|
||||||
|
},
|
||||||
'heater': {
|
'heater': {
|
||||||
'required': {
|
'required': {
|
||||||
'heater': {
|
'heater': {
|
||||||
@ -229,7 +250,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'target-temperature', 'heat-level'}
|
'properties': {'target-temperature', 'heat-level'}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
@ -238,9 +259,140 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'temperature', 'relative-humidity'}
|
'properties': {'temperature', 'relative-humidity'}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
'entity': 'heater'
|
'entity': 'heater'
|
||||||
|
},
|
||||||
|
'bath-heater': {
|
||||||
|
'required': {
|
||||||
|
'ptc-bath-heater': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'mode': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'target-temperature', 'temperature'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'fan-control': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'environment': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'temperature'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'bath-heater',
|
||||||
|
},
|
||||||
|
'electric-blanket': {
|
||||||
|
'required': {
|
||||||
|
'electric-blanket': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'},
|
||||||
|
'target-temperature': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mode', 'temperature'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {},
|
||||||
|
'entity': 'electric-blanket'
|
||||||
|
},
|
||||||
|
'speaker': {
|
||||||
|
'required': {
|
||||||
|
'speaker': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'volume': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mute'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'play-control': {
|
||||||
|
'required': {
|
||||||
|
'actions': {'play'}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'playing-state'},
|
||||||
|
'actions': {'pause', 'stop', 'next', 'previous'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {},
|
||||||
|
'entity': 'wifi-speaker'
|
||||||
|
},
|
||||||
|
'television': {
|
||||||
|
'required': {
|
||||||
|
'speaker': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'volume': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'mute'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'television': {
|
||||||
|
'required': {
|
||||||
|
'actions': {'turn-off'}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'input-control'},
|
||||||
|
'actions': {'turn-on'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'play-control': {
|
||||||
|
'required': {},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'playing-state'},
|
||||||
|
'actions': {'play', 'pause', 'stop', 'next', 'previous'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'television'
|
||||||
|
},
|
||||||
|
'watch': {
|
||||||
|
'required': {
|
||||||
|
'watch': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'longitude': {'read'},
|
||||||
|
'latitude': {'read'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'area-id'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'battery': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'battery-level': {'read'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'device_tracker'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,11 +411,12 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'events': set<event instance name: str>,
|
'events': set<event instance name: str>,
|
||||||
'actions': set<action instance name: str>
|
'actions': set<action instance name: str>
|
||||||
},
|
},
|
||||||
'entity': str
|
'entity': str,
|
||||||
|
'entity_category'?: str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
SPEC_SERVICE_TRANS_MAP: dict = {
|
||||||
'light': {
|
'light': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -271,16 +424,28 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {
|
'properties': {'mode', 'brightness', 'color', 'color-temperature'}
|
||||||
'mode', 'brightness', 'color', 'color-temperature'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'entity': 'light'
|
'entity': 'light'
|
||||||
},
|
},
|
||||||
'indicator-light': 'light',
|
|
||||||
'ambient-light': 'light',
|
'ambient-light': 'light',
|
||||||
'night-light': 'light',
|
'night-light': 'light',
|
||||||
'white-light': 'light',
|
'white-light': 'light',
|
||||||
|
'indicator-light': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {
|
||||||
|
'mode',
|
||||||
|
'brightness',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'entity': 'light',
|
||||||
|
'entity_category': EntityCategory.CONFIG
|
||||||
|
},
|
||||||
'fan': {
|
'fan': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -295,6 +460,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
},
|
},
|
||||||
'fan-control': 'fan',
|
'fan-control': 'fan',
|
||||||
'ceiling-fan': 'fan',
|
'ceiling-fan': 'fan',
|
||||||
|
'air-fresh': 'fan',
|
||||||
|
'air-purifier': 'fan',
|
||||||
'water-heater': {
|
'water-heater': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -302,24 +469,37 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {'on', 'temperature', 'target-temperature', 'mode'}
|
'properties': {'temperature', 'target-temperature', 'mode'}
|
||||||
},
|
},
|
||||||
'entity': 'water_heater'
|
'entity': 'water_heater'
|
||||||
},
|
},
|
||||||
'curtain': {
|
'curtain': {
|
||||||
'required': {
|
'required': {
|
||||||
'properties': {
|
'properties': {
|
||||||
'motor-control': {'write'}
|
'motor-control': {'write'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'optional': {
|
'optional': {
|
||||||
'properties': {
|
'properties': {'status', 'current-position', 'target-position'}
|
||||||
'motor-control', 'status', 'current-position', 'target-position'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'entity': 'cover'
|
'entity': 'cover'
|
||||||
},
|
},
|
||||||
'window-opener': 'curtain'
|
'window-opener': 'curtain',
|
||||||
|
'motor-controller': 'curtain',
|
||||||
|
'airer': 'curtain',
|
||||||
|
'air-conditioner': {
|
||||||
|
'required': {
|
||||||
|
'properties': {
|
||||||
|
'on': {'read', 'write'},
|
||||||
|
'mode': {'read', 'write'},
|
||||||
|
'target-temperature': {'read', 'write'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
'properties': {'target-humidity'}
|
||||||
|
},
|
||||||
|
'entity': 'air-conditioner'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"""SPEC_PROP_TRANS_MAP
|
"""SPEC_PROP_TRANS_MAP
|
||||||
@ -334,121 +514,145 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
|||||||
'<property instance name>':{
|
'<property instance name>':{
|
||||||
'device_class': str,
|
'device_class': str,
|
||||||
'entity': str,
|
'entity': str,
|
||||||
'optional':{
|
'state_class'?: str,
|
||||||
'state_class': str,
|
'unit_of_measurement'?: str
|
||||||
'unit_of_measurement': str
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
SPEC_PROP_TRANS_MAP: dict = {
|
||||||
'entities': {
|
'entities': {
|
||||||
'sensor': {
|
'sensor': {
|
||||||
'format': {'int', 'float'},
|
'format': {'int', 'float'},
|
||||||
'access': {'read'}
|
'access': {'read'}
|
||||||
},
|
},
|
||||||
|
'binary_sensor': {
|
||||||
|
'format': {'bool', 'int'},
|
||||||
|
'access': {'read'}
|
||||||
|
},
|
||||||
'switch': {
|
'switch': {
|
||||||
'format': {'bool'},
|
'format': {'bool'},
|
||||||
'access': {'read', 'write'}
|
'access': {'read', 'write'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'properties': {
|
'properties': {
|
||||||
|
'submersion-state': {
|
||||||
|
'device_class': BinarySensorDeviceClass.MOISTURE,
|
||||||
|
'entity': 'binary_sensor'
|
||||||
|
},
|
||||||
|
'contact-state': {
|
||||||
|
'device_class': BinarySensorDeviceClass.DOOR,
|
||||||
|
'entity': 'binary_sensor'
|
||||||
|
},
|
||||||
|
'occupancy-status': {
|
||||||
|
'device_class': BinarySensorDeviceClass.OCCUPANCY,
|
||||||
|
'entity': 'binary_sensor',
|
||||||
|
},
|
||||||
'temperature': {
|
'temperature': {
|
||||||
'device_class': SensorDeviceClass.TEMPERATURE,
|
'device_class': SensorDeviceClass.TEMPERATURE,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': UnitOfTemperature.CELSIUS
|
||||||
},
|
},
|
||||||
'relative-humidity': {
|
'relative-humidity': {
|
||||||
'device_class': SensorDeviceClass.HUMIDITY,
|
'device_class': SensorDeviceClass.HUMIDITY,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': PERCENTAGE
|
||||||
},
|
},
|
||||||
'air-quality-index': {
|
'air-quality-index': {
|
||||||
'device_class': SensorDeviceClass.AQI,
|
'device_class': SensorDeviceClass.AQI,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
},
|
},
|
||||||
'pm2.5-density': {
|
'pm2.5-density': {
|
||||||
'device_class': SensorDeviceClass.PM25,
|
'device_class': SensorDeviceClass.PM25,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||||
},
|
},
|
||||||
'pm10-density': {
|
'pm10-density': {
|
||||||
'device_class': SensorDeviceClass.PM10,
|
'device_class': SensorDeviceClass.PM10,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||||
},
|
},
|
||||||
'pm1': {
|
'pm1': {
|
||||||
'device_class': SensorDeviceClass.PM1,
|
'device_class': SensorDeviceClass.PM1,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||||
},
|
},
|
||||||
'atmospheric-pressure': {
|
'atmospheric-pressure': {
|
||||||
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': UnitOfPressure.PA
|
||||||
},
|
},
|
||||||
'tvoc-density': {
|
'tvoc-density': {
|
||||||
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
'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': {
|
'battery-level': {
|
||||||
'device_class': SensorDeviceClass.BATTERY,
|
'device_class': SensorDeviceClass.BATTERY,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': PERCENTAGE
|
||||||
},
|
},
|
||||||
'voltage': {
|
'voltage': {
|
||||||
'device_class': SensorDeviceClass.VOLTAGE,
|
'device_class': SensorDeviceClass.VOLTAGE,
|
||||||
'entity': 'sensor',
|
'entity': 'sensor',
|
||||||
'optional': {
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
'state_class': SensorStateClass.MEASUREMENT,
|
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
},
|
||||||
}
|
'electric-current': {
|
||||||
|
'device_class': SensorDeviceClass.CURRENT,
|
||||||
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||||
},
|
},
|
||||||
'illumination': {
|
'illumination': {
|
||||||
'device_class': SensorDeviceClass.ILLUMINANCE,
|
'device_class': SensorDeviceClass.ILLUMINANCE,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor',
|
||||||
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
|
'unit_of_measurement': LIGHT_LUX
|
||||||
},
|
},
|
||||||
'no-one-determine-time': {
|
'no-one-determine-time': {
|
||||||
'device_class': SensorDeviceClass.DURATION,
|
'device_class': SensorDeviceClass.DURATION,
|
||||||
'entity': 'sensor'
|
'entity': 'sensor'
|
||||||
},
|
},
|
||||||
|
'has-someone-duration': 'no-one-determine-time',
|
||||||
|
'no-one-duration': 'no-one-determine-time',
|
||||||
'electric-power': {
|
'electric-power': {
|
||||||
'device_class': SensorDeviceClass.POWER,
|
'device_class': SensorDeviceClass.POWER,
|
||||||
'entity': 'sensor',
|
'entity': 'sensor',
|
||||||
'optional': {
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
'state_class': SensorStateClass.MEASUREMENT,
|
'unit_of_measurement': UnitOfPower.WATT
|
||||||
'unit_of_measurement': UnitOfPower.WATT
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'electric-current': {
|
'surge-power': {
|
||||||
'device_class': SensorDeviceClass.CURRENT,
|
'device_class': SensorDeviceClass.POWER,
|
||||||
'entity': 'sensor',
|
'entity': 'sensor',
|
||||||
'optional': {
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
'state_class': SensorStateClass.MEASUREMENT,
|
'unit_of_measurement': UnitOfPower.WATT
|
||||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'power-consumption': {
|
'power-consumption': {
|
||||||
'device_class': SensorDeviceClass.ENERGY,
|
'device_class': SensorDeviceClass.ENERGY,
|
||||||
'entity': 'sensor',
|
'entity': 'sensor',
|
||||||
'optional': {
|
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'power': {
|
'power': {
|
||||||
'device_class': SensorDeviceClass.POWER,
|
'device_class': SensorDeviceClass.POWER,
|
||||||
'entity': 'sensor',
|
'entity': 'sensor',
|
||||||
'optional': {
|
'state_class': SensorStateClass.MEASUREMENT,
|
||||||
'state_class': SensorStateClass.MEASUREMENT,
|
'unit_of_measurement': UnitOfPower.WATT
|
||||||
'unit_of_measurement': UnitOfPower.WATT
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
'total-battery': {
|
|
||||||
'device_class': SensorDeviceClass.ENERGY,
|
|
||||||
'entity': 'sensor',
|
|
||||||
'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'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,6 +670,4 @@ SPEC_EVENT_TRANS_MAP: dict[str, str] = {
|
|||||||
'doorbell-ring': EventDeviceClass.DOORBELL
|
'doorbell-ring': EventDeviceClass.DOORBELL
|
||||||
}
|
}
|
||||||
|
|
||||||
SPEC_ACTION_TRANS_MAP = {
|
SPEC_ACTION_TRANS_MAP = {}
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -88,7 +88,7 @@ class Number(MIoTPropertyEntity, NumberEntity):
|
|||||||
if self.spec.external_unit:
|
if self.spec.external_unit:
|
||||||
self._attr_native_unit_of_measurement = self.spec.external_unit
|
self._attr_native_unit_of_measurement = self.spec.external_unit
|
||||||
# Set icon
|
# Set icon
|
||||||
if self.spec.icon:
|
if self.spec.icon and not self.device_class:
|
||||||
self._attr_icon = self.spec.icon
|
self._attr_icon = self.spec.icon
|
||||||
# Set value range
|
# Set value range
|
||||||
if self._value_range:
|
if self._value_range:
|
||||||
|
@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
|||||||
# Set device_class
|
# Set device_class
|
||||||
if self._value_list:
|
if self._value_list:
|
||||||
self._attr_device_class = SensorDeviceClass.ENUM
|
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_native_unit_of_measurement = None
|
||||||
self._attr_options = self._value_list.descriptions
|
self._attr_options = self._value_list.descriptions
|
||||||
else:
|
else:
|
||||||
@ -109,11 +109,14 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
|||||||
self._attr_device_class, None) # type: ignore
|
self._attr_device_class, None) # type: ignore
|
||||||
self._attr_native_unit_of_measurement = list(
|
self._attr_native_unit_of_measurement = list(
|
||||||
unit_sets)[0] if unit_sets else None
|
unit_sets)[0] if unit_sets else None
|
||||||
|
# Set suggested precision
|
||||||
|
if spec.format_ in {int, float} and spec.expr is None:
|
||||||
|
self._attr_suggested_display_precision = spec.precision
|
||||||
# Set state_class
|
# Set state_class
|
||||||
if spec.state_class:
|
if spec.state_class:
|
||||||
self._attr_state_class = spec.state_class
|
self._attr_state_class = spec.state_class
|
||||||
# Set icon
|
# Set icon
|
||||||
if spec.icon:
|
if spec.icon and not self.device_class:
|
||||||
self._attr_icon = spec.icon
|
self._attr_icon = spec.icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"eula": {
|
"eula": {
|
||||||
"title": "Risikohinweis",
|
"title": "Risikohinweis",
|
||||||
"description": "1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\r\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\r\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet. \r\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\r\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.",
|
"description": "1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\r\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\r\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet.\r\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\r\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.",
|
||||||
"data": {
|
"data": {
|
||||||
"eula": "Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration."
|
"eula": "Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration."
|
||||||
}
|
}
|
||||||
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen",
|
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen",
|
||||||
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren",
|
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren",
|
||||||
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren",
|
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren",
|
||||||
"network_detect_config": "Integrierte Netzwerkkonfiguration"
|
"network_detect_config": "Integrierte Netzwerkkonfiguration",
|
||||||
|
"cover_dead_zone_width": "Die Breite des toten Winkels des Vorhangs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Bestätigen Sie die Konfiguration",
|
"title": "Bestätigen Sie die Konfiguration",
|
||||||
"description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln",
|
"description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nDie Breite des toten Winkels des Vorhangs:\t{cover_width_new}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Änderungen bestätigen"
|
"confirm": "Änderungen bestätigen"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Display device status change notifications",
|
"display_devices_changed_notify": "Display device status change notifications",
|
||||||
"update_trans_rules": "Update entity conversion rules",
|
"update_trans_rules": "Update entity conversion rules",
|
||||||
"update_lan_ctrl_config": "Update LAN control configuration",
|
"update_lan_ctrl_config": "Update LAN control configuration",
|
||||||
"network_detect_config": "Integrated Network Configuration"
|
"network_detect_config": "Integrated network configuration",
|
||||||
|
"cover_dead_zone_width": "Cover dead zone width"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Confirm Configuration",
|
"title": "Confirm Configuration",
|
||||||
"description": "Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\r\nThe integration will reload using the updated configuration.\r\n\r\nIntegration Language: \t{lang_new}\r\nNickname: \t{nick_name_new}\r\nDebug mode for action: \t{action_debug}\r\nHide non-standard created entities: \t{hide_non_standard_entities}\r\nDisplay device status change notifications:\t{display_devices_changed_notify}\r\nDevice Changes: \tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\r\nTransformation rules change: \tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules",
|
"description": "Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\r\nThe integration will reload using the updated configuration.\r\n\r\nIntegration Language:\t{lang_new}\r\nNickname:\t{nick_name_new}\r\nDebug mode for action:\t{action_debug}\r\nHide non-standard created entities:\t{hide_non_standard_entities}\r\nCover dead zone width:\t{cover_width_new}\r\nDisplay device status change notifications:\t{display_devices_changed_notify}\r\nDevice Changes:\tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\r\nTransformation rules change:\tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Confirm the change"
|
"confirm": "Confirm the change"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"eula": {
|
"eula": {
|
||||||
"title": "Aviso de riesgo",
|
"title": "Aviso de riesgo",
|
||||||
"description": "1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\r\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\r\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\r\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**. \r\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.",
|
"description": "1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\r\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\r\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\r\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**.\r\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.",
|
||||||
"data": {
|
"data": {
|
||||||
"eula": "He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración."
|
"eula": "He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración."
|
||||||
}
|
}
|
||||||
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo",
|
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo",
|
||||||
"update_trans_rules": "Actualizar reglas de conversión de entidad",
|
"update_trans_rules": "Actualizar reglas de conversión de entidad",
|
||||||
"update_lan_ctrl_config": "Actualizar configuración de control LAN",
|
"update_lan_ctrl_config": "Actualizar configuración de control LAN",
|
||||||
"network_detect_config": "Configuración de Red Integrada"
|
"network_detect_config": "Configuración de Red Integrada",
|
||||||
|
"cover_dead_zone_width": "Anchura del punto ciego de la cortina"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Confirmar configuración",
|
"title": "Confirmar configuración",
|
||||||
"description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas",
|
"description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nAnchura del punto ciego de la cortina:\t{cover_width_new}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Confirmar modificación"
|
"confirm": "Confirmar modificación"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil",
|
"display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil",
|
||||||
"update_trans_rules": "Mettre à jour les règles de conversion d'entités",
|
"update_trans_rules": "Mettre à jour les règles de conversion d'entités",
|
||||||
"update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN",
|
"update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN",
|
||||||
"network_detect_config": "Configuration Réseau Intégrée"
|
"network_detect_config": "Configuration Réseau Intégrée",
|
||||||
|
"cover_dead_zone_width": "Largeur de la zone aveugle du rideau"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Confirmer la configuration",
|
"title": "Confirmer la configuration",
|
||||||
"description": "**{nick_name}** Bonjour ! Veuillez confirmer les dernières informations de configuration et cliquer sur \"Soumettre\".\r\nL'intégration rechargera avec la nouvelle configuration.\r\n\r\nLangue d'intégration : {lang_new}\r\nPseudo utilisateur : {nick_name_new}\r\nMode de débogage d'action : {action_debug}\r\nMasquer les entités générées non standard : {hide_non_standard_entities}\r\nAfficher les notifications de changement d'état de l'appareil:\t{display_devices_changed_notify}\r\nModifications des appareils : Ajouter **{devices_add}** appareils, supprimer **{devices_remove}** appareils\r\nModifications des règles de conversion : **{trans_rules_count}** règles au total, mise à jour de **{trans_rules_count_success}** règles",
|
"description": "**{nick_name}** Bonjour ! Veuillez confirmer les dernières informations de configuration et cliquer sur \"Soumettre\".\r\nL'intégration rechargera avec la nouvelle configuration.\r\n\r\nLangue d'intégration : {lang_new}\r\nPseudo utilisateur : {nick_name_new}\r\nMode de débogage d'action : {action_debug}\r\nMasquer les entités générées non standard : {hide_non_standard_entities}\r\nLargeur de la zone aveugle du rideau:\t{cover_width_new}\r\nAfficher les notifications de changement d'état de l'appareil:\t{display_devices_changed_notify}\r\nModifications des appareils : Ajouter **{devices_add}** appareils, supprimer **{devices_remove}** appareils\r\nModifications des règles de conversion : **{trans_rules_count}** règles au total, mise à jour de **{trans_rules_count_success}** règles",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Confirmer la modification"
|
"confirm": "Confirmer la modification"
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
},
|
},
|
||||||
"config_options": {
|
"config_options": {
|
||||||
"title": "Opzioni di Configurazione",
|
"title": "Opzioni di Configurazione",
|
||||||
"description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.",
|
"description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\nID istanza di integrazione: {instance_id}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.",
|
||||||
"data": {
|
"data": {
|
||||||
"integration_language": "Lingua dell'Integrazione",
|
"integration_language": "Lingua dell'Integrazione",
|
||||||
"update_user_info": "Aggiorna le informazioni dell'utente",
|
"update_user_info": "Aggiorna le informazioni dell'utente",
|
||||||
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo",
|
"display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo",
|
||||||
"update_trans_rules": "Aggiorna le regole di conversione delle entità",
|
"update_trans_rules": "Aggiorna le regole di conversione delle entità",
|
||||||
"update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN",
|
"update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN",
|
||||||
"network_detect_config": "Configurazione di Rete Integrata"
|
"network_detect_config": "Configurazione di Rete Integrata",
|
||||||
|
"cover_dead_zone_width": "Larghezza dell’angolo cieco della tenda"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Conferma Configurazione",
|
"title": "Conferma Configurazione",
|
||||||
"description": "Ciao **{nick_name}**, si prega di confermare le informazioni di configurazione più recenti e poi fare clic su INVIA.\r\nL'integrazione verrà ricaricata utilizzando la configurazione aggiornata.\r\n\r\nLingua dell'Integrazione: \t{lang_new}\r\nSoprannome: \t{nick_name_new}\r\nModalità di debug per azione: \t{action_debug}\r\nNascondi entità create non standard: \t{hide_non_standard_entities}\r\nMostra notifiche di cambio stato del dispositivo:\t{display_devices_changed_notify}\r\nCambiamenti del Dispositivo: \tAggiungi **{devices_add}** dispositivi, Rimuovi **{devices_remove}** dispositivi\r\nCambiamenti delle regole di trasformazione: \tCi sono un totale di **{trans_rules_count}** regole, e aggiornate **{trans_rules_count_success}** regole",
|
"description": "Ciao **{nick_name}**, si prega di confermare le informazioni di configurazione più recenti e poi fare clic su INVIA.\r\nL'integrazione verrà ricaricata utilizzando la configurazione aggiornata.\r\n\r\nLingua dell'Integrazione:\t{lang_new}\r\nSoprannome:\t{nick_name_new}\r\nModalità di debug per azione:\t{action_debug}\r\nNascondi entità create non standard:\t{hide_non_standard_entities}\r\nLarghezza dell’angolo cieco della tenda:\t{cover_width_new}\r\nMostra notifiche di cambio stato del dispositivo:\t{display_devices_changed_notify}\r\nCambiamenti del Dispositivo:\tAggiungi **{devices_add}** dispositivi, Rimuovi **{devices_remove}** dispositivi\r\nCambiamenti delle regole di trasformazione:\tCi sono un totale di **{trans_rules_count}** regole, e aggiornate **{trans_rules_count_success}** regole",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Conferma la modifica"
|
"confirm": "Conferma la modifica"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "デバイスの状態変化通知を表示",
|
"display_devices_changed_notify": "デバイスの状態変化通知を表示",
|
||||||
"update_trans_rules": "エンティティ変換ルールを更新する",
|
"update_trans_rules": "エンティティ変換ルールを更新する",
|
||||||
"update_lan_ctrl_config": "LAN制御構成を更新する",
|
"update_lan_ctrl_config": "LAN制御構成を更新する",
|
||||||
"network_detect_config": "統合ネットワーク構成"
|
"network_detect_config": "統合ネットワーク構成",
|
||||||
|
"cover_dead_zone_width": "カーテンの死角幅"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "構成を確認する",
|
"title": "構成を確認する",
|
||||||
"description": "**{nick_name}** さん、こんにちは! 最新の構成情報を確認してください。[送信] をクリックして、更新された構成を使用して再度読み込みます。\r\n\r\n統合言語:\t{lang_new}\r\nユーザー名:\t{nick_name_new}\r\nAction デバッグモード:\t{action_debug}\r\n非標準生成エンティティを非表示にする:\t{hide_non_standard_entities}\r\nデバイスの状態変化通知を表示:\t{display_devices_changed_notify}\r\nデバイス変更:\t追加 **{devices_add}** 個のデバイス、削除 **{devices_remove}** 個のデバイス\r\n変換ルール変更:\t合計 **{trans_rules_count}** 個の規則、更新 **{trans_rules_count_success}** 個の規則",
|
"description": "**{nick_name}** さん、こんにちは! 最新の構成情報を確認してください。[送信] をクリックして、更新された構成を使用して再度読み込みます。\r\n\r\n統合言語:\t{lang_new}\r\nユーザー名:\t{nick_name_new}\r\nAction デバッグモード:\t{action_debug}\r\n非標準生成エンティティを非表示にする:\t{hide_non_standard_entities}\r\nカーテンの死角幅:\t{cover_width_new}\r\nデバイスの状態変化通知を表示:\t{display_devices_changed_notify}\r\nデバイス変更:\t追加 **{devices_add}** 個のデバイス、削除 **{devices_remove}** 個のデバイス\r\n変換ルール変更:\t合計 **{trans_rules_count}** 個の規則、更新 **{trans_rules_count_success}** 個の規則",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "変更を確認する"
|
"confirm": "変更を確認する"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven",
|
"display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven",
|
||||||
"update_trans_rules": "Werk entiteitsconversieregels bij",
|
"update_trans_rules": "Werk entiteitsconversieregels bij",
|
||||||
"update_lan_ctrl_config": "Werk LAN controleconfiguratie bij",
|
"update_lan_ctrl_config": "Werk LAN controleconfiguratie bij",
|
||||||
"network_detect_config": "Geïntegreerde Netwerkconfiguratie"
|
"network_detect_config": "Geïntegreerde Netwerkconfiguratie",
|
||||||
|
"cover_dead_zone_width": "Breedte van de dode hoek van het gordijn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Bevestig Configuratie",
|
"title": "Bevestig Configuratie",
|
||||||
"description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal: \t{lang_new}\r\nBijnaam: \t{nick_name_new}\r\nDebugmodus voor actie: \t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten: \t{hide_non_standard_entities}\r\nApparaatstatuswijzigingen weergeven:\t{display_devices_changed_notify}\r\nWijzigingen in apparaten: \tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels: \tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt",
|
"description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal:\t{lang_new}\r\nBijnaam:\t{nick_name_new}\r\nDebugmodus voor actie:\t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten:\t{hide_non_standard_entities}\r\nBreedte van de dode hoek van het gordijn:\t{cover_width_new}\r\nApparaatstatuswijzigingen weergeven:\t{display_devices_changed_notify}\r\nWijzigingen in apparaten:\tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels:\tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Bevestig de wijziging"
|
"confirm": "Bevestig de wijziging"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
|
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
|
||||||
"update_trans_rules": "Atualizar regras de conversão de entidades",
|
"update_trans_rules": "Atualizar regras de conversão de entidades",
|
||||||
"update_lan_ctrl_config": "Atualizar configuração de controle LAN",
|
"update_lan_ctrl_config": "Atualizar configuração de controle LAN",
|
||||||
"network_detect_config": "Configuração de Rede Integrada"
|
"network_detect_config": "Configuração de Rede Integrada",
|
||||||
|
"cover_dead_zone_width": "Largura da área cega da cortina"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Confirmar Configuração",
|
"title": "Confirmar Configuração",
|
||||||
"description": "Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nApelido:\t{nick_name_new}\r\nModo de depuração para ação:\t{action_debug}\r\nOcultar entidades não padrão criadas:\t{hide_non_standard_entities}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações de Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração nas Regras de Transformação:\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas",
|
"description": "Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nApelido:\t{nick_name_new}\r\nModo de depuração para ação:\t{action_debug}\r\nOcultar entidades não padrão criadas:\t{hide_non_standard_entities}\r\nLargura da área cega da cortina:\t{cover_width_new}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações de Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração nas Regras de Transformação:\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Confirmar a mudança"
|
"confirm": "Confirmar a mudança"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
|
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
|
||||||
"update_trans_rules": "Atualizar regras de conversão de entidades",
|
"update_trans_rules": "Atualizar regras de conversão de entidades",
|
||||||
"update_lan_ctrl_config": "Atualizar configuração de controlo LAN",
|
"update_lan_ctrl_config": "Atualizar configuração de controlo LAN",
|
||||||
"network_detect_config": "Configuração de Rede Integrada"
|
"network_detect_config": "Configuração de Rede Integrada",
|
||||||
|
"cover_dead_zone_width": "Largura da zona cega da cortina"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Confirmar Configuração",
|
"title": "Confirmar Configuração",
|
||||||
"description": "Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nAlcunha:\t{nick_name_new}\r\nModo de depuração de ação:\t{action_debug}\r\nOcultar entidades não padrão:\t{hide_non_standard_entities}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações aos Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração das Regras de Transformação:\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas",
|
"description": "Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nAlcunha:\t{nick_name_new}\r\nModo de depuração de ação:\t{action_debug}\r\nOcultar entidades não padrão:\t{hide_non_standard_entities}\r\nLargura da zona cega da cortina:\t{cover_width_new}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações aos Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração das Regras de Transformação:\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Confirmar a alteração"
|
"confirm": "Confirmar a alteração"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства",
|
"display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства",
|
||||||
"update_trans_rules": "Обновить правила преобразования сущностей",
|
"update_trans_rules": "Обновить правила преобразования сущностей",
|
||||||
"update_lan_ctrl_config": "Обновить конфигурацию управления LAN",
|
"update_lan_ctrl_config": "Обновить конфигурацию управления LAN",
|
||||||
"network_detect_config": "Интегрированная Сетевая Конфигурация"
|
"network_detect_config": "Интегрированная Сетевая Конфигурация",
|
||||||
|
"cover_dead_zone_width": "Ширина «мертвой зоны» шторы"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "Подтверждение настройки",
|
"title": "Подтверждение настройки",
|
||||||
"description": "**{nick_name}** Здравствуйте! Подтвердите последнюю информацию о настройке и нажмите «Отправить». Интеграция будет перезагружена с использованием обновленных настроек.\r\n\r\nЯзык интеграции:\t{lang_new}\r\nИмя пользователя:\t{nick_name_new}\r\nРежим отладки Action:\t{action_debug}\r\nСкрыть непроизводственные сущности:\t{hide_non_standard_entities}\r\nОтображать уведомления о изменении состояния устройства:\t{display_devices_changed_notify}\r\nИзменение устройства:\tДобавлено **{devices_add}** устройство, удалено **{devices_remove}** устройства\r\nИзменение правил преобразования:\tВсего **{trans_rules_count}** правил, обновлено **{trans_rules_count_success}** правил",
|
"description": "**{nick_name}** Здравствуйте! Подтвердите последнюю информацию о настройке и нажмите «Отправить». Интеграция будет перезагружена с использованием обновленных настроек.\r\n\r\nЯзык интеграции:\t{lang_new}\r\nИмя пользователя:\t{nick_name_new}\r\nРежим отладки Action:\t{action_debug}\r\nСкрыть непроизводственные сущности:\t{hide_non_standard_entities}\r\nШирина «мертвой зоны» шторы:\t{cover_width_new}\r\nОтображать уведомления о изменении состояния устройства:\t{display_devices_changed_notify}\r\nИзменение устройства:\tДобавлено **{devices_add}** устройство, удалено **{devices_remove}** устройства\r\nИзменение правил преобразования:\tВсего **{trans_rules_count}** правил, обновлено **{trans_rules_count_success}** правил",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "Подтвердить изменения"
|
"confirm": "Подтвердить изменения"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "显示设备状态变化通知",
|
"display_devices_changed_notify": "显示设备状态变化通知",
|
||||||
"update_trans_rules": "更新实体转换规则",
|
"update_trans_rules": "更新实体转换规则",
|
||||||
"update_lan_ctrl_config": "更新局域网控制配置",
|
"update_lan_ctrl_config": "更新局域网控制配置",
|
||||||
"network_detect_config": "集成网络配置"
|
"network_detect_config": "集成网络配置",
|
||||||
|
"cover_dead_zone_width": "窗帘盲区宽度"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "确认配置",
|
"title": "确认配置",
|
||||||
"description": "**{nick_name}** 您好!请确认最新的配置信息,然后点击“提交”。\r\n集成将会使用更新后的配置重新载入。\r\n\r\n集成语言:\t{lang_new}\r\n用户昵称:\t{nick_name_new}\r\nAction 调试模式:\t{action_debug}\r\n隐藏非标准生成实体:\t{hide_non_standard_entities}\r\n显示设备状态变化通知:\t{display_devices_changed_notify}\r\n设备变化:\t新增 **{devices_add}** 个设备,移除 **{devices_remove}** 个设备\r\n转换规则变化:\t共条 **{trans_rules_count}** 规则,更新 **{trans_rules_count_success}** 条规则",
|
"description": "**{nick_name}** 您好!请确认最新的配置信息,然后点击“提交”。\r\n集成将会使用更新后的配置重新载入。\r\n\r\n集成语言:\t{lang_new}\r\n用户昵称:\t{nick_name_new}\r\nAction 调试模式:\t{action_debug}\r\n隐藏非标准生成实体:\t{hide_non_standard_entities}\r\n窗帘盲区宽度:\t{cover_width_new}\r\n显示设备状态变化通知:\t{display_devices_changed_notify}\r\n设备变化:\t新增 **{devices_add}** 个设备,移除 **{devices_remove}** 个设备\r\n转换规则变化:\t共条 **{trans_rules_count}** 规则,更新 **{trans_rules_count_success}** 条规则",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "确认修改"
|
"confirm": "确认修改"
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,8 @@
|
|||||||
"display_devices_changed_notify": "顯示設備狀態變化通知",
|
"display_devices_changed_notify": "顯示設備狀態變化通知",
|
||||||
"update_trans_rules": "更新實體轉換規則",
|
"update_trans_rules": "更新實體轉換規則",
|
||||||
"update_lan_ctrl_config": "更新局域網控制配置",
|
"update_lan_ctrl_config": "更新局域網控制配置",
|
||||||
"network_detect_config": "集成網絡配置"
|
"network_detect_config": "集成網絡配置",
|
||||||
|
"cover_dead_zone_width": "窗簾盲區寬度"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update_user_info": {
|
"update_user_info": {
|
||||||
@ -183,7 +184,7 @@
|
|||||||
},
|
},
|
||||||
"config_confirm": {
|
"config_confirm": {
|
||||||
"title": "確認配置",
|
"title": "確認配置",
|
||||||
"description": "**{nick_name}** 您好!請確認最新的配置信息,然後點擊“提交”。\r\n集成將會使用更新後的配置重新載入。\r\n\r\n集成語言:\t{lang_new}\r\n用戶暱稱:\t{nick_name_new}\r\nAction 調試模式:\t{action_debug}\r\n隱藏非標準生成實體:\t{hide_non_standard_entities}\r\n顯示設備狀態變化通知:\t{display_devices_changed_notify}\r\n設備變化:\t新增 **{devices_add}** 個設備,移除 **{devices_remove}** 個設備\r\n轉換規則變化:\t共條 **{trans_rules_count}** 規則,更新 **{trans_rules_count_success}** 條規則",
|
"description": "**{nick_name}** 您好!請確認最新的配置信息,然後點擊“提交”。\r\n集成將會使用更新後的配置重新載入。\r\n\r\n集成語言:\t{lang_new}\r\n用戶暱稱:\t{nick_name_new}\r\nAction 調試模式:\t{action_debug}\r\n隱藏非標準生成實體:\t{hide_non_standard_entities}\r\n窗簾盲區寬度:\t{cover_width_new}\r\n顯示設備狀態變化通知:\t{display_devices_changed_notify}\r\n設備變化:\t新增 **{devices_add}** 個設備,移除 **{devices_remove}** 個設備\r\n轉換規則變化:\t共條 **{trans_rules_count}** 規則,更新 **{trans_rules_count_success}** 條規則",
|
||||||
"data": {
|
"data": {
|
||||||
"confirm": "確認修改"
|
"confirm": "確認修改"
|
||||||
}
|
}
|
||||||
|
@ -47,29 +47,32 @@ Vacuum entities for Xiaomi Home.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (StateVacuumEntity,
|
||||||
StateVacuumEntity,
|
VacuumEntityFeature)
|
||||||
VacuumEntityFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
from .miot.const import DOMAIN
|
from .miot.const import DOMAIN
|
||||||
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
|
||||||
from .miot.miot_spec import (
|
from .miot.miot_spec import (MIoTSpecAction, MIoTSpecProperty)
|
||||||
MIoTSpecAction,
|
|
||||||
MIoTSpecProperty)
|
try: # VacuumActivity is introduced in HA core 2025.1.0
|
||||||
|
from homeassistant.components.vacuum import VacuumActivity
|
||||||
|
HA_CORE_HAS_ACTIVITY = True
|
||||||
|
except ImportError:
|
||||||
|
HA_CORE_HAS_ACTIVITY = False
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
config_entry.entry_id]
|
config_entry.entry_id]
|
||||||
@ -88,6 +91,11 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
|||||||
_prop_status: Optional[MIoTSpecProperty]
|
_prop_status: Optional[MIoTSpecProperty]
|
||||||
_prop_fan_level: Optional[MIoTSpecProperty]
|
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||||
_prop_battery_level: Optional[MIoTSpecProperty]
|
_prop_battery_level: Optional[MIoTSpecProperty]
|
||||||
|
_prop_status_cleaning: Optional[list[int]]
|
||||||
|
_prop_status_docked: Optional[list[int]]
|
||||||
|
_prop_status_paused: Optional[list[int]]
|
||||||
|
_prop_status_returning: Optional[list[int]]
|
||||||
|
_prop_status_error: Optional[list[int]]
|
||||||
|
|
||||||
_action_start_sweep: Optional[MIoTSpecAction]
|
_action_start_sweep: Optional[MIoTSpecAction]
|
||||||
_action_stop_sweeping: Optional[MIoTSpecAction]
|
_action_stop_sweeping: Optional[MIoTSpecAction]
|
||||||
@ -99,15 +107,22 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
|||||||
_status_map: Optional[dict[int, str]]
|
_status_map: Optional[dict[int, str]]
|
||||||
_fan_level_map: Optional[dict[int, str]]
|
_fan_level_map: Optional[dict[int, str]]
|
||||||
|
|
||||||
def __init__(
|
_device_name: str
|
||||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
|
||||||
) -> None:
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
|
entity_data: MIoTEntityData) -> None:
|
||||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
|
self._device_name = miot_device.name
|
||||||
self._attr_supported_features = VacuumEntityFeature(0)
|
self._attr_supported_features = VacuumEntityFeature(0)
|
||||||
|
|
||||||
self._prop_status = None
|
self._prop_status = None
|
||||||
self._prop_fan_level = None
|
self._prop_fan_level = None
|
||||||
self._prop_battery_level = None
|
self._prop_battery_level = None
|
||||||
|
self._prop_status_cleaning = []
|
||||||
|
self._prop_status_docked = []
|
||||||
|
self._prop_status_paused = []
|
||||||
|
self._prop_status_returning = []
|
||||||
|
self._prop_status_error = []
|
||||||
self._action_start_sweep = None
|
self._action_start_sweep = None
|
||||||
self._action_stop_sweeping = None
|
self._action_stop_sweeping = None
|
||||||
self._action_pause_sweeping = None
|
self._action_pause_sweeping = None
|
||||||
@ -121,21 +136,50 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
|||||||
for prop in entity_data.props:
|
for prop in entity_data.props:
|
||||||
if prop.name == 'status':
|
if prop.name == 'status':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('invalid status value_list, %s',
|
||||||
'invalid status value_list, %s', self.entity_id)
|
self.entity_id)
|
||||||
continue
|
continue
|
||||||
self._status_map = prop.value_list.to_map()
|
self._status_map = prop.value_list.to_map()
|
||||||
|
self._attr_supported_features |= VacuumEntityFeature.STATE
|
||||||
self._prop_status = prop
|
self._prop_status = prop
|
||||||
|
for item in prop.value_list.items:
|
||||||
|
item_str: str = item.name
|
||||||
|
item_name: str = re.sub(r'[^a-z]', '', item_str)
|
||||||
|
if item_name in {
|
||||||
|
'charging', 'charged', 'chargingcompleted',
|
||||||
|
'fullcharge', 'fullpower', 'findchargerpause',
|
||||||
|
'drying', 'washing', 'wash', 'inthewash',
|
||||||
|
'inthedry', 'stationworking', 'dustcollecting',
|
||||||
|
'upgrade', 'upgrading', 'updating'
|
||||||
|
}:
|
||||||
|
self._prop_status_docked.append(item.value)
|
||||||
|
elif item_name in {'paused', 'pause'}:
|
||||||
|
self._prop_status_paused.append(item.value)
|
||||||
|
elif item_name in {
|
||||||
|
'gocharging', 'cleancompletegocharging',
|
||||||
|
'findchargewash', 'backtowashmop', 'gowash',
|
||||||
|
'gowashing', 'summon'
|
||||||
|
}:
|
||||||
|
self._prop_status_returning.append(item.value)
|
||||||
|
elif item_name in {
|
||||||
|
'error', 'breakcharging', 'gochargebreak'
|
||||||
|
}:
|
||||||
|
self._prop_status_error.append(item.value)
|
||||||
|
elif (item_name.find('sweeping') != -1) or (
|
||||||
|
item_name.find('mopping') != -1) or (item_name in {
|
||||||
|
'cleaning', 'remoteclean', 'continuesweep',
|
||||||
|
'busy', 'building', 'buildingmap', 'mapping'
|
||||||
|
}):
|
||||||
|
self._prop_status_cleaning.append(item.value)
|
||||||
elif prop.name == 'fan-level':
|
elif prop.name == 'fan-level':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('invalid fan-level value_list, %s',
|
||||||
'invalid fan-level value_list, %s', self.entity_id)
|
self.entity_id)
|
||||||
continue
|
continue
|
||||||
self._fan_level_map = prop.value_list.to_map()
|
self._fan_level_map = prop.value_list.to_map()
|
||||||
self._attr_fan_speed_list = list(self._fan_level_map.values())
|
self._attr_fan_speed_list = list(self._fan_level_map.values())
|
||||||
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
||||||
self._prop_fan_level = prop
|
self._prop_fan_level = prop
|
||||||
|
|
||||||
elif prop.name == 'battery-level':
|
elif prop.name == 'battery-level':
|
||||||
self._attr_supported_features |= VacuumEntityFeature.BATTERY
|
self._attr_supported_features |= VacuumEntityFeature.BATTERY
|
||||||
self._prop_battery_level = prop
|
self._prop_battery_level = prop
|
||||||
@ -155,16 +199,18 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
|||||||
elif action.name == 'stop-and-gocharge':
|
elif action.name == 'stop-and-gocharge':
|
||||||
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||||
self._action_stop_and_gocharge = action
|
self._action_stop_and_gocharge = action
|
||||||
|
|
||||||
elif action.name == 'identify':
|
elif action.name == 'identify':
|
||||||
self._attr_supported_features |= VacuumEntityFeature.LOCATE
|
self._attr_supported_features |= VacuumEntityFeature.LOCATE
|
||||||
self._action_identify = action
|
self._action_identify = action
|
||||||
|
|
||||||
async def async_start(self) -> None:
|
async def async_start(self) -> None:
|
||||||
"""Start or resume the cleaning task."""
|
"""Start or resume the cleaning task."""
|
||||||
if self.state.lower() in ['paused', '暂停中']:
|
if self._prop_status is not None:
|
||||||
await self.action_async(action=self._action_continue_sweep)
|
status = self.get_prop_value(prop=self._prop_status)
|
||||||
return
|
if (status in self._prop_status_paused
|
||||||
|
) and self._action_continue_sweep:
|
||||||
|
await self.action_async(action=self._action_continue_sweep)
|
||||||
|
return
|
||||||
await self.action_async(action=self._action_start_sweep)
|
await self.action_async(action=self._action_start_sweep)
|
||||||
|
|
||||||
async def async_stop(self, **kwargs: Any) -> None:
|
async def async_stop(self, **kwargs: Any) -> None:
|
||||||
@ -179,31 +225,74 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
|||||||
"""Set the vacuum cleaner to return to the dock."""
|
"""Set the vacuum cleaner to return to the dock."""
|
||||||
await self.action_async(action=self._action_stop_and_gocharge)
|
await self.action_async(action=self._action_stop_and_gocharge)
|
||||||
|
|
||||||
async def async_clean_spot(self, **kwargs: Any) -> None:
|
|
||||||
"""Perform a spot clean-up."""
|
|
||||||
|
|
||||||
async def async_locate(self, **kwargs: Any) -> None:
|
async def async_locate(self, **kwargs: Any) -> None:
|
||||||
"""Locate the vacuum cleaner."""
|
"""Locate the vacuum cleaner."""
|
||||||
await self.action_async(action=self._action_identify)
|
await self.action_async(action=self._action_identify)
|
||||||
|
|
||||||
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||||
"""Set fan speed."""
|
"""Set fan speed."""
|
||||||
|
fan_level_value = self.get_map_key(map_=self._fan_level_map,
|
||||||
|
value=fan_speed)
|
||||||
|
await self.set_property_async(prop=self._prop_fan_level,
|
||||||
|
value=fan_level_value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> Optional[str]:
|
def name(self) -> Optional[str]:
|
||||||
"""Return the current state of the vacuum cleaner."""
|
"""Name of the vacuum entity."""
|
||||||
return self.get_map_value(
|
return self._device_name
|
||||||
map_=self._status_map,
|
|
||||||
key=self.get_prop_value(prop=self._prop_status))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def battery_level(self) -> Optional[int]:
|
def battery_level(self) -> Optional[int]:
|
||||||
"""Return the current battery level of the vacuum cleaner."""
|
"""The current battery level of the vacuum cleaner."""
|
||||||
return self.get_prop_value(prop=self._prop_battery_level)
|
return self.get_prop_value(prop=self._prop_battery_level)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_speed(self) -> Optional[str]:
|
def fan_speed(self) -> Optional[str]:
|
||||||
"""Return the current fan speed of the vacuum cleaner."""
|
"""The current fan speed of the vacuum cleaner."""
|
||||||
return self.get_map_value(
|
return self.get_map_value(
|
||||||
map_=self._fan_level_map,
|
map_=self._fan_level_map,
|
||||||
key=self.get_prop_value(prop=self._prop_fan_level))
|
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||||
|
|
||||||
|
if HA_CORE_HAS_ACTIVITY:
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activity(self) -> Optional[str]:
|
||||||
|
"""The current vacuum activity.
|
||||||
|
To fix the HA warning below:
|
||||||
|
Detected that custom integration 'xiaomi_home' is setting state
|
||||||
|
directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum
|
||||||
|
.Vacuum'>)should implement the 'activity' property and return
|
||||||
|
its state using the VacuumActivity enum.This will stop working in
|
||||||
|
Home Assistant 2026.1.
|
||||||
|
|
||||||
|
Refer to
|
||||||
|
https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property
|
||||||
|
|
||||||
|
There are only 6 states in VacuumActivity enum. To be compatible with
|
||||||
|
more constants, try get matching VacuumActivity enum first, return state
|
||||||
|
string as before if there is no match. In Home Assistant 2026.1, every
|
||||||
|
state should map to a VacuumActivity enum.
|
||||||
|
"""
|
||||||
|
status = self.get_prop_value(prop=self._prop_status)
|
||||||
|
if status is None:
|
||||||
|
return None
|
||||||
|
if status in self._prop_status_cleaning:
|
||||||
|
return VacuumActivity.CLEANING
|
||||||
|
if status in self._prop_status_docked:
|
||||||
|
return VacuumActivity.DOCKED
|
||||||
|
if status in self._prop_status_paused:
|
||||||
|
return VacuumActivity.PAUSED
|
||||||
|
if status in self._prop_status_returning:
|
||||||
|
return VacuumActivity.RETURNING
|
||||||
|
if status in self._prop_status_error:
|
||||||
|
return VacuumActivity.ERROR
|
||||||
|
return VacuumActivity.IDLE
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[str]:
|
||||||
|
"""The current state of the vacuum."""
|
||||||
|
status = self.get_prop_value(prop=self._prop_status)
|
||||||
|
return None if (status is None) else self.get_map_value(
|
||||||
|
map_=self._status_map, key=status)
|
||||||
|
@ -52,25 +52,22 @@ from typing import Any, Optional
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (STATE_ON, STATE_OFF,
|
||||||
STATE_ON,
|
ATTR_TEMPERATURE,
|
||||||
STATE_OFF,
|
WaterHeaterEntity,
|
||||||
ATTR_TEMPERATURE,
|
WaterHeaterEntityFeature)
|
||||||
WaterHeaterEntity,
|
|
||||||
WaterHeaterEntityFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
from .miot.const import DOMAIN
|
from .miot.const import DOMAIN
|
||||||
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
|
||||||
from .miot.miot_spec import MIoTSpecProperty
|
from .miot.miot_spec import MIoTSpecProperty
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||||
@ -79,8 +76,8 @@ async def async_setup_entry(
|
|||||||
new_entities = []
|
new_entities = []
|
||||||
for miot_device in device_list:
|
for miot_device in device_list:
|
||||||
for data in miot_device.entity_list.get('water_heater', []):
|
for data in miot_device.entity_list.get('water_heater', []):
|
||||||
new_entities.append(WaterHeater(
|
new_entities.append(
|
||||||
miot_device=miot_device, entity_data=data))
|
WaterHeater(miot_device=miot_device, entity_data=data))
|
||||||
|
|
||||||
if new_entities:
|
if new_entities:
|
||||||
async_add_entities(new_entities)
|
async_add_entities(new_entities)
|
||||||
@ -95,9 +92,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
|||||||
|
|
||||||
_mode_map: Optional[dict[Any, Any]]
|
_mode_map: Optional[dict[Any, Any]]
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, miot_device: MIoTDevice,
|
||||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
entity_data: MIoTEntityData) -> None:
|
||||||
) -> None:
|
|
||||||
"""Initialize the Water heater."""
|
"""Initialize the Water heater."""
|
||||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||||
self._attr_temperature_unit = None
|
self._attr_temperature_unit = None
|
||||||
@ -112,20 +108,17 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
|||||||
for prop in entity_data.props:
|
for prop in entity_data.props:
|
||||||
# on
|
# on
|
||||||
if prop.name == 'on':
|
if prop.name == 'on':
|
||||||
|
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
|
||||||
self._prop_on = prop
|
self._prop_on = prop
|
||||||
# temperature
|
# temperature
|
||||||
if prop.name == 'temperature':
|
if prop.name == 'temperature':
|
||||||
if prop.value_range:
|
if not prop.value_range:
|
||||||
if (
|
_LOGGER.error('invalid temperature value_range format, %s',
|
||||||
self._attr_temperature_unit is None
|
self.entity_id)
|
||||||
and prop.external_unit
|
continue
|
||||||
):
|
if prop.external_unit:
|
||||||
self._attr_temperature_unit = prop.external_unit
|
self._attr_temperature_unit = prop.external_unit
|
||||||
self._prop_temp = prop
|
self._prop_temp = prop
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
|
||||||
'invalid temperature value_range format, %s',
|
|
||||||
self.entity_id)
|
|
||||||
# target-temperature
|
# target-temperature
|
||||||
if prop.name == 'target-temperature':
|
if prop.name == 'target-temperature':
|
||||||
if not prop.value_range:
|
if not prop.value_range:
|
||||||
@ -135,7 +128,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
|||||||
continue
|
continue
|
||||||
self._attr_min_temp = prop.value_range.min_
|
self._attr_min_temp = prop.value_range.min_
|
||||||
self._attr_max_temp = prop.value_range.max_
|
self._attr_max_temp = prop.value_range.max_
|
||||||
self._attr_precision = prop.value_range.step
|
self._attr_target_temperature_step = prop.value_range.step
|
||||||
if self._attr_temperature_unit is None and prop.external_unit:
|
if self._attr_temperature_unit is None and prop.external_unit:
|
||||||
self._attr_temperature_unit = prop.external_unit
|
self._attr_temperature_unit = prop.external_unit
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
@ -144,17 +137,15 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
|||||||
# mode
|
# mode
|
||||||
if prop.name == 'mode':
|
if prop.name == 'mode':
|
||||||
if not prop.value_list:
|
if not prop.value_list:
|
||||||
_LOGGER.error(
|
_LOGGER.error('mode value_list is None, %s', self.entity_id)
|
||||||
'mode value_list is None, %s', self.entity_id)
|
|
||||||
continue
|
continue
|
||||||
self._mode_map = prop.value_list.to_map()
|
self._mode_map = prop.value_list.to_map()
|
||||||
self._attr_operation_list = list(self._mode_map.values())
|
self._attr_operation_list = list(self._mode_map.values())
|
||||||
self._attr_supported_features |= (
|
|
||||||
WaterHeaterEntityFeature.OPERATION_MODE)
|
|
||||||
self._prop_mode = prop
|
self._prop_mode = prop
|
||||||
if not self._attr_operation_list:
|
if not self._attr_operation_list:
|
||||||
self._attr_operation_list = [STATE_ON]
|
self._attr_operation_list = [STATE_ON]
|
||||||
self._attr_operation_list.append(STATE_OFF)
|
self._attr_operation_list.append(STATE_OFF)
|
||||||
|
self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Turn the water heater on."""
|
"""Turn the water heater on."""
|
||||||
@ -165,50 +156,45 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
|||||||
await self.set_property_async(prop=self._prop_on, value=False)
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set the temperature the water heater should heat water to."""
|
"""Set the target temperature."""
|
||||||
await self.set_property_async(
|
await self.set_property_async(prop=self._prop_target_temp,
|
||||||
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
|
value=kwargs[ATTR_TEMPERATURE])
|
||||||
|
|
||||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
"""Set the operation mode of the water heater.
|
"""Set the operation mode of the water heater."""
|
||||||
Must be in the operation_list.
|
|
||||||
"""
|
|
||||||
if operation_mode == STATE_OFF:
|
if operation_mode == STATE_OFF:
|
||||||
await self.set_property_async(prop=self._prop_on, value=False)
|
await self.set_property_async(prop=self._prop_on, value=False)
|
||||||
return
|
return
|
||||||
if operation_mode == STATE_ON:
|
if operation_mode == STATE_ON:
|
||||||
await self.set_property_async(prop=self._prop_on, value=True)
|
await self.set_property_async(prop=self._prop_on, value=True)
|
||||||
return
|
return
|
||||||
if self.get_prop_value(prop=self._prop_on) is False:
|
if self.get_prop_value(prop=self._prop_on) is not True:
|
||||||
await self.set_property_async(
|
await self.set_property_async(prop=self._prop_on,
|
||||||
prop=self._prop_on, value=True, update=False)
|
value=True,
|
||||||
await self.set_property_async(
|
write_ha_state=False)
|
||||||
prop=self._prop_mode,
|
await self.set_property_async(prop=self._prop_mode,
|
||||||
value=self.get_map_key(
|
value=self.get_map_key(
|
||||||
map_=self._mode_map,
|
map_=self._mode_map,
|
||||||
value=operation_mode))
|
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)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> Optional[float]:
|
def current_temperature(self) -> Optional[float]:
|
||||||
"""Return the current temperature."""
|
"""The current temperature."""
|
||||||
return self.get_prop_value(prop=self._prop_temp)
|
return (None if self._prop_temp is None else self.get_prop_value(
|
||||||
|
prop=self._prop_temp))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> Optional[float]:
|
def target_temperature(self) -> Optional[float]:
|
||||||
"""Return the target temperature."""
|
"""The target temperature."""
|
||||||
return self.get_prop_value(prop=self._prop_target_temp)
|
return (None if self._prop_target_temp is None else self.get_prop_value(
|
||||||
|
prop=self._prop_target_temp))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self) -> Optional[str]:
|
def current_operation(self) -> Optional[str]:
|
||||||
"""Return the current mode."""
|
"""The current mode."""
|
||||||
if self.get_prop_value(prop=self._prop_on) is False:
|
if self.get_prop_value(prop=self._prop_on) is False:
|
||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
|
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
|
||||||
return STATE_ON
|
return STATE_ON
|
||||||
return self.get_map_value(
|
return (None if self._prop_mode is None else self.get_map_value(
|
||||||
map_=self._mode_map,
|
map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)))
|
||||||
key=self.get_prop_value(prop=self._prop_mode))
|
|
||||||
|
@ -98,6 +98,8 @@ footer :(可选)关联的 issue 或 pull request 编号。
|
|||||||
|
|
||||||
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
|
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
|
||||||
|
|
||||||
|
当您第一次提交拉取请求时,GitHub Action 会提示您签署贡献者许可协议(Contributor License Agreement,CLA)。只有签署了 CLA ,本项目才会合入您的拉取请求。
|
||||||
|
|
||||||
## 获取帮助
|
## 获取帮助
|
||||||
|
|
||||||
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。
|
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。
|
||||||
|
@ -33,9 +33,11 @@ git checkout v1.0.0
|
|||||||
|
|
||||||
### 方法 2: [HACS](https://hacs.xyz/)
|
### 方法 2: [HACS](https://hacs.xyz/)
|
||||||
|
|
||||||
HACS > 右上角三个点 > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > 点击 HACS 的 New 或 Available for download 分类下的 Xiaomi Home ,进入集成详情页 > DOWNLOAD
|
一键从 HACS 安装米家集成:
|
||||||
|
|
||||||
> 米家集成暂未添加到 HACS 商店,敬请期待。
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
|
||||||
|
|
||||||
|
或者,HACS > 在搜索框中输入 **Xiaomi Home** > 点击 **Xiaomi Home** ,进入集成详情页 > DOWNLOAD
|
||||||
|
|
||||||
### 方法 3:通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装
|
### 方法 3:通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
|
|||||||
|
|
||||||
[设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录
|
[设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录
|
||||||
|
|
||||||
[](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 设备
|
### 添加 MIoT 设备
|
||||||
|
|
||||||
@ -59,7 +61,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
|
|||||||
|
|
||||||
方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录
|
方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
[](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
|
||||||
|
|
||||||
### 修改配置项
|
### 修改配置项
|
||||||
|
|
||||||
@ -91,7 +93,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
|
|||||||
|
|
||||||
- 米家集成是否支持本地化控制?
|
- 米家集成是否支持本地化控制?
|
||||||
|
|
||||||
米家集成支持通过[小米中枢网关](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search)(固件版本 3.4.0_000 以上)或内置中枢网关(软件版本 0.8.0 以上)的米家设备实现本地化控制。如果没有小米中枢网关或其他带中枢网关功能的设备,那么所有控制指令都会通过小米云发送。支持 Home Assistant 本地化控制的小米中枢网关(含内置中枢网关)的固件尚未发布,固件升级计划请参阅 MIoT 团队的通知。
|
米家集成支持通过[小米中枢网关](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search)(固件版本 3.3.0_0023 及以上)或内置中枢网关(软件版本 0.8.9 及以上)的米家设备实现本地化控制。如果没有小米中枢网关或其他带中枢网关功能的设备,那么所有控制指令都会通过小米云发送。支持 Home Assistant 本地化控制的小米中枢网关(含内置中枢网关)的固件尚未发布,固件升级计划请参阅 MIoT 团队的通知。
|
||||||
|
|
||||||
小米中枢网关仅在中国大陆可用,在其他地区不可用。
|
小米中枢网关仅在中国大陆可用,在其他地区不可用。
|
||||||
|
|
||||||
@ -155,6 +157,8 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
|
|||||||
|
|
||||||
转换后的实体为 Event,事件参数同时传递给实体的 `_trigger_event` 。
|
转换后的实体为 Event,事件参数同时传递给实体的 `_trigger_event` 。
|
||||||
|
|
||||||
|
MIoT-Spec-V2 事件的 arguments 字段是事件的参数列表,列表元素代表同服务下属性的 piid 。例如,小米智能无线开关(双开)的 [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057)的 siid=2 无线开关服务下包含 eiid=1014 长按事件,该事件触发时会携带一个 piid=2 的按键类型属性作为事件参数, debug 等级日志会打印 `长按, attributes: {'按键类型': 1}` (日志示例,按键类型为 1 表示右键触发了长按事件)。
|
||||||
|
|
||||||
- 方法(Action)
|
- 方法(Action)
|
||||||
|
|
||||||
| in(输入参数列表) | 转换后的实体 |
|
| in(输入参数列表) | 转换后的实体 |
|
||||||
@ -289,39 +293,37 @@ event instance name 下的值表示转换后实体所用的 `_attr_device_class`
|
|||||||
|
|
||||||
### MIoT-Spec-V2 过滤规则
|
### MIoT-Spec-V2 过滤规则
|
||||||
|
|
||||||
`spec_filter.json` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。
|
`spec_filter.yaml` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。
|
||||||
|
|
||||||
`spec_filter.json`的格式如下:
|
`spec_filter.yaml`的格式如下:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
{
|
<MIoT-Spec-V2 device instance urn without the version field>:
|
||||||
"<MIoT-Spec-V2 device instance>":{
|
services: list<service_iid: str>
|
||||||
"services": list<service_iid: str>,
|
properties: list<service_iid.property_iid: str>
|
||||||
"properties": list<service_iid.property_iid: str>,
|
events: list<service_iid.event_iid: str>
|
||||||
"events": list<service_iid.event_iid: str>,
|
actions: list<service_iid.action_iid: str>
|
||||||
"actions": list<service_iid.action_iid: str>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`spec_filter.json` 的键值为 MIoT-Spec-V2 设备实例的 urn (不含版本号“version”字段)。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.json` 的键值不需要指定设备实例的版本号。
|
`spec_filter.yaml` 的键值为 MIoT-Spec-V2 设备实例的 urn (不含版本号“version”字段)。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.yaml` 的键值不需要指定设备实例的版本号。
|
||||||
|
|
||||||
设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。
|
设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
{
|
urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
|
||||||
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{
|
services:
|
||||||
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance.
|
- '*' # 排除所有服务,相当于排除拥有该 MIoT-Spec-V2 的设备。
|
||||||
},
|
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
|
||||||
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
|
services:
|
||||||
"services": ["3"], # Filter out the service whose iid=3.
|
- '3' # 排除 siid=3 的服务。
|
||||||
"properties": ["4.*"] # Filter out all properties in the service whose iid=4.
|
properties:
|
||||||
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service.
|
- '4.*' # 排除 siid=4 服务的所有属性。
|
||||||
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service.
|
events:
|
||||||
}
|
- '4.1' # 排除 siid=4 服务的 eiid=1 的事件。
|
||||||
}
|
actions:
|
||||||
|
- '4.1' # 排除 siid=4 服务的 aiid=1 的方法。
|
||||||
```
|
```
|
||||||
|
|
||||||
所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。
|
所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。
|
||||||
@ -353,7 +355,7 @@ instance code 为 MIoT-Spec-V2 实例代码,格式如下:
|
|||||||
```
|
```
|
||||||
service:<siid> # 服务
|
service:<siid> # 服务
|
||||||
service:<siid>:property:<piid> # 属性
|
service:<siid>:property:<piid> # 属性
|
||||||
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的值
|
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引值
|
||||||
service:<siid>:event:<eiid> # 事件
|
service:<siid>:event:<eiid> # 事件
|
||||||
service:<siid>:action:<aiid> # 方法
|
service:<siid>:action:<aiid> # 方法
|
||||||
```
|
```
|
||||||
@ -376,7 +378,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/translations/` 路径下的 `specv2entity.py`、`spec_filter.json`、`multi_lang.json` 文件的内容,需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则
|
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的任何文件(`spec_filter.yaml`、`spec_modify.yaml`、`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(
|
MIOT_I18N_RELATIVE_PATH: str = path.join(
|
||||||
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
|
||||||
SPEC_BOOL_TRANS_FILE = path.join(
|
SPEC_BOOL_TRANS_FILE = path.join(
|
||||||
ROOT_PATH,
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
|
||||||
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
|
|
||||||
SPEC_FILTER_FILE = path.join(
|
SPEC_FILTER_FILE = path.join(
|
||||||
ROOT_PATH,
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
|
||||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
|
SPEC_MULTI_LANG_FILE = path.join(
|
||||||
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/multi_lang.json')
|
||||||
|
SPEC_ADD_FILE = path.join(
|
||||||
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_add.json')
|
||||||
|
SPEC_MODIFY_FILE = path.join(
|
||||||
|
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
|
||||||
|
|
||||||
|
|
||||||
def load_json_file(file_path: str) -> Optional[dict]:
|
def load_json_file(file_path: str) -> Optional[dict]:
|
||||||
@ -27,7 +31,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
|||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
return json.load(file)
|
return json.load(file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
_LOGGER.info('%s is not found.', file_path,)
|
_LOGGER.info('%s is not found.', file_path)
|
||||||
return None
|
return None
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
_LOGGER.info('%s is not a valid JSON file.', file_path)
|
_LOGGER.info('%s is not a valid JSON file.', file_path)
|
||||||
@ -36,7 +40,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
|||||||
|
|
||||||
def save_json_file(file_path: str, data: dict) -> None:
|
def save_json_file(file_path: str, data: dict) -> None:
|
||||||
with open(file_path, 'w', encoding='utf-8') as file:
|
with open(file_path, 'w', encoding='utf-8') as file:
|
||||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
json.dump(data, file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def load_yaml_file(file_path: str) -> Optional[dict]:
|
def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||||
@ -53,8 +57,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
|
|||||||
|
|
||||||
def save_yaml_file(file_path: str, data: dict) -> None:
|
def save_yaml_file(file_path: str, data: dict) -> None:
|
||||||
with open(file_path, 'w', encoding='utf-8') as file:
|
with open(file_path, 'w', encoding='utf-8') as file:
|
||||||
yaml.safe_dump(
|
yaml.safe_dump(data,
|
||||||
data, file, default_flow_style=False, allow_unicode=True, indent=2)
|
file,
|
||||||
|
default_flow_style=False,
|
||||||
|
allow_unicode=True,
|
||||||
|
indent=2,
|
||||||
|
sort_keys=False)
|
||||||
|
|
||||||
|
|
||||||
def dict_str_str(d: dict) -> bool:
|
def dict_str_str(d: dict) -> bool:
|
||||||
@ -128,37 +136,156 @@ def bool_trans(d: dict) -> bool:
|
|||||||
for key, trans in d['translate'].items():
|
for key, trans in d['translate'].items():
|
||||||
trans_keys: set[str] = set(trans.keys())
|
trans_keys: set[str] = set(trans.keys())
|
||||||
if set(trans.keys()) != default_keys:
|
if set(trans.keys()) != default_keys:
|
||||||
_LOGGER.info(
|
_LOGGER.info('bool trans inconsistent, %s, %s, %s', key,
|
||||||
'bool trans inconsistent, %s, %s, %s',
|
default_keys, trans_keys)
|
||||||
key, default_keys, trans_keys)
|
|
||||||
return False
|
return False
|
||||||
return True
|
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:
|
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||||
_LOGGER.info('invalid type')
|
_LOGGER.info('invalid type')
|
||||||
return False
|
return False
|
||||||
if dict1.keys() != dict2.keys():
|
if dict1.keys() != dict2.keys():
|
||||||
_LOGGER.info(
|
_LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),
|
||||||
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys())
|
dict2.keys())
|
||||||
return False
|
return False
|
||||||
for key in dict1:
|
for key in dict1:
|
||||||
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
||||||
if not compare_dict_structure(dict1[key], dict2[key]):
|
if not compare_dict_structure(dict1[key], dict2[key]):
|
||||||
_LOGGER.info(
|
_LOGGER.info('inconsistent key values, dict, %s', key)
|
||||||
'inconsistent key values, dict, %s', key)
|
|
||||||
return False
|
return False
|
||||||
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
||||||
if not all(
|
if not all(
|
||||||
isinstance(i, type(j))
|
isinstance(i, type(j))
|
||||||
for i, j in zip(dict1[key], dict2[key])):
|
for i, j in zip(dict1[key], dict2[key])):
|
||||||
_LOGGER.info(
|
_LOGGER.info('inconsistent key values, list, %s', key)
|
||||||
'inconsistent key values, list, %s', key)
|
|
||||||
return False
|
return False
|
||||||
elif not isinstance(dict1[key], type(dict2[key])):
|
elif not isinstance(dict1[key], type(dict2[key])):
|
||||||
_LOGGER.info(
|
_LOGGER.info('inconsistent key values, type, %s', key)
|
||||||
'inconsistent key values, type, %s', key)
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -181,6 +308,22 @@ def sort_spec_filter(file_path: str):
|
|||||||
return filter_data
|
return filter_data
|
||||||
|
|
||||||
|
|
||||||
|
def sort_spec_add(file_path: str):
|
||||||
|
filter_data = load_json_file(file_path=file_path)
|
||||||
|
assert isinstance(filter_data, dict), f'{file_path} format error'
|
||||||
|
return dict(sorted(filter_data.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def sort_multi_lang(file_path: str):
|
||||||
|
return sort_spec_add(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_spec_modify(file_path: str):
|
||||||
|
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
|
@pytest.mark.github
|
||||||
def test_bool_trans():
|
def test_bool_trans():
|
||||||
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
|
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'
|
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
|
@pytest.mark.github
|
||||||
def test_miot_i18n():
|
def test_miot_i18n():
|
||||||
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
||||||
@ -222,7 +389,8 @@ def test_miot_lang_integrity():
|
|||||||
# pylint: disable=import-outside-toplevel
|
# pylint: disable=import-outside-toplevel
|
||||||
from miot.const import INTEGRATION_LANGUAGES
|
from miot.const import INTEGRATION_LANGUAGES
|
||||||
integration_lang_list: list[str] = [
|
integration_lang_list: list[str] = [
|
||||||
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())]
|
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())
|
||||||
|
]
|
||||||
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
|
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
|
||||||
assert len(translations_names) == len(integration_lang_list)
|
assert len(translations_names) == len(integration_lang_list)
|
||||||
assert translations_names == set(integration_lang_list)
|
assert translations_names == set(integration_lang_list)
|
||||||
@ -238,21 +406,18 @@ def test_miot_lang_integrity():
|
|||||||
default_dict = load_json_file(
|
default_dict = load_json_file(
|
||||||
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
|
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
|
||||||
for name in list(integration_lang_list)[1:]:
|
for name in list(integration_lang_list)[1:]:
|
||||||
compare_dict = load_json_file(
|
compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))
|
||||||
path.join(TRANS_RELATIVE_PATH, name))
|
|
||||||
if not compare_dict_structure(default_dict, compare_dict):
|
if not compare_dict_structure(default_dict, compare_dict):
|
||||||
_LOGGER.info(
|
_LOGGER.info('compare_dict_structure failed /translations, %s',
|
||||||
'compare_dict_structure failed /translations, %s', name)
|
name)
|
||||||
assert False
|
assert False
|
||||||
# Check i18n files structure
|
# Check i18n files structure
|
||||||
default_dict = load_json_file(
|
default_dict = load_json_file(
|
||||||
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
|
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
|
||||||
for name in list(integration_lang_list)[1:]:
|
for name in list(integration_lang_list)[1:]:
|
||||||
compare_dict = load_json_file(
|
compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))
|
||||||
path.join(MIOT_I18N_RELATIVE_PATH, name))
|
|
||||||
if not compare_dict_structure(default_dict, compare_dict):
|
if not compare_dict_structure(default_dict, compare_dict):
|
||||||
_LOGGER.info(
|
_LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)
|
||||||
'compare_dict_structure failed /miot/i18n, %s', name)
|
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
@ -270,12 +435,27 @@ def test_miot_data_sort():
|
|||||||
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
|
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
|
||||||
' and run the following command sorting, ',
|
' and run the following command sorting, ',
|
||||||
'pytest -s -v -m update ./test/check_rule_format.py')
|
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||||
|
assert json.dumps(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(
|
assert json.dumps(
|
||||||
load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
|
load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(
|
||||||
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
|
sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (
|
||||||
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
|
f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'
|
||||||
' and run the following command sorting, ',
|
' and run the following command sorting, ',
|
||||||
'pytest -s -v -m update ./test/check_rule_format.py')
|
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||||
|
assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(
|
||||||
|
sort_spec_add(file_path=SPEC_ADD_FILE)), (
|
||||||
|
f'{SPEC_ADD_FILE} not sorted, goto project root path'
|
||||||
|
' and run the following command sorting, ',
|
||||||
|
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||||
|
assert json.dumps(load_yaml_file(file_path=SPEC_MODIFY_FILE)) == json.dumps(
|
||||||
|
sort_spec_modify(file_path=SPEC_MODIFY_FILE)), (
|
||||||
|
f'{SPEC_MODIFY_FILE} not sorted, goto project root path'
|
||||||
|
' and run the following command sorting, ',
|
||||||
|
'pytest -s -v -m update ./test/check_rule_format.py')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.update
|
@pytest.mark.update
|
||||||
@ -286,3 +466,11 @@ def test_sort_spec_data():
|
|||||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||||
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||||
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
|
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
|
||||||
|
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
|
||||||
|
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
|
||||||
|
sort_data = sort_spec_add(file_path=SPEC_ADD_FILE)
|
||||||
|
save_json_file(file_path=SPEC_ADD_FILE, data=sort_data)
|
||||||
|
_LOGGER.info('%s formatted.', SPEC_ADD_FILE)
|
||||||
|
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
|
||||||
|
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
|
# Your central hub gateway did
|
||||||
test_did = '111111'
|
test_did = '111111'
|
||||||
# Your central hub gateway did
|
# Your central hub gateway token
|
||||||
test_token = '11223344556677d9a03d43936fc384205'
|
test_token = '11223344556677d9a03d43936fc384205'
|
||||||
test_model = 'xiaomi.gateway.hub1'
|
test_model = 'xiaomi.gateway.hub1'
|
||||||
# Your computer interface list, such as enp3s0, wlp5s0
|
# 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 asyncio.sleep(0.2)
|
||||||
|
|
||||||
await miot_lan.deinit_async()
|
await miot_lan.deinit_async()
|
||||||
|
await mips_service.deinit_async()
|
||||||
|
await miot_network.deinit_async()
|
||||||
|
Reference in New Issue
Block a user