57 Commits

Author SHA1 Message Date
00f24bd3e1 docs: update changelog and version to v0.3.1 (#1049)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-29 09:27:24 +08:00
f384034854 Fix specs (#1037) 2025-04-29 09:11:17 +08:00
b0204ad9b7 fix: the humidifier property (#1035)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-28 09:35:47 +08:00
b4ece958ac fix: set fan on/off state before set the percentage (#1031)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-27 20:13:34 +08:00
db77af8a13 feat: make air-purifier as fan (#987)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-27 16:11:55 +08:00
a9f1fc630d fix: zhimi.fan.v3 fan level (#1033) 2025-04-27 15:40:29 +08:00
51a27a1e30 Merge pull request #1032 from XiaoMi/revert-1027-zhimi.fan.v3
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
Revert "add device zhimi.fan.v3  prop.2.2"
2025-04-27 10:18:24 +08:00
2e0ea642a4 Revert "add device zhimi.fan.v3 prop.2.2" 2025-04-27 10:16:27 +08:00
80d962897a Merge pull request #1027 from XiaoMi/zhimi.fan.v3
add device zhimi.fan.v3  prop.2.2
2025-04-27 10:15:55 +08:00
d17784070d add device zhimi.fan.v3 prop.2.6 2025-04-27 09:56:05 +08:00
218c96e5e6 add device zhimi.fan.v3 prop.2.2 2025-04-25 14:33:07 +08:00
eacc0d02da fix: update device list error when there is no shared devices (#1024)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-25 12:23:04 +08:00
23f0a2d360 docs: update changelog and version to v0.3.0 (#1022)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-25 08:40:14 +08:00
3abccc2491 feat: import shared devices (#1021) 2025-04-25 08:29:11 +08:00
7a459de766 fix: ignore 'Event loop is closed' when unsub a closed event loop (#991)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-24 17:55:15 +08:00
2f619ff51d fix: contact-state for linp.magnet.m1 and loock.safe.v1 (#977)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-10 10:19:50 +08:00
cb34b6ce46 chore: change issue_template (#964)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-05 11:44:38 +08:00
d0a7940c59 feat: support _attr_hvac_action of the climate entity (#956) 2025-04-05 11:38:03 +08:00
899d616da4 feat: custom defined MIoT-Spec-V2 instance (#953) 2025-04-05 11:37:17 +08:00
c6be6be1ec fix: initialization problem of _attr_fan_modes (#955)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-04-02 11:30:58 +08:00
77b0a4531b fix: fix some specs (#949) 2025-04-02 08:53:15 +08:00
7d9250914c docs: update changelog and version to v0.2.4 (#937)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-28 09:26:10 +08:00
a09289ef90 Fix specs (#929)
* fix: cuco.plug.cp2 voltage and power value ratio

* fix: cgllc.airmonitor.s1 unit ppb

* fix: roswan.waterpuri.lte01 tds unit

* fix: lumi.relay.c2acn01 power consumption unit

* fix: xiaomi.bhf_light.s1 fan level of ventilation

* fix: error log
2025-03-28 09:10:09 +08:00
b0428dc95a feat: make submersion-state, contact-state, occupancy-status as binary_sensor (#905)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-27 15:45:46 +08:00
19ed04f2f5 fix: correct unit,icon and translations for hhcc-v1 (#927)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-25 09:54:02 +08:00
e174a73f52 Update spec_modify.yaml (#921)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-24 16:34:48 +08:00
a1aa1c024f docs: update changelog and version to v0.2.3 (#911)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-21 09:53:39 +08:00
372e635681 Fix specs (#910)
* fix: chuangmi.plug.212a01 power consumption value

* fix: yeelink.bhf_light.v10 mode description in English
2025-03-21 09:36:14 +08:00
3759aa9a1b fix: climate on/off feature initialization (#899)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-20 18:02:25 +08:00
60d054cf19 docs: update changelog and version to v0.2.2 (#882)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-14 08:47:45 +08:00
6680d9e8cb feat: add conversion rules for the air-conditioner service and the air-fresh service (#879) 2025-03-14 08:23:03 +08:00
0ef8cb6370 fix: xiaomi.aircondition.m9 humidity-range unit (#878)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-13 17:41:02 +08:00
8f0a69c611 feat: convert the mode of the ptc bath heater to the preset mode (#874) 2025-03-13 17:37:44 +08:00
8be0fa5d61 fix: MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2 (#866)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-12 15:22:03 +08:00
07cb4ed193 feat: avoid setting icon when device_class is defined (#855) 2025-03-12 15:17:02 +08:00
5c46504d0e docs: update changelog and version to v0.2.1 (#848)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-07 14:20:17 +08:00
97d89b3a04 feat: thermostat preset mode (#833)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-03-07 08:48:35 +08:00
4482d257dc revert: multi_lang.json (#834) 2025-03-07 08:48:12 +08:00
d0387be15b fix #838 (#839) 2025-03-07 08:47:52 +08:00
27cf1085bd fix: opening and closing for linp.wopener.wd1lb (#826) 2025-03-07 08:46:17 +08:00
e69448f2eb feat: add entity_category for indicator-light (#697)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-03-05 16:13:17 +08:00
7901607648 fix: fan-level without value-list but with value-range (#808) 2025-03-05 15:31:18 +08:00
5adcb7ce00 fix: wind-reverse format type (#810) 2025-03-05 15:31:02 +08:00
672e5b3f5d docs: update changelog and version to v0.2.0 (#783)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-02-28 17:39:31 +08:00
417af787c4 fix: some event:motion-detected does not contain 'arguments' (#712) 2025-02-28 16:45:17 +08:00
6f058bf392 fix: fix sensor display precision (#708) 2025-02-28 16:43:41 +08:00
52485d8c7a Update README_ZH.md (#747)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-02-25 09:04:20 +08:00
48554ec0f7 feat: add support for electric blanket (#781) 2025-02-25 08:58:23 +08:00
0ce94f7316 feat: add device with motor-control service as cover entity (#688) 2025-02-25 08:55:06 +08:00
20b0004746 refactor: refactor climate.py (#614)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
* feat: add thermostat as climate entity

* feat: add bath-heater as climate entity

* refactor: climate entity

* fix: thermostat on/off

* fix: get the current fan mode

* perf: get fan level

* fix: fix climate hvac_mode

* fix: misuse of getting key or value from dict[int, any]

* style: add comments

* style: format the file based on google style

* fix: initialize _attr_hvac_modes

* feat: add heat and defog mode of ptc bath heater

---------

Co-authored-by: topsworld <sworldtop@gmail.com>
2025-02-19 09:21:46 +08:00
57422ddf0d fix: fan level with value-list & fan reverse (#689)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
* fix: fan level with value-list

* feat: update wind-reverse logic

* feat: use macro define for fan entity

* fix: fix fan async_set_direction error

---------

Co-authored-by: topsworld <sworldtop@gmail.com>
2025-01-24 10:43:49 +08:00
2e60962e94 fix: climate error (#690)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-01-22 20:01:39 +08:00
52fd6371ab fix: fix water heater error & some type error (#684)
* fix: fix water heater target-temp error

* feat: update miot device logic

* fix: fix some type error
2025-01-22 19:55:41 +08:00
8778b00c3a feat: support modify spec and value conversion (#663)
* fix: fix miot_device type error

* fix: fix type error

* feat: remove spec cache storage

* feat: update std_lib and multi_lang logic

* feat: update entity value-range

* feat: update value-list logic

* feat: update prop.format_ logic

* fix: fix miot cloud log error

* fix: fix fan entity

* style: ignore type error

* style: rename spec_filter func name

* feat: move bool_trans from storage to spec

* feat: move sepc_filter from storage to spec, use the YAML format file

* feat: same prop supports multiple sub

* feat: same event supports multiple sub

* fix: fix device remove error

* feat: add func slugify_did

* fix: fix multi lang error

* feat: update action debug logic

* feat: ignore normal disconnect log

* feat: support binary mode

* feat: change miot spec name type define

* style: ignore i18n tranlate type error

* fix: fix pylint warning

* fix: miot storage type error

* feat: support binary display mode configure

* feat: set default sensor state_class

* fix: fix sensor entity type error

* fix: fix __init__ type error

* feat: update test case logic

* fix: github actions add dependencies lib

* fix: fix some type error

* feat: update device list changed notify logic

* feat: update prop expr logic

* feat: add spec modify

* feat: update device sub id logic

* feat: update get miot client instance logic

* fix: fix some type error

* feat: update miot device unit and icon trans

* perf: update spec trans entity logic

* feat: update spec trans entity rule

* feat: update spec_modify

* feat: update sensor ENUM icon

* fix: fix miot device error

* fix: fix miot spec error

* featL update format check and spec modify file

* feat: update checkout rule format

* feat: handle special property.unit

* feat: add expr for cuco-cp1md

* feat: fix climate hvac error

* feat: set sensor suggested display precision

* feat: update climate set hvac logic

* feat: add expr for cuco-v3

* feat: update spec expr for chuangmi-212a01
2025-01-22 19:21:02 +08:00
3c16f0ffbb docs: CLA request (#681)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-01-22 16:42:08 +08:00
3a5b641ec7 fix: fix variable name or comment errors & fix test_lan error (#678)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
* fix: renamed _url_addr_list to _http_addr_map for clarity

* fix: redirected ping command output to DEVNULL

* fix: add resource cleanup in test_lan_async
2025-01-21 20:16:15 +08:00
8fb6f9065e fix: add prop trans rule for surge-power (#595)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
* fix: potentiaol problems for properties identification error for sensors

* fix incorrect SPEC name
2025-01-21 16:09:58 +08:00
32 changed files with 2745 additions and 1363 deletions

View File

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

View File

@ -1,4 +1,95 @@
# CHANGELOG
## v0.3.1
### Changed
- Setting the fan speed level when the fan is off will turning the fan on first. [#1031](https://github.com/XiaoMi/ha_xiaomi_home/pull/1031)
### Fixed
- Fix update device list error when there is no shared devices. [#1024](https://github.com/XiaoMi/ha_xiaomi_home/pull/1024)
- Fix the humidifier get_prop_value error when the property is None. [#1035](https://github.com/XiaoMi/ha_xiaomi_home/pull/1035)
- Fix the MIoT-Spec-V2 of zhimi.fan.v3 fan-level, cuco.plug.cp1md voltage and current, zimi.plug.zncz01 electric-power, giot.plug.v8icm power-consumption unit, yunmi.kettle.r3 tds unit, and dmaker.fan.p5 fan-level. [#1037](https://github.com/XiaoMi/ha_xiaomi_home/pull/1037)
## v0.3.0
注意v0.3.0 变更了部分实体 unique_id 的生成规则,如果勾选 xiaomi_home > 配置 > 更新实体转换规则,会导致部分实体已配置的自动化失效。如果想要避免重新配置大量自动化,可使用这个[补丁](https://github.com/XiaoMi/ha_xiaomi_home/pull/972)。
CAUTION: v0.3.0 changes the unique_id of some entities. If you check the option `xiaomi_home > CONFIGURE > Update entity conversion rules`, it may cause the automation settings for these entities to fail. To avoid having to reconfigure a large number of automation settings, you can use this [patch](https://github.com/XiaoMi/ha_xiaomi_home/pull/972).
### Added
- Import the devices in the shared homes and the separated shared devices. [#1021](https://github.com/XiaoMi/ha_xiaomi_home/pull/1021)
- Support _attr_hvac_action of the climate entity. [#956](https://github.com/XiaoMi/ha_xiaomi_home/pull/956)
- Add custom defined MIoT-Spec-V2 instance via spec_add.json. [#953](https://github.com/XiaoMi/ha_xiaomi_home/pull/953)
### Fixed
- Ignore 'Event loop is closed' when unsub a closed event loop. [#991](https://github.com/XiaoMi/ha_xiaomi_home/pull/991)
- Fix contact-state for linp.magnet.m1 and loock.safe.v1. [#977](https://github.com/XiaoMi/ha_xiaomi_home/pull/977)
- Fix the mode initialization error of aupu.bhf_light.s368m. [#955](https://github.com/XiaoMi/ha_xiaomi_home/pull/955)
- Fix the MIoT-Spec-V2 of lumi.gateway.mcn001, qmi.plug.psv3, lumi.motion.acn001, izq.sensor_occupy.24, linp.sensor_occupy.hb01 and yunmi.waterpuri.s20. [#949](https://github.com/XiaoMi/ha_xiaomi_home/pull/949)
## v0.2.4
### Added
- Convert the submersion-state, the contact-state and the occupancy-status property to the binary_sensor entity. [#905](https://github.com/XiaoMi/ha_xiaomi_home/pull/905)
### Changed
- suittc.airrtc.wk168 mode descriptions are set to strings of numbers from 1 to 16. [#921](https://github.com/XiaoMi/ha_xiaomi_home/pull/921)
- Do not set _attr_suggested_display_precision when the spec.expr is set in spec_modify.yaml [#929](https://github.com/XiaoMi/ha_xiaomi_home/pull/929)
- Set "unknown event msg" log to info level.
### Fixed
- hhcc.plantmonitor.v1 soil moisture and soil ec icon and unit. [#927](https://github.com/XiaoMi/ha_xiaomi_home/pull/27)
- cuco.plug.cp2 voltage and power value ratio.
- cgllc.airmonitor.s1 unit ppb.
- roswan.waterpuri.lte01 tds unit.
- lumi.relay.c2acn01 power consumption unit
- xiaomi.bhf_light.s1 fan level of ventilation.
## v0.2.3
### Changed
- Specify the service name and the property name during the climate entity's on/off feature initialization. [#899](https://github.com/XiaoMi/ha_xiaomi_home/pull/899)
- Remove the useless total-battery property from `SPEC_PROP_TRANS_MAP`.
### Fixed
- Fix the hvac mode setting error when changing the preset mode of the ptc-bath-heater.
- Fix the ambiguous descriptions of yeelink.bhf_light.v10 ptc-bath-heater mode value-list.
- Fix the power consumption value of chuangmi.plug.212a01. [#910](https://github.com/XiaoMi/ha_xiaomi_home/pull/910)
## v0.2.2
This version has modified the conversion rules of the climate entity, which will have effect on the devices with the ptc-bath-heater, the air-conditioner and the air-fresh service. After updating, you need to restart Home Assistant and check `xiaomi_home > CONFIGURE >
Update entity conversion rules > NEXT` to reload the integration.
这个版本修改了浴霸、空调、新风机的实体转换规则,更新之后需要重启 Home Assistant并且勾选 `xiaomi_home > 配置 > 更新实体转换规则 > 下一步` 重新加载集成。
### Added
- Add conversion rules for the air-conditioner service and the air-fresh service. [#879](https://github.com/XiaoMi/ha_xiaomi_home/pull/879)
### Changed
- Convert the mode of the ptc bath heater to the preset mode of the climate entity. [#874](https://github.com/XiaoMi/ha_xiaomi_home/pull/874)
- Use Home Assistant default icon when device_class is set. [#855](https://github.com/XiaoMi/ha_xiaomi_home/pull/855)
### Fixed
- Fix xiaomi.aircondition.m9 humidity-range unit. [#878](https://github.com/XiaoMi/ha_xiaomi_home/pull/878)
- Fix MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2. [#866](https://github.com/XiaoMi/ha_xiaomi_home/pull/866)
## v0.2.1
### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)
- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)
- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)
- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)
## v0.2.0
This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.
这个版本修改了一些传感器默认单位,更新后会导致 Home Assistant 弹出一些兼容性提示,您可以重新添加集成解决。
### Added
- Add prop trans rule for surge-power. [#595](https://github.com/XiaoMi/ha_xiaomi_home/pull/595)
- Support modify spec and value conversion. [#663](https://github.com/XiaoMi/ha_xiaomi_home/pull/663)
- Support the electric blanket. [#781](https://github.com/XiaoMi/ha_xiaomi_home/pull/781)
- Add device with motor-control service as cover entity. [#688](https://github.com/XiaoMi/ha_xiaomi_home/pull/688)
### Changed
- Update README file. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681) [#747](https://github.com/XiaoMi/ha_xiaomi_home/pull/747)
- Update CONTRIBUTING.md. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681)
- Refactor climate.py. [#614](https://github.com/XiaoMi/ha_xiaomi_home/pull/614)
### Fixed
- Fix variable name or comment errors & fix test_lan error. [#678](https://github.com/XiaoMi/ha_xiaomi_home/pull/678) [#690](https://github.com/XiaoMi/ha_xiaomi_home/pull/690)
- Fix water heater error & some type error. [#684](https://github.com/XiaoMi/ha_xiaomi_home/pull/684)
- Fix fan level with value-list & fan reverse [#689](https://github.com/XiaoMi/ha_xiaomi_home/pull/689)
- Fix sensor display precision [#708](https://github.com/XiaoMi/ha_xiaomi_home/pull/708)
- Fix event:motion-detected without arguments [#712](https://github.com/XiaoMi/ha_xiaomi_home/pull/712)
## v0.1.5b2
### Added
@ -91,10 +182,10 @@ This version will cause some Xiaomi routers that do not support access (#564) to
### Changed
### Fixed
- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
## v0.1.0
### Added

View File

@ -98,6 +98,9 @@ footer: Optional. The footer is the place to reference GitHub issues and PRs tha
When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
When you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.
## How to Get Help
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -565,27 +565,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
# i18n
tip_central = ''
group_id = home_info.get('group_id', None)
dev_list = {
device['did']: device
for device in list(self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get('did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
tip_central = ''
group_id = home_info.get('group_id', None)
dev_list = {
device['did']: device
for device in list(self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
self._cc_home_list_show = dict(sorted(home_list.items()))
@ -660,10 +665,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not home_selected:
return await self.__show_homes_select_form(
'no_family_selected')
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
if home_id in home_selected:
self._home_selected[home_id] = home_info
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in home_selected:
self._home_selected[home_id] = home_info
self._area_name_rule = user_input.get(
'area_name_rule', self._area_name_rule)
# Storage device list
@ -1420,27 +1429,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
# i18n
tip_central = ''
group_id = home_info.get('group_id', None)
did_list = {
device['did']: device for device in list(
self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
tip_central = ''
group_id = home_info.get('group_id', None)
did_list = {
device['did']: device for device in list(
self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
# Remove deleted item
self._home_selected_list = [
home_id for home_id in self._home_selected_list
@ -1460,10 +1473,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return await self.__show_homes_select_form('no_family_selected')
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
self._home_selected = {}
for home_id, home_info in self._cc_home_info[
'homes']['home_list'].items():
if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info
for device_source in ['home_list','share_home_list',
'separated_shared_list']:
if device_source not in self._cc_home_info['homes']:
continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info
# Get device list
device_list: dict = {
did: dev_info

View File

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

View File

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

View File

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

View File

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

View File

@ -96,7 +96,7 @@ class Light(MIoTServiceEntity, LightEntity):
"""Light entities for Xiaomi Home."""
# pylint: disable=unused-argument
_VALUE_RANGE_MODE_COUNT_MAX = 30
_prop_on: MIoTSpecProperty
_prop_on: Optional[MIoTSpecProperty]
_prop_brightness: Optional[MIoTSpecProperty]
_prop_color_temp: Optional[MIoTSpecProperty]
_prop_color: Optional[MIoTSpecProperty]
@ -179,7 +179,7 @@ class Light(MIoTServiceEntity, LightEntity):
) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX
):
_LOGGER.info(
_LOGGER.error(
'too many mode values, %s, %s, %s',
self.entity_id, prop.name, prop.value_range)
else:
@ -250,23 +250,25 @@ class Light(MIoTServiceEntity, LightEntity):
Shall set attributes in kwargs if applicable.
"""
result: bool = False
# on
# Dirty logic for lumi.gateway.mgl03 indicator light
value_on = True if self._prop_on.format_ == bool else 1
result = await self.set_property_async(
prop=self._prop_on, value=value_on)
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
await self.set_property_async(
prop=self._prop_on, value=value_on)
# brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
result = await self.set_property_async(
prop=self._prop_brightness, value=brightness)
await self.set_property_async(
prop=self._prop_brightness, value=brightness,
write_ha_state=False)
# color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
result = await self.set_property_async(
await self.set_property_async(
prop=self._prop_color_temp,
value=kwargs[ATTR_COLOR_TEMP_KELVIN])
value=kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False)
self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
@ -274,19 +276,23 @@ class Light(MIoTServiceEntity, LightEntity):
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
result = await self.set_property_async(
prop=self._prop_color, value=rgb)
await self.set_property_async(
prop=self._prop_color, value=rgb,
write_ha_state=False)
self._attr_color_mode = ColorMode.RGB
# mode
if ATTR_EFFECT in kwargs:
result = await self.set_property_async(
await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
return result
map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
write_ha_state=False)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off."""
if not self._prop_on:
return
# Dirty logic for lumi.gateway.mgl03 indicator light
value_on = False if self._prop_on.format_ == bool else 0
return await self.set_property_async(prop=self._prop_on, value=value_on)
await self.set_property_async(prop=self._prop_on, value=value_on)

View File

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

View File

@ -150,7 +150,7 @@ class MIoTClient:
# Device list update timestamp
_device_list_update_ts: int
_sub_source_list: dict[str, str]
_sub_source_list: dict[str, Optional[str]]
_sub_tree: MIoTMatcher
_sub_device_state: dict[str, MipsDeviceState]
@ -620,7 +620,7 @@ class MIoTClient:
# Priority local control
if self._ctrl_mode == CtrlMode.AUTO:
# Gateway control
device_gw: dict = self._device_list_gateway.get(did, None)
device_gw = self._device_list_gateway.get(did, None)
if (
device_gw and device_gw.get('online', False)
and device_gw.get('specv2_access', False)
@ -641,7 +641,7 @@ class MIoTClient:
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc))
# Lan control
device_lan: dict = self._device_list_lan.get(did, None)
device_lan = self._device_list_lan.get(did, None)
if device_lan and device_lan.get('online', False):
result = await self._miot_lan.set_prop_async(
did=did, siid=siid, piid=piid, value=value)
@ -657,7 +657,7 @@ class MIoTClient:
# Cloud control
device_cloud = self._device_list_cloud.get(did, None)
if device_cloud and device_cloud.get('online', False):
result: list = await self._http.set_prop_async(
result = await self._http.set_prop_async(
params=[
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
])
@ -746,7 +746,7 @@ class MIoTClient:
if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}')
device_gw: dict = self._device_list_gateway.get(did, None)
device_gw = self._device_list_gateway.get(did, None)
# Priority local control
if self._ctrl_mode == CtrlMode.AUTO:
if (
@ -782,7 +782,7 @@ class MIoTClient:
self.__get_exec_error_with_rc(rc=rc))
# Cloud control
device_cloud = self._device_list_cloud.get(did, None)
if device_cloud.get('online', False):
if device_cloud and device_cloud.get('online', False):
result: dict = await self._http.action_async(
did=did, siid=siid, aiid=aiid, in_list=in_list)
if result:
@ -798,14 +798,15 @@ class MIoTClient:
dids=[did]))
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc))
# Show error message
# TODO: Show error message
_LOGGER.error(
'client action failed, %s.%d.%d', did, siid, aiid)
return None
return []
def sub_prop(
self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, piid: int = None, handler_ctx: Any = None
siid: Optional[int] = None, piid: Optional[int] = None,
handler_ctx: Any = None
) -> bool:
if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}')
@ -818,7 +819,9 @@ class MIoTClient:
_LOGGER.debug('client sub prop, %s', topic)
return True
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
def unsub_prop(
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
) -> bool:
topic = (
f'{did}/p/'
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
@ -829,7 +832,8 @@ class MIoTClient:
def sub_event(
self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, eiid: int = None, handler_ctx: Any = None
siid: Optional[int] = None, eiid: Optional[int] = None,
handler_ctx: Any = None
) -> bool:
if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}')
@ -841,7 +845,9 @@ class MIoTClient:
_LOGGER.debug('client sub event, %s', topic)
return True
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
def unsub_event(
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
) -> bool:
topic = (
f'{did}/e/'
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
@ -873,16 +879,7 @@ class MIoTClient:
sub_from = self._sub_source_list.pop(did, None)
# Unsub
if sub_from:
if sub_from == 'cloud':
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif sub_from == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
self.__unsub_from(sub_from, did)
# Storage
await self._storage.save_async(
domain='miot_devices',
@ -930,6 +927,39 @@ class MIoTClient:
delay_sec, lambda: self._main_loop.create_task(
self.refresh_user_cert_async()))
@final
def __unsub_from(self, sub_from: str, did: str) -> None:
mips: Any = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
try:
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
except RuntimeError as e:
if 'Event loop is closed' in str(e):
# Ignore unsub exception when loop is closed
pass
else:
raise
@final
def __sub_from(self, sub_from: str, did: str) -> None:
mips = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
@final
def __update_device_msg_sub(self, did: str) -> None:
if did not in self._device_list_cache:
@ -961,27 +991,9 @@ class MIoTClient:
return
# Unsub old
if from_old:
if from_old == 'cloud':
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif from_old == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif from_old in self._mips_local:
mips = self._mips_local[from_old]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
self.__unsub_from(from_old, did)
# Sub new
if from_new == 'cloud':
self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg)
self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg)
elif from_new == 'lan':
self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg)
self._miot_lan.sub_event(did=did, handler=self.__on_event_msg)
elif from_new in self._mips_local:
mips = self._mips_local[from_new]
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
self.__sub_from(from_new, did)
self._sub_source_list[did] = from_new
_LOGGER.info(
'device sub changed, %s, from %s to %s', did, from_old, from_new)
@ -1081,7 +1093,7 @@ class MIoTClient:
if state_old == state_new:
continue
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify()
@ -1091,8 +1103,8 @@ class MIoTClient:
self, group_id: str, state: bool
) -> None:
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
mips: MipsLocalClient = self._mips_local.get(group_id, None)
if mips is None:
mips = self._mips_local.get(group_id, None)
if not mips:
_LOGGER.error(
'local mips state changed, mips not exist, %s', group_id)
return
@ -1124,7 +1136,7 @@ class MIoTClient:
if state_old == state_new:
continue
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify()
@ -1171,7 +1183,7 @@ class MIoTClient:
if state_old == state_new:
continue
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self._device_list_lan = {}
@ -1201,7 +1213,7 @@ class MIoTClient:
if state_old == state_new:
return
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(
did, MIoTDeviceState.ONLINE if state_new
@ -1257,7 +1269,7 @@ class MIoTClient:
if state_old == state_new:
return
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(
did, MIoTDeviceState.ONLINE if state_new
@ -1301,9 +1313,8 @@ class MIoTClient:
async def __load_cache_device_async(self) -> None:
"""Load device list from cache."""
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
domain='miot_devices',
name=f'{self._uid}_{self._cloud_server}',
type_=dict)
domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
type_=dict) # type: ignore
if not cache_list:
self.__show_client_error_notify(
message=self._i18n.translate(
@ -1346,7 +1357,7 @@ class MIoTClient:
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
did, {}).get('online', None)
cloud_state_new: Optional[bool] = None
device_new: dict = cloud_list.pop(did, None)
device_new = cloud_list.pop(did, None)
if device_new:
cloud_state_new = device_new.get('online', None)
# Update cache device info
@ -1371,7 +1382,7 @@ class MIoTClient:
continue
info['online'] = state_new
# Call device state changed callback
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(
did, MIoTDeviceState.ONLINE if state_new
@ -1426,8 +1437,7 @@ class MIoTClient:
self, dids: list[str]
) -> None:
_LOGGER.debug('refresh cloud device with dids, %s', dids)
cloud_list: dict[str, dict] = (
await self._http.get_devices_with_dids_async(dids=dids))
cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
if cloud_list is None:
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
return
@ -1466,11 +1476,11 @@ class MIoTClient:
for did, info in self._device_list_cache.items():
if did not in filter_dids:
continue
device_old: dict = self._device_list_gateway.get(did, None)
device_old = self._device_list_gateway.get(did, None)
gw_state_old = device_old.get(
'online', False) if device_old else False
gw_state_new: bool = False
device_new: dict = gw_list.pop(did, None)
device_new = gw_list.pop(did, None)
if device_new:
# Update gateway device info
self._device_list_gateway[did] = {
@ -1493,7 +1503,7 @@ class MIoTClient:
if state_old == state_new:
continue
info['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(
did, MIoTDeviceState.ONLINE if state_new
@ -1518,7 +1528,7 @@ class MIoTClient:
if state_old == state_new:
continue
self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None)
sub = self._sub_device_state.get(did, None)
if sub and sub.handler:
sub.handler(
did, MIoTDeviceState.ONLINE if state_new
@ -1533,7 +1543,7 @@ class MIoTClient:
'refresh gw devices with group_id, %s', group_id)
# Remove timer
self._mips_local_state_changed_timers.pop(group_id, None)
mips: MipsLocalClient = self._mips_local.get(group_id, None)
mips = self._mips_local.get(group_id, None)
if not mips:
_LOGGER.error('mips not exist, %s', group_id)
return
@ -1900,77 +1910,73 @@ async def get_miot_instance_async(
) -> MIoTClient:
if entry_id is None:
raise MIoTClientError('invalid entry_id')
miot_client: MIoTClient = None
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None):
miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
if miot_client:
_LOGGER.info('instance exist, %s', entry_id)
miot_client = a
else:
if entry_data is None:
raise MIoTClientError('entry data is None')
# Get running loop
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
if loop is None:
raise MIoTClientError('loop is None')
# MIoT storage
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
'miot_storage', None)
if not storage:
storage = MIoTStorage(
root_path=entry_data['storage_path'], loop=loop)
hass.data[DOMAIN]['miot_storage'] = storage
_LOGGER.info('create miot_storage instance')
global_config: dict = await storage.load_user_config_async(
uid='global_config', cloud_server='all',
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
# MIoT network
network_detect_addr: dict = global_config.get(
'network_detect_addr', {})
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
'miot_network', None)
if not network:
network = MIoTNetwork(
ip_addr_list=network_detect_addr.get('ip', []),
url_addr_list=network_detect_addr.get('url', []),
refresh_interval=NETWORK_REFRESH_INTERVAL,
loop=loop)
hass.data[DOMAIN]['miot_network'] = network
await network.init_async()
_LOGGER.info('create miot_network instance')
# MIoT service
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
'mips_service', None)
if not mips_service:
aiozc = await zeroconf.async_get_async_instance(hass)
mips_service = MipsService(aiozc=aiozc, loop=loop)
hass.data[DOMAIN]['mips_service'] = mips_service
await mips_service.init_async()
_LOGGER.info('create mips_service instance')
# MIoT lan
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get(
'miot_lan', None)
if not miot_lan:
miot_lan = MIoTLan(
net_ifs=global_config.get('net_interfaces', []),
network=network,
mips_service=mips_service,
enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop)
hass.data[DOMAIN]['miot_lan'] = miot_lan
_LOGGER.info('create miot_lan instance')
# MIoT client
miot_client = MIoTClient(
entry_id=entry_id,
entry_data=entry_data,
return miot_client
# Create new instance
if not entry_data:
raise MIoTClientError('entry data is None')
# Get running loop
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
if not loop:
raise MIoTClientError('loop is None')
# MIoT storage
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
'miot_storage', None)
if not storage:
storage = MIoTStorage(
root_path=entry_data['storage_path'], loop=loop)
hass.data[DOMAIN]['miot_storage'] = storage
_LOGGER.info('create miot_storage instance')
global_config: dict = await storage.load_user_config_async(
uid='global_config', cloud_server='all',
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
# MIoT network
network_detect_addr: dict = global_config.get('network_detect_addr', {})
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
'miot_network', None)
if not network:
network = MIoTNetwork(
ip_addr_list=network_detect_addr.get('ip', []),
url_addr_list=network_detect_addr.get('url', []),
refresh_interval=NETWORK_REFRESH_INTERVAL,
loop=loop)
hass.data[DOMAIN]['miot_network'] = network
await network.init_async()
_LOGGER.info('create miot_network instance')
# MIoT service
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
'mips_service', None)
if not mips_service:
aiozc = await zeroconf.async_get_async_instance(hass)
mips_service = MipsService(aiozc=aiozc, loop=loop)
hass.data[DOMAIN]['mips_service'] = mips_service
await mips_service.init_async()
_LOGGER.info('create mips_service instance')
# MIoT lan
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
if not miot_lan:
miot_lan = MIoTLan(
net_ifs=global_config.get('net_interfaces', []),
network=network,
storage=storage,
mips_service=mips_service,
miot_lan=miot_lan,
loop=loop
)
miot_client.persistent_notify = persistent_notify
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
_LOGGER.info(
'new miot_client instance, %s, %s', entry_id, entry_data)
await miot_client.init_async()
enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop)
hass.data[DOMAIN]['miot_lan'] = miot_lan
_LOGGER.info('create miot_lan instance')
# MIoT client
miot_client = MIoTClient(
entry_id=entry_id,
entry_data=entry_data,
network=network,
storage=storage,
mips_service=mips_service,
miot_lan=miot_lan,
loop=loop
)
miot_client.persistent_notify = persistent_notify
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
_LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)
await miot_client.init_async()
return miot_client

View File

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

View File

@ -56,6 +56,7 @@ from homeassistant.const import (
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
@ -72,6 +73,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfVolume,
UnitOfVolumeFlowRate,
UnitOfDataRate
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.components.switch import SwitchDeviceClass
@ -243,12 +245,12 @@ class MIoTDevice:
def sub_device_state(
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
) -> int:
self._sub_id += 1
sub_id = self.__gen_sub_id()
if key in self._device_state_sub_list:
self._device_state_sub_list[key][str(self._sub_id)] = handler
self._device_state_sub_list[key][str(sub_id)] = handler
else:
self._device_state_sub_list[key] = {str(self._sub_id): handler}
return self._sub_id
self._device_state_sub_list[key] = {str(sub_id): handler}
return sub_id
def unsub_device_state(self, key: str, sub_id: int) -> None:
sub_list = self._device_state_sub_list.get(key, None)
@ -266,14 +268,14 @@ class MIoTDevice:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
sub_id = self.__gen_sub_id()
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
self._value_sub_list[key][str(sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_prop(
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
return self._sub_id
return sub_id
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
key: str = f'p.{siid}.{piid}'
@ -294,14 +296,14 @@ class MIoTDevice:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
sub_id = self.__gen_sub_id()
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
self._value_sub_list[key][str(sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_event(
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
return self._sub_id
return sub_id
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
key: str = f'e.{siid}.{eiid}'
@ -343,10 +345,11 @@ class MIoTDevice:
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}')
def gen_service_entity_id(self, ha_domain: str, siid: int) -> str:
def gen_service_entity_id(self, ha_domain: str, siid: int,
description: str) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_s_{siid}')
f'{self._model_strs[-1][:20]}_s_{siid}_{description}')
def gen_prop_entity_id(
self, ha_domain: str, spec_name: str, siid: int, piid: int
@ -414,10 +417,12 @@ class MIoTDevice:
spec_name: str = spec_instance.name
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:
return None
# 1. The device shall have all required services.
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
if not {
service.name for service in spec_instance.services
service.name for service in spec_instance.services
}.issuperset(required_services):
return None
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
@ -427,9 +432,13 @@ class MIoTDevice:
for service in spec_instance.services:
if service.platform:
continue
required_properties: dict
optional_properties: dict
required_actions: set
optional_actions: set
# 2. The service shall have all required properties, actions.
if service.name in required_services:
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
'required'].get(
service.name, {}
).get('required', {}).get('properties', {})
@ -446,7 +455,7 @@ class MIoTDevice:
service.name, {}
).get('optional', {}).get('actions', set({}))
elif service.name in optional_services:
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
'optional'].get(
service.name, {}
).get('required', {}).get('properties', {})
@ -484,7 +493,7 @@ class MIoTDevice:
set(required_properties.keys()), optional_properties):
if prop.unit:
prop.external_unit = self.unit_convert(prop.unit)
prop.icon = self.icon_convert(prop.unit)
# prop.icon = self.icon_convert(prop.unit)
prop.platform = platform
entity_data.props.add(prop)
# action
@ -499,85 +508,94 @@ class MIoTDevice:
return entity_data
def parse_miot_service_entity(
self, service_instance: MIoTSpecService
self, miot_service: MIoTSpecService
) -> Optional[MIoTEntityData]:
service = service_instance
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP):
if (
miot_service.platform
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
):
return None
service_name = service.name
service_name = miot_service.name
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
service_name = SPEC_SERVICE_TRANS_MAP[service_name]
# 1. The service shall have all required properties.
if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:
return None
# Required properties, required access mode
required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
'required'].get('properties', {})
if not {
prop.name for prop in service.properties if prop.access
prop.name for prop in miot_service.properties if prop.access
}.issuperset(set(required_properties.keys())):
return None
# 2. The required property shall have all required access mode.
for prop in service.properties:
for prop in miot_service.properties:
if prop.name in required_properties:
if not set(prop.access).issuperset(
required_properties[prop.name]):
return None
# Required actions
# Required events
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
entity_data = MIoTEntityData(platform=platform, spec=service_instance)
entity_data = MIoTEntityData(platform=platform, spec=miot_service)
# Optional properties
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][
'optional'].get('properties', set({}))
for prop in service.properties:
for prop in miot_service.properties:
if prop.name in set.union(
set(required_properties.keys()), optional_properties):
if prop.unit:
prop.external_unit = self.unit_convert(prop.unit)
prop.icon = self.icon_convert(prop.unit)
# prop.icon = self.icon_convert(prop.unit)
prop.platform = platform
entity_data.props.add(prop)
# action
# event
# No actions or events is in SPEC_SERVICE_TRANS_MAP now.
service.platform = platform
# Optional actions
# Optional events
miot_service.platform = platform
# entity_category
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
'entity_category', None):
miot_service.entity_category = entity_category
return entity_data
def parse_miot_property_entity(
self, property_instance: MIoTSpecProperty
) -> Optional[dict[str, str]]:
prop = property_instance
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
if (
prop.platform
or (prop.name not in SPEC_PROP_TRANS_MAP['properties'])
miot_prop.platform
or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
):
return None
prop_name = prop.name
return False
prop_name = miot_prop.name
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
# Check
prop_access: set = set({})
if prop.readable:
if miot_prop.readable:
prop_access.add('read')
if prop.writable:
if miot_prop.writable:
prop_access.add('write')
if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']):
return None
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
return False
if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']:
return None
if prop.unit:
prop.external_unit = self.unit_convert(prop.unit)
prop.icon = self.icon_convert(prop.unit)
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
return False
miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
'device_class']
result = {'platform': platform, 'device_class': device_class}
# optional:
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional']
if 'state_class' in optional:
result['state_class'] = optional['state_class']
if not prop.unit and 'unit_of_measurement' in optional:
result['unit_of_measurement'] = optional['unit_of_measurement']
return result
# Optional params
if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
prop_name]['state_class']
if (
not miot_prop.external_unit
and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
prop_name]
):
# Priority: spec_modify.unit > unit_convert > specv2entity.unit
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
prop_name]['unit_of_measurement']
# Priority: default.icon when device_class is set > spec_modify.icon
# > icon_convert
miot_prop.platform = platform
return True
def spec_transform(self) -> None:
"""Parse service, property, event, action from device spec."""
@ -589,7 +607,7 @@ class MIoTDevice:
# STEP 2: service conversion
for service in self.spec_instance.services:
service_entity = self.parse_miot_service_entity(
service_instance=service)
miot_service=service)
if service_entity:
self.append_entity(entity_data=service_entity)
# STEP 3.1: property conversion
@ -598,20 +616,11 @@ class MIoTDevice:
continue
if prop.unit:
prop.external_unit = self.unit_convert(prop.unit)
prop.icon = self.icon_convert(prop.unit)
prop_entity = self.parse_miot_property_entity(
property_instance=prop)
if prop_entity:
prop.platform = prop_entity['platform']
prop.device_class = prop_entity['device_class']
if 'state_class' in prop_entity:
prop.state_class = prop_entity['state_class']
if 'unit_of_measurement' in prop_entity:
prop.external_unit = self.unit_convert(
prop_entity['unit_of_measurement'])
prop.icon = self.icon_convert(
prop_entity['unit_of_measurement'])
# general conversion
if not prop.icon:
prop.icon = self.icon_convert(prop.unit)
# Special conversion
self.parse_miot_property_entity(miot_prop=prop)
# General conversion
if not prop.platform:
if prop.writable:
if prop.format_ == str:
@ -625,7 +634,7 @@ class MIoTDevice:
prop.platform = 'number'
else:
# Irregular property will not be transformed.
pass
continue
elif prop.readable or prop.notifiable:
if prop.format_ == bool:
prop.platform = 'binary_sensor'
@ -653,11 +662,66 @@ class MIoTDevice:
self.append_action(action=action)
def unit_convert(self, spec_unit: str) -> Optional[str]:
"""Convert MIoT unit to Home Assistant unit."""
"""Convert MIoT unit to Home Assistant unit.
25/01/20: All online prop unit statistical tables: unit, quantity.
{
"no_unit": 148499,
"percentage": 10042,
"kelvin": 1895,
"rgb": 772, // color
"celsius": 5762,
"none": 16106,
"hours": 1540,
"minutes": 5061,
"ms": 27,
"watt": 216,
"arcdegrees": 159,
"ppm": 177,
"μg/m3": 106,
"days": 571,
"seconds": 2749,
"B/s": 21,
"pascal": 110,
"mg/m3": 339,
"lux": 125,
"kWh": 124,
"mv": 2,
"V": 38,
"A": 29,
"mV": 4,
"L": 352,
"m": 37,
"毫摩尔每升": 2, // blood-sugar, cholesterol
"mmol/L": 1, // urea
"weeks": 26,
"meter": 3,
"dB": 26,
"hour": 14,
"calorie": 19, // 1 cal = 4.184 J
"ppb": 3,
"arcdegress": 30,
"bpm": 4, // realtime-heartrate
"gram": 7,
"km/h": 9,
"W": 1,
"m3/h": 2,
"kilopascal": 1,
"mL": 4,
"mmHg": 4,
"w": 1,
"liter": 1,
"cm": 3,
"mA": 2,
"kilogram": 2,
"kcal/d": 2, // basal-metabolism
"times": 1 // exercise-count
}
"""
unit_map = {
'percentage': PERCENTAGE,
'weeks': UnitOfTime.WEEKS,
'days': UnitOfTime.DAYS,
'hour': UnitOfTime.HOURS,
'hours': UnitOfTime.HOURS,
'minutes': UnitOfTime.MINUTES,
'seconds': UnitOfTime.SECONDS,
@ -672,90 +736,117 @@ class MIoTDevice:
'ppb': CONCENTRATION_PARTS_PER_BILLION,
'lux': LIGHT_LUX,
'pascal': UnitOfPressure.PA,
'kilopascal': UnitOfPressure.KPA,
'mmHg': UnitOfPressure.MMHG,
'bar': UnitOfPressure.BAR,
'watt': UnitOfPower.WATT,
'L': UnitOfVolume.LITERS,
'liter': UnitOfVolume.LITERS,
'mL': UnitOfVolume.MILLILITERS,
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
'm/s': UnitOfSpeed.METERS_PER_SECOND,
'watt': UnitOfPower.WATT,
'w': UnitOfPower.WATT,
'W': UnitOfPower.WATT,
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
'A': UnitOfElectricCurrent.AMPERE,
'mA': UnitOfElectricCurrent.MILLIAMPERE,
'V': UnitOfElectricPotential.VOLT,
'mv': UnitOfElectricPotential.MILLIVOLT,
'mV': UnitOfElectricPotential.MILLIVOLT,
'cm': UnitOfLength.CENTIMETERS,
'm': UnitOfLength.METERS,
'meter': UnitOfLength.METERS,
'km': UnitOfLength.KILOMETERS,
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
'gram': UnitOfMass.GRAMS,
'kilogram': UnitOfMass.KILOGRAMS,
'dB': SIGNAL_STRENGTH_DECIBELS,
'arcdegrees': DEGREE,
'arcdegress': DEGREE,
'kB': UnitOfInformation.KILOBYTES,
'MB': UnitOfInformation.MEGABYTES,
'GB': UnitOfInformation.GIGABYTES,
'TB': UnitOfInformation.TERABYTES,
'B/s': UnitOfDataRate.BYTES_PER_SECOND,
'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,
'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,
'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND
}
# Handle UnitOfConductivity separately since
# it might not be available in all HA versions
try:
# pylint: disable=import-outside-toplevel
from homeassistant.const import UnitOfConductivity
from homeassistant.const import UnitOfConductivity # type: ignore
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
except Exception: # pylint: disable=broad-except
unit_map['μS/cm'] = 'μS/cm'
unit_map['mWh'] = 'mWh'
return unit_map.get(spec_unit, None)
def icon_convert(self, spec_unit: str) -> Optional[str]:
if spec_unit in ['percentage']:
if spec_unit in {'percentage'}:
return 'mdi:percent'
if spec_unit in [
'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']:
if spec_unit in {
'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
}:
return 'mdi:clock'
if spec_unit in ['celsius']:
if spec_unit in {'celsius'}:
return 'mdi:temperature-celsius'
if spec_unit in ['fahrenheit']:
if spec_unit in {'fahrenheit'}:
return 'mdi:temperature-fahrenheit'
if spec_unit in ['kelvin']:
if spec_unit in {'kelvin'}:
return 'mdi:temperature-kelvin'
if spec_unit in ['μg/m3', 'mg/m3', 'ppm', 'ppb']:
if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:
return 'mdi:blur'
if spec_unit in ['lux']:
if spec_unit in {'lux'}:
return 'mdi:brightness-6'
if spec_unit in ['pascal', 'megapascal', 'bar']:
if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
return 'mdi:gauge'
if spec_unit in ['watt']:
if spec_unit in {'watt', 'w', 'W'}:
return 'mdi:flash-triangle'
if spec_unit in ['L', 'mL']:
if spec_unit in {'L', 'mL'}:
return 'mdi:gas-cylinder'
if spec_unit in ['km/h', 'm/s']:
if spec_unit in {'km/h', 'm/s'}:
return 'mdi:speedometer'
if spec_unit in ['kWh']:
if spec_unit in {'kWh'}:
return 'mdi:transmission-tower'
if spec_unit in ['A', 'mA']:
if spec_unit in {'A', 'mA'}:
return 'mdi:current-ac'
if spec_unit in ['V', 'mV']:
if spec_unit in {'V', 'mv', 'mV'}:
return 'mdi:current-dc'
if spec_unit in ['m', 'km']:
if spec_unit in {'cm', 'm', 'meter', 'km'}:
return 'mdi:ruler'
if spec_unit in ['rgb']:
if spec_unit in {'rgb'}:
return 'mdi:palette'
if spec_unit in ['m3/h', 'L/s']:
if spec_unit in {'m3/h', 'L/s'}:
return 'mdi:pipe-leak'
if spec_unit in ['μS/cm']:
if spec_unit in {'μS/cm'}:
return 'mdi:resistor-nodes'
if spec_unit in ['gram']:
if spec_unit in {'gram', 'kilogram'}:
return 'mdi:weight'
if spec_unit in ['dB']:
if spec_unit in {'dB'}:
return 'mdi:signal-distance-variant'
if spec_unit in ['times']:
if spec_unit in {'times'}:
return 'mdi:counter'
if spec_unit in ['mmol/L']:
if spec_unit in {'mmol/L'}:
return 'mdi:dots-hexagon'
if spec_unit in ['arcdegress']:
return 'mdi:angle-obtuse'
if spec_unit in ['kB']:
if spec_unit in {'kB', 'MB', 'GB'}:
return 'mdi:network-pos'
if spec_unit in ['calorie', 'kCal']:
if spec_unit in {'arcdegress', 'arcdegrees'}:
return 'mdi:angle-obtuse'
if spec_unit in {'B/s', 'KB/s', 'MB/s', 'GB/s'}:
return 'mdi:network'
if spec_unit in {'calorie', 'kCal'}:
return 'mdi:food'
return None
def __gen_sub_id(self) -> int:
self._sub_id += 1
return self._sub_id
def __on_device_state_changed(
self, did: str, state: MIoTDeviceState, ctx: Any
) -> None:
@ -806,10 +897,12 @@ class MIoTServiceEntity(Entity):
self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id(
DOMAIN, siid=self.entity_data.spec.iid)
DOMAIN, siid=self.entity_data.spec.iid,
description=self.entity_data.spec.description)
self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}')
self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr
self._attr_unique_id = self.entity_id
self._attr_should_poll = False
@ -903,14 +996,14 @@ class MIoTServiceEntity(Entity):
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
def get_map_value(
self, map_: dict[int, Any], key: int
self, map_: Optional[dict[int, Any]], key: int
) -> Any:
if map_ is None:
return None
return map_.get(key, None)
def get_map_key(
self, map_: dict[int, Any], value: Any
self, map_: Optional[dict[int, Any]], value: Any
) -> Optional[int]:
if map_ is None:
return None
@ -919,7 +1012,7 @@ class MIoTServiceEntity(Entity):
return key
return None
def get_prop_value(self, prop: MIoTSpecProperty) -> Any:
def get_prop_value(self, prop: Optional[MIoTSpecProperty]) -> Any:
if not prop:
_LOGGER.error(
'get_prop_value error, property is None, %s, %s',
@ -927,7 +1020,9 @@ class MIoTServiceEntity(Entity):
return None
return self._prop_value_map.get(prop, None)
def set_prop_value(self, prop: MIoTSpecProperty, value: Any) -> None:
def set_prop_value(
self, prop: Optional[MIoTSpecProperty], value: Any
) -> None:
if not prop:
_LOGGER.error(
'set_prop_value error, property is None, %s, %s',
@ -936,13 +1031,14 @@ class MIoTServiceEntity(Entity):
self._prop_value_map[prop] = value
async def set_property_async(
self, prop: MIoTSpecProperty, value: Any, update: bool = True
self, prop: Optional[MIoTSpecProperty], value: Any,
update_value: bool = True, write_ha_state: bool = True
) -> bool:
value = prop.value_format(value)
if not prop:
raise RuntimeError(
f'set property failed, property is None, '
f'{self.entity_id}, {self.name}')
value = prop.value_format(value)
if prop not in self.entity_data.props:
raise RuntimeError(
f'set property failed, unknown property, '
@ -958,8 +1054,9 @@ class MIoTServiceEntity(Entity):
except MIoTClientError as e:
raise RuntimeError(
f'{e}, {self.entity_id}, {self.name}, {prop.name}') from e
if update:
if update_value:
self._prop_value_map[prop] = value
if write_ha_state:
self.async_write_ha_state()
return True
@ -1184,6 +1281,7 @@ class MIoTPropertyEntity(Entity):
def __on_value_changed(self, params: dict, ctx: Any) -> None:
_LOGGER.debug('property changed, %s', params)
self._value = self.spec.value_format(params['value'])
self._value = self.spec.eval_expr(self._value)
if not self._pending_write_ha_state_timer:
self.async_write_ha_state()

View File

@ -1213,10 +1213,13 @@ class MipsLocalClient(_MipsClient):
or 'did' not in msg
or 'siid' not in msg
or 'eiid' not in msg
or 'arguments' not in msg
# or 'arguments' not in msg
):
# self.log_error(f'on_event_msg, recv unknown msg, {payload}')
self.log_info('unknown event msg, %s', payload)
return
if 'arguments' not in msg:
self.log_info('wrong event msg, %s', payload)
msg['arguments'] = []
if handler:
self.log_debug('local, on event_occurred, %s', payload)
handler(msg, ctx)
@ -1414,7 +1417,7 @@ class MipsLocalClient(_MipsClient):
@final
@on_dev_list_changed.setter
def on_dev_list_changed(
self, func: Callable[[Any, list[str]], Coroutine]
self, func: Optional[Callable[[Any, list[str]], Coroutine]]
) -> None:
"""run in main loop."""
self._on_dev_list_changed = func

View File

@ -94,7 +94,7 @@ class MIoTNetwork:
_main_loop: asyncio.AbstractEventLoop
_ip_addr_map: dict[str, float]
_url_addr_list: dict[str, float]
_http_addr_map: dict[str, float]
_http_session: aiohttp.ClientSession
_refresh_interval: int
@ -283,8 +283,8 @@ class MIoTNetwork:
[
'ping', '-c', '1', '-w',
str(self._DETECT_TIMEOUT), address]),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
await process.communicate()
if process.returncode == 0:

File diff suppressed because it is too large Load Diff

View File

@ -59,43 +59,6 @@ data:
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
translate:
contact_state:
de:
'false': Kein Kontakt
'true': Kontakt
en:
'false': No Contact
'true': Contact
es:
'false': Sin contacto
'true': Contacto
fr:
'false': Pas de contact
'true': Contact
it:
'false': Nessun contatto
'true': Contatto
ja:
'false': 非接触
'true': 接触
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Нет контакта
'true': Контакт
zh-Hans:
'false': 分离
'true': 接触
zh-Hant:
'false': 分離
'true': 接觸
default:
de:
'false': Falsch
@ -133,6 +96,43 @@ translate:
zh-Hant:
'false':
'true':
contact_state:
de:
'false': Kein Kontakt
'true': Kontakt
en:
'false': No Contact
'true': Contact
es:
'false': Sin contacto
'true': Contacto
fr:
'false': Pas de contact
'true': Contact
it:
'false': Nessun contatto
'true': Contatto
ja:
'false': 非接触
'true': 接触
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Нет контакта
'true': Контакт
zh-Hans:
'false': 分离
'true': 接触
zh-Hant:
'false': 分離
'true': 接觸
motion_state:
de:
'false': Keine Bewegung erkannt

View File

@ -0,0 +1,187 @@
{
"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: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: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:plant-monitor:0000A030:hhcc-v1": {
"en": {
"service:002:property:001": "Soil Moisture"
},
"zh-Hans": {
"service:002:property:001": "土壤湿度",
"service:002:property:003": "光照强度"
}
}
}

View File

@ -0,0 +1,22 @@
{
"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"
]
}
]
}
]
}

View File

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

View File

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

View File

@ -48,12 +48,19 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX,
UnitOfEnergy,
UnitOfPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfTemperature,
UnitOfPressure,
PERCENTAGE
)
# pylint: disable=pointless-string-statement
@ -96,7 +103,7 @@ from homeassistant.const import (
}
}
"""
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
SPEC_DEVICE_TRANS_MAP: dict = {
'humidifier': {
'required': {
'humidifier': {
@ -132,7 +139,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'optional': {
'properties': {'mode', 'target-humidity'}
}
},
}
},
'optional': {
'environment': {
@ -158,8 +165,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'continue-sweep',
'stop-and-gocharge'
}
},
}
}
},
'optional': {
@ -172,9 +178,9 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'required': {
'properties': {
'battery-level': {'read'}
},
}
}
},
}
},
'entity': 'vacuum'
},
@ -190,7 +196,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
},
'optional': {
'properties': {'target-humidity'}
},
}
}
},
'optional': {
@ -219,6 +225,31 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'entity': 'air-conditioner'
},
'air-condition-outlet': 'air-conditioner',
'thermostat': {
'required': {
'thermostat': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'target-temperature', 'mode', 'fan-level',
'temperature'}
}
}
},
'optional': {
'environment': {
'required': {},
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
}
},
'entity': 'thermostat'
},
'heater': {
'required': {
'heater': {
@ -229,7 +260,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
},
'optional': {
'properties': {'target-temperature', 'heat-level'}
},
}
}
},
'optional': {
@ -238,10 +269,58 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
}
},
'entity': 'heater'
}
},
'bath-heater': {
'required': {
'ptc-bath-heater': {
'required': {
'properties': {
'mode':{'read', 'write'}
}
},
'optional': {
'properties': {'target-temperature', 'temperature'}
}
}
},
'optional': {
'fan-control': {
'required': {},
'optional': {
'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
}
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature'}
}
}
},
'entity': 'bath-heater',
},
'electric-blanket': {
'required': {
'electric-blanket': {
'required': {
'properties': {
'on': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'temperature'}
}
}
},
'optional': {},
'entity': 'electric-blanket'
},
}
"""SPEC_SERVICE_TRANS_MAP
@ -259,11 +338,12 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'entity': str
'entity': str,
'entity_category'?: str
}
}
"""
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
SPEC_SERVICE_TRANS_MAP: dict = {
'light': {
'required': {
'properties': {
@ -277,10 +357,23 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
},
'entity': 'light'
},
'indicator-light': 'light',
'ambient-light': 'light',
'night-light': 'light',
'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': {
'required': {
'properties': {
@ -295,6 +388,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
},
'fan-control': 'fan',
'ceiling-fan': 'fan',
'air-fresh': 'fan',
'air-purifier': 'fan',
'water-heater': {
'required': {
'properties': {
@ -314,12 +409,27 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
},
'optional': {
'properties': {
'motor-control', 'status', 'current-position', 'target-position'
'status', 'current-position', 'target-position'
}
},
'entity': 'cover'
},
'window-opener': 'curtain'
'window-opener': 'curtain',
'motor-controller': 'curtain',
'airer': 'curtain',
'air-conditioner': {
'required': {
'properties': {
'on': {'read', 'write'},
'mode': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'target-humidity'}
},
'entity': 'air-conditioner'
}
}
"""SPEC_PROP_TRANS_MAP
@ -334,121 +444,145 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
'<property instance name>':{
'device_class': str,
'entity': str,
'optional':{
'state_class': str,
'unit_of_measurement': str
}
'state_class'?: str,
'unit_of_measurement'?: str
}
}
}
"""
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
SPEC_PROP_TRANS_MAP: dict = {
'entities': {
'sensor': {
'format': {'int', 'float'},
'access': {'read'}
},
'binary_sensor': {
'format': {'bool', 'int'},
'access': {'read'}
},
'switch': {
'format': {'bool'},
'access': {'read', 'write'}
}
},
'properties': {
'submersion-state': {
'device_class': BinarySensorDeviceClass.MOISTURE,
'entity': 'binary_sensor'
},
'contact-state': {
'device_class': BinarySensorDeviceClass.DOOR,
'entity': 'binary_sensor'
},
'occupancy-status': {
'device_class': BinarySensorDeviceClass.OCCUPANCY,
'entity': 'binary_sensor',
},
'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfTemperature.CELSIUS
},
'relative-humidity': {
'device_class': SensorDeviceClass.HUMIDITY,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
},
'air-quality-index': {
'device_class': SensorDeviceClass.AQI,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
},
'pm2.5-density': {
'device_class': SensorDeviceClass.PM25,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
},
'pm10-density': {
'device_class': SensorDeviceClass.PM10,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
},
'pm1': {
'device_class': SensorDeviceClass.PM1,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
},
'atmospheric-pressure': {
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPressure.PA
},
'tvoc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
},
'voc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
},
'voc-density': 'tvoc-density',
'battery-level': {
'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
},
'voltage': {
'device_class': SensorDeviceClass.VOLTAGE,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricPotential.VOLT
}
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricPotential.VOLT
},
'electric-current': {
'device_class': SensorDeviceClass.CURRENT,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
},
'illumination': {
'device_class': SensorDeviceClass.ILLUMINANCE,
'entity': 'sensor'
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': LIGHT_LUX
},
'no-one-determine-time': {
'device_class': SensorDeviceClass.DURATION,
'entity': 'sensor'
},
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time',
'electric-power': {
'device_class': SensorDeviceClass.POWER,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT
}
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT
},
'electric-current': {
'device_class': SensorDeviceClass.CURRENT,
'surge-power': {
'device_class': SensorDeviceClass.POWER,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
}
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT
},
'power-consumption': {
'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
}
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
},
'power': {
'device_class': SensorDeviceClass.POWER,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT
}
},
'total-battery': {
'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
}
},
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time'
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPower.WATT
}
}
}

View File

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

View File

@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
# Set device_class
if self._value_list:
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_icon = 'mdi:message-text'
self._attr_icon = 'mdi:format-text'
self._attr_native_unit_of_measurement = None
self._attr_options = self._value_list.descriptions
else:
@ -109,11 +109,14 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_device_class, None) # type: ignore
self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None
# Set suggested precision
if spec.format_ in {int, float} and spec.expr is None:
self._attr_suggested_display_precision = spec.precision
# Set state_class
if spec.state_class:
self._attr_state_class = spec.state_class
# Set icon
if spec.icon:
if spec.icon and not self.device_class:
self._attr_icon = spec.icon
@property

View File

@ -100,7 +100,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
) -> None:
"""Initialize the Water heater."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_temperature_unit = None
self._attr_temperature_unit = None # type: ignore
self._attr_supported_features = WaterHeaterEntityFeature(0)
self._prop_on = None
self._prop_temp = None
@ -112,20 +112,20 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
for prop in entity_data.props:
# on
if prop.name == 'on':
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
self._prop_on = prop
# temperature
if prop.name == 'temperature':
if prop.value_range:
if (
self._attr_temperature_unit is None
and prop.external_unit
):
self._attr_temperature_unit = prop.external_unit
self._prop_temp = prop
else:
if not prop.value_range:
_LOGGER.error(
'invalid temperature value_range format, %s',
self.entity_id)
continue
if prop.external_unit:
self._attr_temperature_unit = prop.external_unit
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._prop_temp = prop
# target-temperature
if prop.name == 'target-temperature':
if not prop.value_range:
@ -133,8 +133,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
'invalid target-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._attr_target_temperature_low = prop.value_range.min_
self._attr_target_temperature_high = prop.value_range.max_
self._attr_precision = prop.value_range.step
if self._attr_temperature_unit is None and prop.external_unit:
self._attr_temperature_unit = prop.external_unit
@ -166,6 +166,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature the water heater should heat water to."""
if not self._prop_target_temp:
return
await self.set_property_async(
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
@ -181,16 +183,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return
if self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async(
prop=self._prop_on, value=True, update=False)
prop=self._prop_on, value=True, write_ha_state=False)
await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map,
value=operation_mode))
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)
map_=self._mode_map, value=operation_mode))
@property
def current_temperature(self) -> Optional[float]:
@ -200,6 +197,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
@property
def target_temperature(self) -> Optional[float]:
"""Return the target temperature."""
if not self._prop_target_temp:
return None
return self.get_prop_value(prop=self._prop_target_temp)
@property

View File

@ -98,6 +98,8 @@ footer :(可选)关联的 issue 或 pull request 编号。
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
当您第一次提交拉取请求时GitHub Action 会提示您签署贡献者许可协议Contributor License AgreementCLA。只有签署了 CLA ,本项目才会合入您的拉取请求。
## 获取帮助
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。

View File

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

View File

@ -15,11 +15,13 @@ TRANS_RELATIVE_PATH: str = path.join(
MIOT_I18N_RELATIVE_PATH: str = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
SPEC_BOOL_TRANS_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
SPEC_FILTER_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
SPEC_ADD_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_add.json')
SPEC_MODIFY_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
def load_json_file(file_path: str) -> Optional[dict]:
@ -27,7 +29,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)
except FileNotFoundError:
_LOGGER.info('%s is not found.', file_path,)
_LOGGER.info('%s is not found.', file_path)
return None
except json.JSONDecodeError:
_LOGGER.info('%s is not a valid JSON file.', file_path)
@ -36,7 +38,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
def save_json_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
json.dump(data, file, ensure_ascii=False, indent=2)
def load_yaml_file(file_path: str) -> Optional[dict]:
@ -53,8 +55,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
def save_yaml_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file:
yaml.safe_dump(
data, file, default_flow_style=False, allow_unicode=True, indent=2)
yaml.safe_dump(data,
file,
default_flow_style=False,
allow_unicode=True,
indent=2,
sort_keys=False)
def dict_str_str(d: dict) -> bool:
@ -128,37 +134,148 @@ def bool_trans(d: dict) -> bool:
for key, trans in d['translate'].items():
trans_keys: set[str] = set(trans.keys())
if set(trans.keys()) != default_keys:
_LOGGER.info(
'bool trans inconsistent, %s, %s, %s',
key, default_keys, trans_keys)
_LOGGER.info('bool trans inconsistent, %s, %s, %s', key,
default_keys, trans_keys)
return False
return True
def spec_add(data: dict) -> bool:
"""dict[str, list[dict[str, int| str | list]]]"""
if not isinstance(data, dict):
return False
for urn, content in data.items():
if not isinstance(urn, str) or not isinstance(content, (list, str)):
return False
if isinstance(content, str):
continue
for service in content:
if ('iid' not in service) or ('type' not in service) or (
'description'
not in service) or (('properties' not in service) and
('actions' not in service) and
('events' not in service)):
return False
type_strs: list[str] = service['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
if 'properties' in service:
if not isinstance(service['properties'], list):
return False
for prop in service['properties']:
if ('iid' not in prop) or ('type' not in prop) or (
'description' not in prop) or (
'format' not in prop) or ('access' not in prop):
return False
if not isinstance(prop['iid'], int) or not isinstance(
prop['type'], str) or not isinstance(
prop['description'], str) or not isinstance(
prop['format'], str) or not isinstance(
prop['access'], list):
return False
type_strs = prop['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for access in prop['access']:
if access not in ['read', 'write', 'notify']:
return False
if 'value-range' in prop:
if not isinstance(prop['value-range'], list):
return False
for value in prop['value-range']:
if not isinstance(value, (int, float)):
return False
if 'value-list' in prop:
if not isinstance(prop['value-list'], list):
return False
for item in prop['value-list']:
if 'value' not in item or 'description' not in item:
return False
if not isinstance(item['value'],
int) or not isinstance(
item['description'], str):
return False
if 'actions' in service:
if not isinstance(service['actions'], list):
return False
for action in service['actions']:
if ('iid' not in action) or ('type' not in action) or (
'description' not in action) or (
'in' not in action) or ('out' not in action):
return False
if not isinstance(action['iid'], int) or not isinstance(
action['type'], str) or not isinstance(
action['description'], str) or not isinstance(
action['in'], list) or not isinstance(
action['out'], list):
return False
type_strs = action['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in action['in']:
if not isinstance(param, int):
return False
for param in action['out']:
if not isinstance(param, int):
return False
if 'events' in service:
if not isinstance(service['events'], list):
return False
for event in service['events']:
if ('iid' not in event) or ('type' not in event) or (
'description' not in event) or ('arguments'
not in event):
return False
if not isinstance(event['iid'], int) or not isinstance(
event['type'], str) or not isinstance(
event['description'], str) or not isinstance(
event['arguments'], list):
return False
type_strs = event['type'].split(':')
if type_strs[1] != 'miot-spec-v2':
return False
for param in event['arguments']:
if not isinstance(param, int):
return False
return True
def spec_modify(data: dict) -> bool:
"""dict[str, str | dict[str, dict]]"""
if not isinstance(data, dict):
return False
for urn, content in data.items():
if not isinstance(urn, str) or not isinstance(content, (dict, str)):
return False
if isinstance(content, str):
continue
for key, value in content.items():
if not isinstance(key, str) or not isinstance(value, dict):
return False
return True
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
_LOGGER.info('invalid type')
return False
if dict1.keys() != dict2.keys():
_LOGGER.info(
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys())
_LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),
dict2.keys())
return False
for key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
if not compare_dict_structure(dict1[key], dict2[key]):
_LOGGER.info(
'inconsistent key values, dict, %s', key)
_LOGGER.info('inconsistent key values, dict, %s', key)
return False
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if not all(
isinstance(i, type(j))
for i, j in zip(dict1[key], dict2[key])):
_LOGGER.info(
'inconsistent key values, list, %s', key)
_LOGGER.info('inconsistent key values, list, %s', key)
return False
elif not isinstance(dict1[key], type(dict2[key])):
_LOGGER.info(
'inconsistent key values, type, %s', key)
_LOGGER.info('inconsistent key values, type, %s', key)
return False
return True
@ -181,6 +298,18 @@ def sort_spec_filter(file_path: str):
return filter_data
def sort_spec_add(file_path: str):
filter_data = load_json_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
return dict(sorted(filter_data.items()))
def sort_spec_modify(file_path: str):
filter_data = load_yaml_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
return dict(sorted(filter_data.items()))
@pytest.mark.github
def test_bool_trans():
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
@ -197,6 +326,22 @@ def test_spec_filter():
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_spec_add():
data = load_json_file(SPEC_ADD_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_ADD_FILE} failed'
assert spec_add(data), f'{SPEC_ADD_FILE} format error'
@pytest.mark.github
def test_spec_modify():
data = load_yaml_file(SPEC_MODIFY_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_MODIFY_FILE} failed'
assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'
@pytest.mark.github
def test_miot_i18n():
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
@ -222,7 +367,8 @@ def test_miot_lang_integrity():
# pylint: disable=import-outside-toplevel
from miot.const import INTEGRATION_LANGUAGES
integration_lang_list: list[str] = [
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())]
f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())
]
translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))
assert len(translations_names) == len(integration_lang_list)
assert translations_names == set(integration_lang_list)
@ -238,21 +384,18 @@ def test_miot_lang_integrity():
default_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, name))
compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info(
'compare_dict_structure failed /translations, %s', name)
_LOGGER.info('compare_dict_structure failed /translations, %s',
name)
assert False
# Check i18n files structure
default_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]:
compare_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, name))
compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info(
'compare_dict_structure failed /miot/i18n, %s', name)
_LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)
assert False
@ -270,12 +413,21 @@ def test_miot_data_sort():
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(
load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(
sort_spec_add(file_path=SPEC_ADD_FILE)), (
f'{SPEC_ADD_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_yaml_file(file_path=SPEC_MODIFY_FILE)) == json.dumps(
sort_spec_modify(file_path=SPEC_MODIFY_FILE)), (
f'{SPEC_MODIFY_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
@pytest.mark.update
@ -286,3 +438,9 @@ def test_sort_spec_data():
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
sort_data = sort_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)

View File

@ -56,7 +56,7 @@ async def test_lan_async(test_devices: dict):
# Your central hub gateway did
test_did = '111111'
# Your central hub gateway did
# Your central hub gateway token
test_token = '11223344556677d9a03d43936fc384205'
test_model = 'xiaomi.gateway.hub1'
# Your computer interface list, such as enp3s0, wlp5s0
@ -152,3 +152,5 @@ async def test_lan_async(test_devices: dict):
await asyncio.sleep(0.2)
await miot_lan.deinit_async()
await mips_service.deinit_async()
await miot_network.deinit_async()