94 Commits

Author SHA1 Message Date
8cbb451153 docs: update changelog and version to v0.4.0 (#1281)
Some checks failed
Tests / check-rule-format (push) Failing after 3s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 11s
Validate / validate-lint (push) Failing after 3s
Validate / validate-setup (push) Failing after 5s
2025-07-22 09:06:01 +08:00
0fee02ae5c feat: add notifications for the local connection to the central hub gateway (#1280)
Some checks failed
Tests / check-rule-format (push) Failing after 6s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 11s
Validate / validate-lint (push) Failing after 3s
Validate / validate-setup (push) Failing after 7s
* feat: add notifications for the central hub gateway connection status (#1103)

* fix: i18n langurage support

* fix: add notification of disconnection when ServiceStateChange.REMOVED
2025-07-21 16:49:42 +08:00
580ff87e7f fix: specs (#1256)
* fix: xiaomi.aircondition.c24 total power consumption unit (#1243)

* fix: adp.motor.adswb4 motor switch (#1257)

* docs: modify README

* fix: cgllc.airm.cgd1st environment temperature (#1270)

* fix: shhf.light.sflt11 fan switch status (#1276)
2025-07-21 16:44:47 +08:00
94583a23d1 fix: subscribe when connected to the central hub gateway (#1266)
Some checks failed
Tests / check-rule-format (push) Failing after 16s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 10s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 6s
2025-07-16 14:43:50 +08:00
925cf3b90f feat: do not subscribe BLE device online/offline state message (#1264) 2025-07-16 13:57:43 +08:00
836bd01ead fix: cover status (#1262) 2025-07-16 13:57:18 +08:00
4ad040d2ea feat: get devices from the third party cloud (#1258)
Some checks failed
Tests / check-rule-format (push) Failing after 5s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 11s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 5s
2025-07-15 16:26:35 +08:00
981429670a feat: add an alongside switch entity for viomi.waterheater.m1 (#1255) 2025-07-15 16:26:13 +08:00
a82fd86c60 fix: mdns discovered ip address (#1250)
Some checks failed
Tests / check-rule-format (push) Failing after 8s
Validate / validate-hassfest (push) Failing after 6s
Validate / validate-hacs (push) Failing after 12s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 9s
2025-07-14 09:04:38 +08:00
df3faea257 feat: set the cover closed position (#1242)
* feat: add the cover closed position configure option

* feat: estimate the cover entity's is_closed property by the cover closed position (#944)

* fix: translations

* feat: set max cover closed position as 5

* docs: modify README

* fix: remove useless spaces
2025-07-14 09:03:54 +08:00
e09676661c feat: add the watch as the device tracker entity (#1189)
Some checks failed
Tests / check-rule-format (push) Failing after 6s
Validate / validate-hassfest (push) Failing after 6s
Validate / validate-hacs (push) Failing after 14s
Validate / validate-lint (push) Failing after 6s
Validate / validate-setup (push) Failing after 8s
2025-07-09 14:14:27 +08:00
aebeaf0245 feat: add wifi speaker and television as the media player entity (#706) 2025-07-09 14:02:22 +08:00
4c2e10038c docs: update changelog and version to v0.3.4 (#1238) 2025-07-09 09:39:43 +08:00
9afc62f39a docs: modify README (#1237)
* docs: modify spec_filter.yaml in README

* docs: modify event descriptions in README
2025-07-09 09:17:57 +08:00
b46805b92c fix: airer status for cover entity (#1235)
* fix: xiaomi.airer.pro3 airer status rising (#1222)

* fix: airer status

* fix: filter out non alphabetic characters from status descriptions
2025-07-09 09:16:55 +08:00
a43447ef61 Fix specs (#1236)
* fix: ignore bjkcz.curtain.kczble curtain status (#1184)

* fix: yutai.plug.fsov8m power consumption (#1229)
2025-07-09 09:14:10 +08:00
ted
e5165f34da fix: Fix the HA warning in the logs related to vacuum state setting (#694)
Some checks failed
Tests / check-rule-format (push) Failing after 12s
Validate / validate-hassfest (push) Failing after 8s
Validate / validate-hacs (push) Failing after 3m6s
Validate / validate-lint (push) Failing after 11s
Validate / validate-setup (push) Failing after 9s
2025-07-08 13:48:17 +08:00
9fbbb26d33 fix: translation it.json (#1215) 2025-07-08 13:46:36 +08:00
5b1d003bb2 feat: subscribe the BLE device up messages even though the device is offline (#1207)
Some checks failed
Tests / check-rule-format (push) Failing after 10s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 11s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 5s
* feat: subscribe the BLE device up messages even though the device is offline (#1170)

* fix: set all BLE devices online
2025-06-30 11:27:12 +08:00
6069eaaba8 feat: exclude unsupported model (#1205)
* feat: ignore unsupported models (#933)

* fix: remove unnecessary logs
2025-06-30 11:12:58 +08:00
fd57e7c565 fix: reconnect delay time (#1200)
* fix: reset the reconnect interval when connected (#1175)

* feat: set the default reconnect delay time as 10 seconds

* fix: get the minimum reconnect interval
2025-06-30 11:12:18 +08:00
096b33f3c9 fix: the operation mode when the device does not have a mode property (#1199) 2025-06-30 11:11:36 +08:00
664787ca58 fix: ptx air-conditioner environment temperature (#1210)
Some checks failed
Tests / check-rule-format (push) Failing after 6s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 13s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 7s
* fix: ptx air-conditioner temperature #1163

* fix: environment temperature siid and piid
2025-06-27 17:52:39 +08:00
d659d13e49 docs: update changelog and version to v0.3.3 (#1188)
Some checks failed
Tests / check-rule-format (push) Failing after 9s
Validate / validate-hassfest (push) Failing after 9s
Validate / validate-hacs (push) Failing after 15s
Validate / validate-lint (push) Failing after 7s
Validate / validate-setup (push) Failing after 10s
2025-06-23 16:40:29 +08:00
3402587b1c Fix specs (#1187)
* fix: 090615.switch.x1tpm English switch name (#1122)

* fix: dmaker.fan.p33 fan-level (#1165)

* fix: humidity-range string value unit (#1179)
2025-06-23 16:21:39 +08:00
028399c0b1 fix: tofan.airrtc.wk01 thermostat and air conditioner service (#1160)
Some checks failed
Tests / check-rule-format (push) Failing after 5s
Validate / validate-hassfest (push) Failing after 6s
Validate / validate-hacs (push) Failing after 12s
Validate / validate-lint (push) Failing after 5s
Validate / validate-setup (push) Failing after 7s
2025-06-19 17:07:31 +08:00
5179e97e38 fix: the error "mips unsub internal error, 4, None" (#1135)
Some checks failed
Tests / check-rule-format (push) Failing after 8s
Validate / validate-hassfest (push) Failing after 6s
Validate / validate-hacs (push) Failing after 12s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 7s
2025-06-12 10:41:37 +08:00
9fdbf3dff2 fix: mrbond.airer.m1t closing status (#1134) 2025-06-12 10:41:12 +08:00
d0508ead25 Fix specs (#1132) 2025-06-12 10:40:54 +08:00
d05bdcbba9 fix: linp.magnet.m1 contact state in sensor text (#1116)
Some checks failed
Tests / check-rule-format (push) Failing after 6s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 12s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 6s
2025-05-29 13:32:42 +08:00
a4f9c29b6b docs: update changelog and version to v0.3.2 (#1119)
Some checks failed
Tests / check-rule-format (push) Failing after 4m35s
Validate / validate-hassfest (push) Failing after 4m35s
Validate / validate-hacs (push) Failing after 4m41s
Validate / validate-lint (push) Failing after 4m33s
Validate / validate-setup (push) Failing after 4m35s
2025-05-23 09:42:16 +08:00
62dd32a132 feat: add an alongside switch entity for the water heater (#1115) 2025-05-23 09:10:11 +08:00
1bd338639b feat: modify MIoT-Spec-V2 property format (#1111) 2025-05-23 08:45:35 +08:00
6a2534934c docs: update HACS instructions for Xiaomi Home integration (#1088)
Some checks failed
Tests / check-rule-format (push) Failing after 4m37s
Validate / validate-hassfest (push) Failing after 4m38s
Validate / validate-hacs (push) Failing after 5m43s
Validate / validate-lint (push) Failing after 4m38s
Validate / validate-setup (push) Failing after 4m39s
2025-05-22 16:10:20 +08:00
d06a564917 docs: add HACS installation path to README.md (#102) 2025-05-22 15:59:30 +08:00
23cc1130fe Fix specs (#1110)
* fix: the power consumption, the voltage and the current of lxzn.switch.cbcsmj

* fix: the fan direction of shhf.light.sfla10

* fix: the door state value-list description in Chinese of loock.lock.t2pv1

* fix: the stepless fan level of zhimi.fan.za4

* fix: the stepless fan level of zhimi.fan.sa1
2025-05-22 11:28:37 +08:00
a83ad60b38 fix: Chinese encoding under LAN Control (#1114) 2025-05-22 11:28:11 +08:00
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
63 changed files with 4374 additions and 1571 deletions

View File

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

View File

@ -1,4 +1,151 @@
# CHANGELOG # CHANGELOG
## v0.4.0
### Added
- Add the watch as the device tracker entity. [#1189](https://github.com/XiaoMi/ha_xiaomi_home/pull/1189)
- Add the wifi speaker and the television as the media player entity. [#706](https://github.com/XiaoMi/ha_xiaomi_home/pull/706)
- Add an option in CONFIGURE to set the cover closed position. [#1242](https://github.com/XiaoMi/ha_xiaomi_home/pull/1242)
- Add notifications to show the status of the local connection to the central hub gateway. [#1280](https://github.com/XiaoMi/ha_xiaomi_home/pull/1280)
- Import the device from the third party cloud. [#1258](https://github.com/XiaoMi/ha_xiaomi_home/pull/1258)
### Changed
- Add an alongside switch entity for viomi.waterheater.m1. [#1255](https://github.com/XiaoMi/ha_xiaomi_home/pull/1255)
- Do not subscribe BLE device online/offline state message. [#1264](https://github.com/XiaoMi/ha_xiaomi_home/pull/1264)
### Fixed
- Keep the first element of the discovered ip address list as the recently added address when getting mdns result. [#1250](https://github.com/XiaoMi/ha_xiaomi_home/pull/1250)
- Subscribe local topics every time when connected to the central hub gateway. [#1266](https://github.com/XiaoMi/ha_xiaomi_home/pull/1266)
- Record the "closing" and "closed" status that occur frequently in the motor-controller, the window-opener and the curtain service. [#1262](https://github.com/XiaoMi/ha_xiaomi_home/pull/1262)
- Fix xiaomi.aircondition.c24 total power consumption unit, adp.motor.adswb4 motor switch, cgllc.airm.cgd1st environment temperature, and shhf.light.sflt11 fan switch status. [#1256](https://github.com/XiaoMi/ha_xiaomi_home/pull/1256)
## v0.3.4
### Added
- Exclude the unsupported device models. [#1205](https://github.com/XiaoMi/ha_xiaomi_home/pull/1205)
### Changed
- Subscribe the BLE device upstream messages even though the device is offline. [#1207](https://github.com/XiaoMi/ha_xiaomi_home/pull/1207)
- Record "opening", "closing" and "closed" status of the airer service that occur frequently and do not record "stop" status for the cover entity. [#1235](https://github.com/XiaoMi/ha_xiaomi_home/pull/1235)
- Modify README about spec_filter.yaml and the event attributes. [#1237](https://github.com/XiaoMi/ha_xiaomi_home/pull/1237)
### Fixed
- Fix the reconnect delay time to be reset when the client is connected to the broker. [#1200](https://github.com/XiaoMi/ha_xiaomi_home/pull/1200)
- Fix the HA warning in the logs related to vacuum state setting. [#694](https://github.com/XiaoMi/ha_xiaomi_home/pull/694)
- Fix the operation mode when the device does not have a mode property. [#1199](https://github.com/XiaoMi/ha_xiaomi_home/pull/1199)
- Fix 090615.aircondition.ktf environment temperature. [#1210](https://github.com/XiaoMi/ha_xiaomi_home/pull/1210)
- Fix a missing variable in translation it.json. [#1215](https://github.com/XiaoMi/ha_xiaomi_home/pull/1215)
- Fix yutai.plug.fsov8m power consumption and ignore bjkcz.curtain.kczble curtain status. [#1236](https://github.com/XiaoMi/ha_xiaomi_home/pull/1236)
## v0.3.3
### Changed
- Change the log level of error "mips unsub internal error, 4, None". [#1135](https://github.com/XiaoMi/ha_xiaomi_home/pull/1135)
- Add necessary logs for distinguishing the set_properties command source. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)
### Fixed
- Fix tofan.airrtc.wk01 thermostat and air conditioner service. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)
- Fix mrbond.airer.m1t closing status. [#1134](https://github.com/XiaoMi/ha_xiaomi_home/pull/1134)
- Fix the MIoT-Spec-V2 of xiaomi.fan.p69 fan service, ainice.sensor_occupy.3b people number, cykj.hood.jyj22 ventilation switch status, xiaomi.fan.p43 fan level, zhimi.airp.ua1a pm10 density, 090615.switch.x1tpm switch status, dmaker.fan.p33 fan-level. [#1132](https://github.com/XiaoMi/ha_xiaomi_home/pull/1132)
- Fix cubee.airrtc.th123e and cubee.airrtc.th123w MIoT-Spec-V2 instance descriptions in Russian.
- Fix ijai.vacuum.v1 suction-state value-list descriptions in Chinese.
- Fix the misuse of Chinese brackets in multi_lang.json.
- The unit of the humidity-range property of xiaomi.aircondition.mt0, xiaomi.aircondition.c35, xiaomi.aircondition.c24 and xiaomi.aircondition.c20 is "none". [#1187](https://github.com/XiaoMi/ha_xiaomi_home/pull/1187)
## v0.3.2
> Xiaomi Home has been added to the Home Assistant Community Store (HACS) as a default since May 8, 2025.
### Added
- Modify MIoT-Spec-V2 property format by spec_modify.yaml. [#1111](https://github.com/XiaoMi/ha_xiaomi_home/pull/1111)
### Changed
- Update the instructions of Xiaomi Home integration installation from HACS. [#102](https://github.com/XiaoMi/ha_xiaomi_home/pull/102) [#1088](https://github.com/XiaoMi/ha_xiaomi_home/pull/1088)
- Add an alongside switch entity for zimi.waterheater.h03 and xiaomi.waterheater.yms2. [#1115](https://github.com/XiaoMi/ha_xiaomi_home/pull/1115)
### Fixed
- Fix Chinese encoding in LAN Control. [#1114](https://github.com/XiaoMi/ha_xiaomi_home/pull/1114)
- Fix the MIoT-Spec-V2 of lxzn.switch.jcbcsm power consumption, voltage and current, shhf.light.sfla10 fan direction, zhimi.fan.za4 fan-level, zhimi.fan.sa1 fan-level. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)
- Revise the Chinese descriptions of loock.lock.t2pv1 door state value-list. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)
## v0.3.1
### Changed
- Setting the fan speed level when the fan is off will turning the fan on first. [#1031](https://github.com/XiaoMi/ha_xiaomi_home/pull/1031)
### Fixed
- Fix update device list error when there is no shared devices. [#1024](https://github.com/XiaoMi/ha_xiaomi_home/pull/1024)
- Fix the humidifier get_prop_value error when the property is None. [#1035](https://github.com/XiaoMi/ha_xiaomi_home/pull/1035)
- Fix the MIoT-Spec-V2 of zhimi.fan.v3 fan-level, cuco.plug.cp1md voltage and current, zimi.plug.zncz01 electric-power, giot.plug.v8icm power-consumption unit, yunmi.kettle.r3 tds unit, and dmaker.fan.p5 fan-level. [#1037](https://github.com/XiaoMi/ha_xiaomi_home/pull/1037)
## v0.3.0
注意v0.3.0 变更了部分实体 unique_id 的生成规则,如果勾选 xiaomi_home > 配置 > 更新实体转换规则,会导致部分实体已配置的自动化失效。如果想要避免重新配置大量自动化,可使用这个[补丁](https://github.com/XiaoMi/ha_xiaomi_home/pull/972)。
CAUTION: v0.3.0 changes the unique_id of some entities. If you check the option `xiaomi_home > CONFIGURE > Update entity conversion rules`, it may cause the automation settings for these entities to fail. To avoid having to reconfigure a large number of automation settings, you can use this [patch](https://github.com/XiaoMi/ha_xiaomi_home/pull/972).
### Added
- Import the devices in the shared homes and the separated shared devices. [#1021](https://github.com/XiaoMi/ha_xiaomi_home/pull/1021)
- Support _attr_hvac_action of the climate entity. [#956](https://github.com/XiaoMi/ha_xiaomi_home/pull/956)
- Add custom defined MIoT-Spec-V2 instance via spec_add.json. [#953](https://github.com/XiaoMi/ha_xiaomi_home/pull/953)
### Fixed
- Ignore 'Event loop is closed' when unsub a closed event loop. [#991](https://github.com/XiaoMi/ha_xiaomi_home/pull/991)
- Fix contact-state for linp.magnet.m1 and loock.safe.v1. [#977](https://github.com/XiaoMi/ha_xiaomi_home/pull/977)
- Fix the mode initialization error of aupu.bhf_light.s368m. [#955](https://github.com/XiaoMi/ha_xiaomi_home/pull/955)
- Fix the MIoT-Spec-V2 of lumi.gateway.mcn001, qmi.plug.psv3, lumi.motion.acn001, izq.sensor_occupy.24, linp.sensor_occupy.hb01 and yunmi.waterpuri.s20. [#949](https://github.com/XiaoMi/ha_xiaomi_home/pull/949)
## v0.2.4
### Added
- Convert the submersion-state, the contact-state and the occupancy-status property to the binary_sensor entity. [#905](https://github.com/XiaoMi/ha_xiaomi_home/pull/905)
### Changed
- suittc.airrtc.wk168 mode descriptions are set to strings of numbers from 1 to 16. [#921](https://github.com/XiaoMi/ha_xiaomi_home/pull/921)
- Do not set _attr_suggested_display_precision when the spec.expr is set in spec_modify.yaml [#929](https://github.com/XiaoMi/ha_xiaomi_home/pull/929)
- Set "unknown event msg" log to info level.
### Fixed
- hhcc.plantmonitor.v1 soil moisture and soil ec icon and unit. [#927](https://github.com/XiaoMi/ha_xiaomi_home/pull/27)
- cuco.plug.cp2 voltage and power value ratio.
- cgllc.airmonitor.s1 unit ppb.
- roswan.waterpuri.lte01 tds unit.
- lumi.relay.c2acn01 power consumption unit
- xiaomi.bhf_light.s1 fan level of ventilation.
## v0.2.3
### Changed
- Specify the service name and the property name during the climate entity's on/off feature initialization. [#899](https://github.com/XiaoMi/ha_xiaomi_home/pull/899)
- Remove the useless total-battery property from `SPEC_PROP_TRANS_MAP`.
### Fixed
- Fix the hvac mode setting error when changing the preset mode of the ptc-bath-heater.
- Fix the ambiguous descriptions of yeelink.bhf_light.v10 ptc-bath-heater mode value-list.
- Fix the power consumption value of chuangmi.plug.212a01. [#910](https://github.com/XiaoMi/ha_xiaomi_home/pull/910)
## v0.2.2
This version has modified the conversion rules of the climate entity, which will have effect on the devices with the ptc-bath-heater, the air-conditioner and the air-fresh service. After updating, you need to restart Home Assistant and check `xiaomi_home > CONFIGURE >
Update entity conversion rules > NEXT` to reload the integration.
这个版本修改了浴霸、空调、新风机的实体转换规则,更新之后需要重启 Home Assistant并且勾选 `xiaomi_home > 配置 > 更新实体转换规则 > 下一步` 重新加载集成。
### Added
- Add conversion rules for the air-conditioner service and the air-fresh service. [#879](https://github.com/XiaoMi/ha_xiaomi_home/pull/879)
### Changed
- Convert the mode of the ptc bath heater to the preset mode of the climate entity. [#874](https://github.com/XiaoMi/ha_xiaomi_home/pull/874)
- Use Home Assistant default icon when device_class is set. [#855](https://github.com/XiaoMi/ha_xiaomi_home/pull/855)
### Fixed
- Fix xiaomi.aircondition.m9 humidity-range unit. [#878](https://github.com/XiaoMi/ha_xiaomi_home/pull/878)
- Fix MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2. [#866](https://github.com/XiaoMi/ha_xiaomi_home/pull/866)
## v0.2.1
### Added
- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)
### Changed
- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)
- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)
### Fixed
- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)
- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)
- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)
## v0.2.0
This version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.
这个版本修改了一些传感器默认单位,更新后会导致 Home Assistant 弹出一些兼容性提示,您可以重新添加集成解决。
### Added
- Add prop trans rule for surge-power. [#595](https://github.com/XiaoMi/ha_xiaomi_home/pull/595)
- Support modify spec and value conversion. [#663](https://github.com/XiaoMi/ha_xiaomi_home/pull/663)
- Support the electric blanket. [#781](https://github.com/XiaoMi/ha_xiaomi_home/pull/781)
- Add device with motor-control service as cover entity. [#688](https://github.com/XiaoMi/ha_xiaomi_home/pull/688)
### Changed
- Update README file. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681) [#747](https://github.com/XiaoMi/ha_xiaomi_home/pull/747)
- Update CONTRIBUTING.md. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681)
- Refactor climate.py. [#614](https://github.com/XiaoMi/ha_xiaomi_home/pull/614)
### Fixed
- Fix variable name or comment errors & fix test_lan error. [#678](https://github.com/XiaoMi/ha_xiaomi_home/pull/678) [#690](https://github.com/XiaoMi/ha_xiaomi_home/pull/690)
- Fix water heater error & some type error. [#684](https://github.com/XiaoMi/ha_xiaomi_home/pull/684)
- Fix fan level with value-list & fan reverse [#689](https://github.com/XiaoMi/ha_xiaomi_home/pull/689)
- Fix sensor display precision [#708](https://github.com/XiaoMi/ha_xiaomi_home/pull/708)
- Fix event:motion-detected without arguments [#712](https://github.com/XiaoMi/ha_xiaomi_home/pull/712)
## v0.1.5b2 ## v0.1.5b2
### Added ### Added
@ -91,10 +238,10 @@ This version will cause some Xiaomi routers that do not support access (#564) to
### Changed ### Changed
### Fixed ### Fixed
- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59 - Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59
- Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22 - Fix get homeinfo error. https://github.com/XiaoMi/ha_xiaomi_home/issues/22
- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16 - Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16
- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85 - Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11 https://github.com/XiaoMi/ha_xiaomi_home/issues/85
- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17 - Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17
## v0.1.0 ## v0.1.0
### Added ### Added

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 contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
When you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.
## How to Get Help ## How to Get Help
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub. If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### Method 2: [HACS](https://hacs.xyz/) ### Method 2: [HACS](https://hacs.xyz/)
HACS > Overflow Menu > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > Xiaomi Home in New or Available for download section of HACS > DOWNLOAD One-click installation from HACS:
> Xiaomi Home has not been added to the HACS store as a default yet. It's coming soon. [![Open your Home Assistant instance and open the Xiaomi Home integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
Or, HACS > In the search box, type **Xiaomi Home** > Click **Xiaomi Home**, getting into the detail page > DOWNLOAD
### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp) ### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)
@ -47,7 +49,7 @@ Download and copy `custom_components/xiaomi_home` folder to `config/custom_compo
[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account [Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home) [![Open your Home Assistant instance and start setting up a new Xiaomi Home integration instance.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
### Add MIoT Devices ### Add MIoT Devices
@ -59,7 +61,7 @@ After a Xiaomi account login and its user configuration are completed, you can c
Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) [![Open your Home Assistant instance and show Xiaomi Home integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
### Update Configurations ### Update Configurations
@ -81,9 +83,9 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
## FAQ ## FAQ
- Does Xiaomi Home Integration support all Xiaomi Home devices? - Does Xiaomi Home Integration support all Xiaomi smart devices?
Xiaomi Home Integration currently supports most categories of Home device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device. Xiaomi Home Integration currently supports most categories of the smart device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.
- Does Xiaomi Home Integration support multiple Xiaomi accounts? - Does Xiaomi Home Integration support multiple Xiaomi accounts?
@ -91,7 +93,7 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
- Does Xiaomi Home Integration support local control? - Does Xiaomi Home Integration support local control?
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.4.0_0000 above) or Xiaomi home devices with built-in central hub gateway (software version 0.8.0 above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans. Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with built-in central hub gateway (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available. Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
@ -153,6 +155,8 @@ In MIoT-Spec-V2 protocol, a product is defined as a device. A device contains se
MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`. MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.
MIoT-Spec-V2 event's arguments field is the list of parameters of the event. The list elements represent the piid of the property in the same service. For example, the [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057) of the Xiaomi Wireless Double-key Switch contains the siid=2 Switch Sensor service. The eiid=1014 Long Press event of the service is triggered when a button is long pressed. The debug level log will print `Press and hold, attributes: {'Button Type': 1}`. This is an example log that the button type is 1, which means the right button is long pressed.
- Action - Action
| in | Entity in Home Assistant | | in | Entity in Home Assistant |
@ -287,39 +291,37 @@ The value of the event instance name indicates `_attr_device_class` of the Home
### MIoT-Spec-V2 Filter ### MIoT-Spec-V2 Filter
`spec_filter.json` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity. `spec_filter.yaml` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.
The format of `spec_filter.json` is as follows. The format of `spec_filter.yaml` is as follows.
``` ```yaml
{ <MIoT-Spec-V2 device instance urn without the version field>:
"<MIoT-Spec-V2 device instance>":{ services: list<service_iid: str>
"services": list<service_iid: str>, properties: list<service_iid.property_iid: str>
"properties": list<service_iid.property_iid: str>, events: list<service_iid.event_iid: str>
"events": list<service_iid.event_iid: str>, actions: list<service_iid.action_iid: str>
"actions": list<service_iid.action_iid: str>,
}
}
``` ```
The key of `spec_filter.json` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.json` does not need to specify the version number of MIoT-Spec-V2 device instance. The key of `spec_filter.yaml` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.yaml` does not need to specify the version number of MIoT-Spec-V2 device instance.
The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported. The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.
Example: Example:
``` ```yaml
{ urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{ services:
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance. - '*' # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2.
}, urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { services:
"services": ["3"], # Filter out the service whose iid=3. - '3' # Filter out the siid=3 service.
"properties": ["4.*"] # Filter out all properties in the service whose iid=4. properties:
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service. - '4.*' # Filter out all properties in the siid=4 service.
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service. events:
} - '4.1' # Filter out the eiid=1 event in the siid=4 service.
} actions:
- '4.1' # Filter out the aiid=1 action in the siid=4 service.
``` ```
Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity. Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.
@ -374,7 +376,7 @@ Example:
} }
``` ```
> If you edit `specv2entity.py`, `spec_filter.json` or `multi_lang.json` in the `custom_components/xiaomi_home/miot/specs` directory in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update Entity Conversion Rule > If you edit any files in the `custom_components/xiaomi_home/miot/specs` directory (`spec_filter.yaml`, `spec_modify.yaml`, `multi_lang.json`, etc.) in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update entity conversion rules
## Documents ## Documents

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,9 @@ from .miot.const import (
DEFAULT_CLOUD_SERVER, DEFAULT_CLOUD_SERVER,
DEFAULT_CTRL_MODE, DEFAULT_CTRL_MODE,
DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_INTEGRATION_LANGUAGE,
DEFAULT_COVER_CLOSED_POSITION,
MIN_COVER_CLOSED_POSITION,
MAX_COVER_CLOSED_POSITION,
DEFAULT_NICK_NAME, DEFAULT_NICK_NAME,
DEFAULT_OAUTH2_API_HOST, DEFAULT_OAUTH2_API_HOST,
DOMAIN, DOMAIN,
@ -129,6 +132,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_cloud_server: str _cloud_server: str
_integration_language: str _integration_language: str
_cover_closed_position: int
_auth_info: dict _auth_info: dict
_nick_name: str _nick_name: str
_home_selected: dict _home_selected: dict
@ -151,6 +155,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._main_loop = asyncio.get_running_loop() self._main_loop = asyncio.get_running_loop()
self._cloud_server = DEFAULT_CLOUD_SERVER self._cloud_server = DEFAULT_CLOUD_SERVER
self._integration_language = DEFAULT_INTEGRATION_LANGUAGE self._integration_language = DEFAULT_INTEGRATION_LANGUAGE
self._cover_closed_position = DEFAULT_COVER_CLOSED_POSITION
self._storage_path = '' self._storage_path = ''
self._virtual_did = '' self._virtual_did = ''
self._uid = '' self._uid = ''
@ -565,27 +570,32 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
# i18n if device_source not in self._cc_home_info['homes']:
tip_central = '' continue
group_id = home_info.get('group_id', None) for home_id, home_info in self._cc_home_info[
dev_list = { 'homes'][device_source].items():
device['did']: device
for device in list(self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
# i18n # i18n
tip_central = self._miot_i18n.translate( tip_central = ''
key='config.other.found_central_gateway') group_id = home_info.get('group_id', None)
home_info['central_did'] = mips_list[group_id].get('did', None) dev_list = {
home_list[home_id] = ( device['did']: device
f'{home_info["home_name"]} ' for device in list(self._cc_home_info['devices'].values())
f'[ {len(dev_list)} {tip_devices} {tip_central} ]') if device.get('home_id', None) == home_id}
if (
mips_list
and group_id in mips_list
and mips_list[group_id].get('did', None) in dev_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(dev_list)} {tip_devices} {tip_central} ]')
self._cc_home_list_show = dict(sorted(home_list.items())) self._cc_home_list_show = dict(sorted(home_list.items()))
@ -660,10 +670,14 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not home_selected: if not home_selected:
return await self.__show_homes_select_form( return await self.__show_homes_select_form(
'no_family_selected') 'no_family_selected')
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
if home_id in home_selected: if device_source not in self._cc_home_info['homes']:
self._home_selected[home_id] = home_info continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in home_selected:
self._home_selected[home_id] = home_info
self._area_name_rule = user_input.get( self._area_name_rule = user_input.get(
'area_name_rule', self._area_name_rule) 'area_name_rule', self._area_name_rule)
# Storage device list # Storage device list
@ -942,6 +956,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
'action_debug': self._action_debug, 'action_debug': self._action_debug,
'hide_non_standard_entities': 'hide_non_standard_entities':
self._hide_non_standard_entities, self._hide_non_standard_entities,
'cover_closed_position': self._cover_closed_position,
'display_binary_mode': self._display_binary_mode, 'display_binary_mode': self._display_binary_mode,
'display_devices_changed_notify': 'display_devices_changed_notify':
self._display_devices_changed_notify self._display_devices_changed_notify
@ -986,6 +1001,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_hide_non_standard_entities: bool _hide_non_standard_entities: bool
_display_binary_mode: list[str] _display_binary_mode: list[str]
_display_devs_notify: list[str] _display_devs_notify: list[str]
_cover_closed_position: int
_oauth_redirect_url_full: str _oauth_redirect_url_full: str
_auth_info: dict _auth_info: dict
@ -1006,6 +1022,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_opt_lan_ctrl_cfg: bool _opt_lan_ctrl_cfg: bool
_opt_network_detect_cfg: bool _opt_network_detect_cfg: bool
_opt_check_network_deps: bool _opt_check_network_deps: bool
_cover_pos_new: int
_trans_rules_count: int _trans_rules_count: int
_trans_rules_count_success: int _trans_rules_count_success: int
@ -1034,6 +1051,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE) self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)
self._integration_language = self._entry_data.get( self._integration_language = self._entry_data.get(
'integration_language', DEFAULT_INTEGRATION_LANGUAGE) 'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
self._cover_closed_position = self._entry_data.get(
'cover_closed_position', DEFAULT_COVER_CLOSED_POSITION)
self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME) self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)
self._action_debug = self._entry_data.get('action_debug', False) self._action_debug = self._entry_data.get('action_debug', False)
self._hide_non_standard_entities = self._entry_data.get( self._hide_non_standard_entities = self._entry_data.get(
@ -1059,6 +1078,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._action_debug_new = False self._action_debug_new = False
self._hide_non_standard_entities_new = False self._hide_non_standard_entities_new = False
self._display_binary_mode_new = [] self._display_binary_mode_new = []
self._cover_pos_new = self._cover_closed_position
self._update_user_info = False self._update_user_info = False
self._update_devices = False self._update_devices = False
self._update_trans_rules = False self._update_trans_rules = False
@ -1331,6 +1351,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
): cv.multi_select( ): cv.multi_select(
self._miot_i18n.translate( self._miot_i18n.translate(
'config.binary_mode')), # type: ignore 'config.binary_mode')), # type: ignore
vol.Optional(
'cover_closed_position',
default=self._cover_closed_position # type: ignore
): vol.All(vol.Coerce(int), vol.Range(
min=MIN_COVER_CLOSED_POSITION,
max=MAX_COVER_CLOSED_POSITION)),
vol.Required( vol.Required(
'update_trans_rules', 'update_trans_rules',
default=self._update_trans_rules # type: ignore default=self._update_trans_rules # type: ignore
@ -1369,6 +1395,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'update_lan_ctrl_config', self._opt_lan_ctrl_cfg) 'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)
self._opt_network_detect_cfg = user_input.get( self._opt_network_detect_cfg = user_input.get(
'network_detect_config', self._opt_network_detect_cfg) 'network_detect_config', self._opt_network_detect_cfg)
self._cover_pos_new = user_input.get(
'cover_closed_position', self._cover_closed_position)
return await self.async_step_update_user_info() return await self.async_step_update_user_info()
@ -1420,27 +1448,31 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
home_list = {} home_list = {}
tip_devices = self._miot_i18n.translate(key='config.other.devices') tip_devices = self._miot_i18n.translate(key='config.other.devices')
# home list # home list
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
# i18n if device_source not in self._cc_home_info['homes']:
tip_central = '' continue
group_id = home_info.get('group_id', None) for home_id, home_info in self._cc_home_info[
did_list = { 'homes'][device_source].items():
device['did']: device for device in list(
self._cc_home_info['devices'].values())
if device.get('home_id', None) == home_id}
if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
# i18n # i18n
tip_central = self._miot_i18n.translate( tip_central = ''
key='config.other.found_central_gateway') group_id = home_info.get('group_id', None)
home_info['central_did'] = mips_list[group_id].get( did_list = {
'did', None) device['did']: device for device in list(
home_list[home_id] = ( self._cc_home_info['devices'].values())
f'{home_info["home_name"]} ' if device.get('home_id', None) == home_id}
f'[ {len(did_list)} {tip_devices} {tip_central} ]') if (
group_id in mips_list
and mips_list[group_id].get('did', None) in did_list
):
# i18n
tip_central = self._miot_i18n.translate(
key='config.other.found_central_gateway')
home_info['central_did'] = mips_list[group_id].get(
'did', None)
home_list[home_id] = (
f'{home_info["home_name"]} '
f'[ {len(did_list)} {tip_devices} {tip_central} ]')
# Remove deleted item # Remove deleted item
self._home_selected_list = [ self._home_selected_list = [
home_id for home_id in self._home_selected_list home_id for home_id in self._home_selected_list
@ -1460,10 +1492,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return await self.__show_homes_select_form('no_family_selected') return await self.__show_homes_select_form('no_family_selected')
self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode) self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)
self._home_selected = {} self._home_selected = {}
for home_id, home_info in self._cc_home_info[ for device_source in ['home_list','share_home_list',
'homes']['home_list'].items(): 'separated_shared_list']:
if home_id in self._home_selected_list: if device_source not in self._cc_home_info['homes']:
self._home_selected[home_id] = home_info continue
for home_id, home_info in self._cc_home_info[
'homes'][device_source].items():
if home_id in self._home_selected_list:
self._home_selected[home_id] = home_info
# Get device list # Get device list
device_list: dict = { device_list: dict = {
did: dev_info did: dev_info
@ -1909,6 +1945,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'nick_name': self._nick_name, 'nick_name': self._nick_name,
'lang_new': INTEGRATION_LANGUAGES[self._lang_new], 'lang_new': INTEGRATION_LANGUAGES[self._lang_new],
'nick_name_new': self._nick_name_new, 'nick_name_new': self._nick_name_new,
'cover_pos_new': self._cover_pos_new,
'devices_add': len(self._devices_add), 'devices_add': len(self._devices_add),
'devices_remove': len(self._devices_remove), 'devices_remove': len(self._devices_remove),
'trans_rules_count': self._trans_rules_count, 'trans_rules_count': self._trans_rules_count,
@ -1935,6 +1972,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if self._lang_new != self._integration_language: if self._lang_new != self._integration_language:
self._entry_data['integration_language'] = self._lang_new self._entry_data['integration_language'] = self._lang_new
self._need_reload = True self._need_reload = True
if self._cover_pos_new != self._cover_closed_position:
self._entry_data['cover_closed_position'] = self._cover_pos_new
self._need_reload = True
if self._update_user_info: if self._update_user_info:
self._entry_data['nick_name'] = self._nick_name_new self._entry_data['nick_name'] = self._nick_name_new
if self._update_devices: if self._update_devices:

View File

@ -46,31 +46,26 @@ off Xiaomi or its affiliates' products.
Cover entities for Xiaomi Home. Cover entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional
import re
import logging import logging
from typing import Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.cover import ( from homeassistant.components.cover import (ATTR_POSITION, CoverEntity,
ATTR_POSITION, CoverEntityFeature,
CoverEntity, CoverDeviceClass)
CoverEntityFeature,
CoverDeviceClass
)
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
hass: HomeAssistant, async_add_entities: AddEntitiesCallback) -> None:
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id] config_entry.entry_id]
@ -82,8 +77,12 @@ async def async_setup_entry(
data.spec.device_class = CoverDeviceClass.CURTAIN data.spec.device_class = CoverDeviceClass.CURTAIN
elif data.spec.name == 'window-opener': elif data.spec.name == 'window-opener':
data.spec.device_class = CoverDeviceClass.WINDOW data.spec.device_class = CoverDeviceClass.WINDOW
new_entities.append( elif data.spec.name == 'motor-controller':
Cover(miot_device=miot_device, entity_data=data)) data.spec.device_class = CoverDeviceClass.SHUTTER
elif data.spec.name == 'airer':
data.spec.device_class = CoverDeviceClass.BLIND
new_entities.append(Cover(miot_device=miot_device,
entity_data=data))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -92,78 +91,105 @@ async def async_setup_entry(
class Cover(MIoTServiceEntity, CoverEntity): class Cover(MIoTServiceEntity, CoverEntity):
"""Cover entities for Xiaomi Home.""" """Cover entities for Xiaomi Home."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
_cover_closed_position: int
_prop_motor_control: Optional[MIoTSpecProperty] _prop_motor_control: Optional[MIoTSpecProperty]
_prop_motor_value_open: Optional[int] _prop_motor_value_open: Optional[int]
_prop_motor_value_close: Optional[int] _prop_motor_value_close: Optional[int]
_prop_motor_value_pause: Optional[int] _prop_motor_value_pause: Optional[int]
_prop_status: Optional[MIoTSpecProperty] _prop_status: Optional[MIoTSpecProperty]
_prop_status_opening: Optional[int] _prop_status_opening: Optional[list[int]]
_prop_status_closing: Optional[int] _prop_status_closing: Optional[list[int]]
_prop_status_stop: Optional[int] _prop_status_closed: Optional[list[int]]
_prop_current_position: Optional[MIoTSpecProperty] _prop_current_position: Optional[MIoTSpecProperty]
_prop_target_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty]
_prop_position_value_min: Optional[int] _prop_position_value_min: Optional[int]
_prop_position_value_max: Optional[int] _prop_position_value_max: Optional[int]
_prop_position_value_range: Optional[int] _prop_position_value_range: Optional[int]
_prop_pos_closing: bool
_prop_pos_opening: bool
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Cover.""" """Initialize the Cover."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = entity_data.spec.device_class self._attr_device_class = entity_data.spec.device_class
self._attr_supported_color_modes = set() self._attr_supported_color_modes = set()
self._attr_supported_features = CoverEntityFeature(0) self._attr_supported_features = CoverEntityFeature(0)
self._cover_closed_position = (
miot_device.miot_client.cover_closed_position)
self._prop_motor_control = None self._prop_motor_control = None
self._prop_motor_value_open = None self._prop_motor_value_open = None
self._prop_motor_value_close = None self._prop_motor_value_close = None
self._prop_motor_value_pause = None self._prop_motor_value_pause = None
self._prop_status = None self._prop_status = None
self._prop_status_opening = None self._prop_status_opening = []
self._prop_status_closing = None self._prop_status_closing = []
self._prop_status_stop = None self._prop_status_closed = []
self._prop_current_position = None self._prop_current_position = None
self._prop_target_position = None self._prop_target_position = None
self._prop_position_value_min = None self._prop_position_value_min = None
self._prop_position_value_max = None self._prop_position_value_max = None
self._prop_position_value_range = None self._prop_position_value_range = None
self._prop_pos_closing = False
self._prop_pos_opening = False
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'motor-control': if prop.name == 'motor-control':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('motor-control value_list is None, %s',
'motor-control value_list is None, %s', self.entity_id) self.entity_id)
continue continue
for item in prop.value_list.items: for item in prop.value_list.items:
if item.name in {'open'}: if item.name in {'open', 'up'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.OPEN) CoverEntityFeature.OPEN)
self._prop_motor_value_open = item.value self._prop_motor_value_open = item.value
elif item.name in {'close'}: elif item.name in {'close', 'down'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.CLOSE) CoverEntityFeature.CLOSE)
self._prop_motor_value_close = item.value self._prop_motor_value_close = item.value
elif item.name in {'pause'}: elif item.name in {'pause', 'stop'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.STOP) CoverEntityFeature.STOP)
self._prop_motor_value_pause = item.value self._prop_motor_value_pause = item.value
self._prop_motor_control = prop self._prop_motor_control = prop
elif prop.name == 'status': elif prop.name == 'status':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('status value_list is None, %s',
'status value_list is None, %s', self.entity_id) self.entity_id)
continue continue
for item in prop.value_list.items: for item in prop.value_list.items:
if item.name in {'opening', 'open'}: item_str: str = item.name
self._prop_status_opening = item.value item_name: str = re.sub(r'[^a-z]', '', item_str)
elif item.name in {'closing', 'close'}: if item_name in {
self._prop_status_closing = item.value 'opening', 'open', 'up', 'uping', 'rise', 'rising'
elif item.name in {'stop', 'pause'}: }:
self._prop_status_stop = item.value self._prop_status_opening.append(item.value)
elif item_name in {
'closing', 'close', 'down', 'dowm', 'falling',
'fallin', 'dropping', 'downing', 'lower'
}:
self._prop_status_closing.append(item.value)
elif item_name in {
'closed', 'closeover', 'stopatlowest',
'stoplowerlimit', 'lowerlimitstop', 'floor',
'lowerlimit'
}:
self._prop_status_closed.append(item.value)
self._prop_status = prop self._prop_status = prop
elif prop.name == 'current-position': elif prop.name == 'current-position':
if not prop.value_range:
_LOGGER.error(
'invalid current-position value_range format, %s',
self.entity_id)
continue
self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = (prop.value_range.max_ -
prop.value_range.min_)
self._prop_current_position = prop self._prop_current_position = prop
elif prop.name == 'target-position': elif prop.name == 'target-position':
if not prop.value_range: if not prop.value_range:
@ -173,35 +199,65 @@ class Cover(MIoTServiceEntity, CoverEntity):
continue continue
self._prop_position_value_min = prop.value_range.min_ self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range.max_ self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = ( self._prop_position_value_range = (prop.value_range.max_ -
self._prop_position_value_max - prop.value_range.min_)
self._prop_position_value_min)
self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._attr_supported_features |= CoverEntityFeature.SET_POSITION
self._prop_target_position = prop self._prop_target_position = prop
# For the device that has the current position property but no status
# property, the current position property will be used to determine the
# opening and the closing status.
if (self._prop_status is None) and (self._prop_current_position
is not None):
self.sub_prop_changed(self._prop_current_position,
self._position_changed_handler)
def _position_changed_handler(self, prop: MIoTSpecProperty,
ctx: Any) -> None:
self._prop_pos_closing = False
self._prop_pos_opening = False
self.async_write_ha_state()
async def async_open_cover(self, **kwargs) -> None: async def async_open_cover(self, **kwargs) -> None:
"""Open the cover.""" """Open the cover."""
await self.set_property_async( current = None if (self._prop_current_position
self._prop_motor_control, self._prop_motor_value_open) is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current < self._prop_position_value_max):
self._prop_pos_opening = True
self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_open)
async def async_close_cover(self, **kwargs) -> None: async def async_close_cover(self, **kwargs) -> None:
"""Close the cover.""" """Close the cover."""
await self.set_property_async( current = None if (self._prop_current_position
self._prop_motor_control, self._prop_motor_value_close) is None) else self.get_prop_value(
prop=self._prop_current_position)
if (current is not None) and (current > self._prop_position_value_min):
self._prop_pos_opening = False
self._prop_pos_closing = True
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_close)
async def async_stop_cover(self, **kwargs) -> None: async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover.""" """Stop the cover."""
await self.set_property_async( self._prop_pos_opening = False
self._prop_motor_control, self._prop_motor_value_pause) self._prop_pos_closing = False
await self.set_property_async(self._prop_motor_control,
self._prop_motor_value_pause)
async def async_set_cover_position(self, **kwargs) -> None: async def async_set_cover_position(self, **kwargs) -> None:
"""Set the position of the cover.""" """Set the position of the cover."""
pos = kwargs.get(ATTR_POSITION, None) pos = kwargs.get(ATTR_POSITION, None)
if pos is None: if pos is None:
return None return None
pos = round(pos*self._prop_position_value_range/100) current = self.current_cover_position
return await self.set_property_async( if current is not None:
prop=self._prop_target_position, value=pos) self._prop_pos_opening = pos > current
self._prop_pos_closing = pos < current
pos = round(pos * self._prop_position_value_range / 100)
await self.set_property_async(prop=self._prop_target_position,
value=pos)
@property @property
def current_cover_position(self) -> Optional[int]: def current_cover_position(self) -> Optional[int]:
@ -209,28 +265,47 @@ class Cover(MIoTServiceEntity, CoverEntity):
0: the cover is closed, 100: the cover is fully opened, None: unknown. 0: the cover is closed, 100: the cover is fully opened, None: unknown.
""" """
if self._prop_current_position is None:
# Assume that the current position is the same as the target
# position when the current position is not defined in the device's
# MIoT-Spec-V2.
if self._prop_target_position is None:
return None
self._prop_pos_opening = False
self._prop_pos_closing = False
return self.get_prop_value(prop=self._prop_target_position)
pos = self.get_prop_value(prop=self._prop_current_position) pos = self.get_prop_value(prop=self._prop_current_position)
if pos is None: return None if pos is None else round(pos * 100 /
return None self._prop_position_value_range)
return round(pos*100/self._prop_position_value_range)
@property @property
def is_opening(self) -> Optional[bool]: def is_opening(self) -> Optional[bool]:
"""Return if the cover is opening.""" """Return if the cover is opening."""
if self._prop_status is None: if self._prop_status and self._prop_status_opening:
return None return (self.get_prop_value(prop=self._prop_status)
return self.get_prop_value( in self._prop_status_opening)
prop=self._prop_status) == self._prop_status_opening # The status has higher priority when determining whether the cover
# is opening.
return self._prop_pos_opening
@property @property
def is_closing(self) -> Optional[bool]: def is_closing(self) -> Optional[bool]:
"""Return if the cover is closing.""" """Return if the cover is closing."""
if self._prop_status is None: if self._prop_status and self._prop_status_closing:
return None return (self.get_prop_value(prop=self._prop_status)
return self.get_prop_value( in self._prop_status_closing)
prop=self._prop_status) == self._prop_status_closing # The status has higher priority when determining whether the cover
# is closing.
return self._prop_pos_closing
@property @property
def is_closed(self) -> Optional[bool]: def is_closed(self) -> Optional[bool]:
"""Return if the cover is closed.""" """Return if the cover is closed."""
return self.get_prop_value(prop=self._prop_current_position) == 0 if self.current_cover_position is not None:
return self.current_cover_position <= self._cover_closed_position
# 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

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Xiaomi Corporation.
The ownership and intellectual property rights of Xiaomi Home Assistant
Integration and related Xiaomi cloud service API interface provided under this
license, including source code and object code (collectively, "Licensed Work"),
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
hereby grants you a personal, limited, non-exclusive, non-transferable,
non-sublicensable, and royalty-free license to reproduce, use, modify, and
distribute the Licensed Work only for your use of Home Assistant for
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
you to use the Licensed Work for any other purpose, including but not limited
to use Licensed Work to develop applications (APP), Web services, and other
forms of software.
You may reproduce and distribute copies of the Licensed Work, with or without
modifications, whether in source or object form, provided that you must give
any other recipients of the Licensed Work a copy of this License and retain all
copyright and disclaimers.
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied, including, without
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
for any direct, indirect, special, incidental, or consequential damages or
losses arising from the use or inability to use the Licensed Work.
Xiaomi reserves all rights not expressly granted to you in this License.
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
does not authorize you in any form to use the trademarks, copyrights, or other
forms of intellectual property rights of Xiaomi and its affiliates, including,
without limitation, without obtaining other written permission from Xiaomi, you
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
may make the public associate with Xiaomi in any form to publicize or promote
the software or hardware devices that use the Licensed Work.
Xiaomi has the right to immediately terminate all your authorization under this
License in the event:
1. You assert patent invalidation, litigation, or other claims against patents
or other intellectual property rights of Xiaomi or its affiliates; or,
2. You make, have made, manufacture, sell, or offer to sell products that knock
off Xiaomi or its affiliates' products.
Device tracker entities for Xiaomi Home.
"""
from __future__ import annotations
from typing import Optional
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.device_tracker import TrackerEntity
from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
from .miot.miot_spec import MIoTSpecProperty
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id]
new_entities = []
for miot_device in device_list:
for data in miot_device.entity_list.get('device_tracker', []):
new_entities.append(
DeviceTracker(miot_device=miot_device, entity_data=data))
if new_entities:
async_add_entities(new_entities)
class DeviceTracker(MIoTServiceEntity, TrackerEntity):
"""Tracker entities for Xiaomi Home."""
_prop_battery_level: Optional[MIoTSpecProperty]
_prop_latitude: Optional[MIoTSpecProperty]
_prop_longitude: Optional[MIoTSpecProperty]
_prop_area_id: Optional[MIoTSpecProperty]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._prop_battery_level = None
self._prop_latitude = None
self._prop_longitude = None
self._prop_area_id = None
# properties
for prop in entity_data.props:
if prop.name == 'battery-level':
self._prop_battery_level = prop
elif prop.name == 'latitude':
self._prop_latitude = prop
elif prop.name == 'longitude':
self._prop_longitude = prop
elif prop.name == 'area-id':
self._prop_area_id = prop
@property
def battery_level(self) -> Optional[int]:
"""The battery level of the device."""
return None if (self._prop_battery_level
is None) else self.get_prop_value(
prop=self._prop_battery_level)
@property
def latitude(self) -> Optional[float]:
"""The latitude coordinate of the device."""
return None if self._prop_latitude is None else self.get_prop_value(
prop=self._prop_latitude)
@property
def longitude(self) -> Optional[float]:
"""The longitude coordinate of the device."""
return None if self._prop_longitude is None else self.get_prop_value(
prop=self._prop_longitude)
@property
def location_name(self) -> Optional[str]:
"""The location name of the device."""
return None if self._prop_area_id is None else self.get_prop_value(
prop=self._prop_area_id)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,470 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Xiaomi Corporation.
The ownership and intellectual property rights of Xiaomi Home Assistant
Integration and related Xiaomi cloud service API interface provided under this
license, including source code and object code (collectively, "Licensed Work"),
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
hereby grants you a personal, limited, non-exclusive, non-transferable,
non-sublicensable, and royalty-free license to reproduce, use, modify, and
distribute the Licensed Work only for your use of Home Assistant for
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
you to use the Licensed Work for any other purpose, including but not limited
to use Licensed Work to develop applications (APP), Web services, and other
forms of software.
You may reproduce and distribute copies of the Licensed Work, with or without
modifications, whether in source or object form, provided that you must give
any other recipients of the Licensed Work a copy of this License and retain all
copyright and disclaimers.
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied, including, without
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
for any direct, indirect, special, incidental, or consequential damages or
losses arising from the use or inability to use the Licensed Work.
Xiaomi reserves all rights not expressly granted to you in this License.
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
does not authorize you in any form to use the trademarks, copyrights, or other
forms of intellectual property rights of Xiaomi and its affiliates, including,
without limitation, without obtaining other written permission from Xiaomi, you
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
may make the public associate with Xiaomi in any form to publicize or promote
the software or hardware devices that use the Licensed Work.
Xiaomi has the right to immediately terminate all your authorization under this
License in the event:
1. You assert patent invalidation, litigation, or other claims against patents
or other intellectual property rights of Xiaomi or its affiliates; or,
2. You make, have made, manufacture, sell, or offer to sell products that knock
off Xiaomi or its affiliates' products.
Media player entities for Xiaomi Home.
"""
from __future__ import annotations
import logging
from typing import Optional
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.media_player import (MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerDeviceClass,
MediaPlayerState, MediaType)
from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
from .miot.miot_spec import MIoTSpecProperty, MIoTSpecAction
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback) -> None:
"""Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id]
new_entities = []
for miot_device in device_list:
for data in miot_device.entity_list.get('wifi-speaker', []):
new_entities.append(
WifiSpeaker(miot_device=miot_device, entity_data=data))
for data in miot_device.entity_list.get('television', []):
new_entities.append(
Television(miot_device=miot_device, entity_data=data))
if new_entities:
async_add_entities(new_entities)
class FeatureVolumeMute(MIoTServiceEntity, MediaPlayerEntity):
"""VOLUME_MUTE feature of the media player entity."""
_prop_mute: Optional[MIoTSpecProperty]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._prop_mute = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'mute':
self._attr_supported_features |= (
MediaPlayerEntityFeature.VOLUME_MUTE)
self._prop_mute = prop
@property
def is_volume_muted(self) -> Optional[bool]:
"""True if volume is currently muted."""
return self.get_prop_value(
prop=self._prop_mute) if self._prop_mute else None
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self.set_property_async(prop=self._prop_mute, value=mute)
class FeatureVolumeSet(MIoTServiceEntity, MediaPlayerEntity):
"""VOLUME_SET feature of the media player entity."""
_prop_volume: Optional[MIoTSpecProperty]
_volume_value_min: Optional[float]
_volume_value_max: Optional[float]
_volume_value_range: Optional[float]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._prop_volume = None
self._volume_value_min = None
self._volume_value_max = None
self._volume_value_range = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'volume':
if not prop.value_range:
_LOGGER.error('invalid volume value_range format, %s',
self.entity_id)
continue
self._volume_value_min = prop.value_range.min_
self._volume_value_max = prop.value_range.max_
self._volume_value_range = (prop.value_range.max_ -
prop.value_range.min_)
self._attr_volume_step = (prop.value_range.step /
self._volume_value_range)
self._attr_supported_features |= (
MediaPlayerEntityFeature.VOLUME_SET |
MediaPlayerEntityFeature.VOLUME_STEP)
self._prop_volume = prop
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level."""
value = volume * self._volume_value_range + self._volume_value_min
if value > self._volume_value_max:
value = self._volume_value_max
elif value < self._volume_value_min:
value = self._volume_value_min
await self.set_property_async(prop=self._prop_volume, value=value)
@property
def volume_level(self) -> Optional[float]:
"""The current volume level, range [0, 1]."""
value = self.get_prop_value(
prop=self._prop_volume) if self._prop_volume else None
if value is None:
return None
return (value - self._volume_value_min) / self._volume_value_range
class FeaturePlay(MIoTServiceEntity, MediaPlayerEntity):
"""PLAY feature of the media player entity."""
_action_play: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_play = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'play':
self._attr_supported_features |= (MediaPlayerEntityFeature.PLAY)
self._action_play = act
async def async_media_play(self) -> None:
"""Send play command."""
await self.action_async(action=self._action_play)
class FeaturePause(MIoTServiceEntity, MediaPlayerEntity):
"""PAUSE feature of the media player entity."""
_action_pause: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_pause = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'pause':
self._attr_supported_features |= (
MediaPlayerEntityFeature.PAUSE)
self._action_pause = act
async def async_media_pause(self) -> None:
"""Send pause command."""
await self.action_async(action=self._action_pause)
class FeatureStop(MIoTServiceEntity, MediaPlayerEntity):
"""STOP feature of the media player entity."""
_action_stop: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_stop = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'stop':
self._attr_supported_features |= (MediaPlayerEntityFeature.STOP)
self._action_stop = act
async def async_media_stop(self) -> None:
"""Send stop command."""
await self.action_async(action=self._action_stop)
class FeatureNextTrack(MIoTServiceEntity, MediaPlayerEntity):
"""NEXT_TRACK feature of the media player entity."""
_action_next: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_next = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'next':
self._attr_supported_features |= (
MediaPlayerEntityFeature.NEXT_TRACK)
self._action_next = act
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.action_async(action=self._action_next)
class FeaturePreviousTrack(MIoTServiceEntity, MediaPlayerEntity):
"""PREVIOUS_TRACK feature of the media player entity."""
_action_previous: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_previous = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'previous':
self._attr_supported_features |= (
MediaPlayerEntityFeature.PREVIOUS_TRACK)
self._action_previous = act
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.action_async(action=self._action_previous)
class FeatureSoundMode(MIoTServiceEntity, MediaPlayerEntity):
"""SELECT_SOUND_MODE feature of the media player entity."""
_prop_play_loop_mode: Optional[MIoTSpecProperty]
_sound_mode_map: Optional[dict[int, str]]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._prop_play_loop_mode = None
self._sound_mode_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'play-loop-mode':
if not prop.value_list:
_LOGGER.error('invalid play-loop-mode value_list, %s',
self.entity_id)
continue
self._sound_mode_map = prop.value_list.to_map()
self._attr_sound_mode_list = list(self._sound_mode_map.values())
self._attr_supported_features |= (
MediaPlayerEntityFeature.SELECT_SOUND_MODE)
self._prop_play_loop_mode = prop
async def async_select_sound_mode(self, sound_mode: str):
"""Switch the sound mode of the entity."""
await self.set_property_async(prop=self._prop_play_loop_mode,
value=self.get_map_key(
map_=self._sound_mode_map,
value=sound_mode))
@property
def sound_mode(self) -> Optional[str]:
"""The current sound mode."""
return (self.get_map_value(map_=self._sound_mode_map,
key=self.get_prop_value(
prop=self._prop_play_loop_mode))
if self._prop_play_loop_mode else None)
class FeatureTurnOn(MIoTServiceEntity, MediaPlayerEntity):
"""TURN_ON feature of the media player entity."""
_action_turn_on: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_turn_on = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'turn-on':
self._attr_supported_features |= (
MediaPlayerEntityFeature.TURN_ON)
self._action_turn_on = act
async def async_turn_on(self) -> None:
"""Turn the media player on."""
await self.action_async(action=self._action_turn_on)
class FeatureTurnOff(MIoTServiceEntity, MediaPlayerEntity):
"""TURN_OFF feature of the media player entity."""
_action_turn_off: Optional[MIoTSpecAction]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._action_turn_off = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# actions
for act in entity_data.actions:
if act.name == 'turn-off':
self._attr_supported_features |= (
MediaPlayerEntityFeature.TURN_OFF)
self._action_turn_off = act
async def async_turn_off(self) -> None:
"""Turn the media player off."""
await self.action_async(action=self._action_turn_off)
class FeatureSource(MIoTServiceEntity, MediaPlayerEntity):
"""SELECT_SOURCE feature of the media player entity."""
_prop_input_control: Optional[MIoTSpecProperty]
_input_source_map: Optional[dict[int, str]]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._prop_input_control = None
self._input_source_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'input-control':
if not prop.value_list:
_LOGGER.error('invalid input-control value_list, %s',
self.entity_id)
continue
self._input_source_map = prop.value_list.to_map()
self._attr_source_list = list(self._input_source_map.values())
self._attr_supported_features |= (
MediaPlayerEntityFeature.SELECT_SOURCE)
self._prop_input_control = prop
async def async_select_source(self, source: str) -> None:
"""Select input source."""
await self.set_property_async(prop=self._prop_input_control,
value=self.get_map_key(
map_=self._input_source_map,
value=source))
@property
def source(self) -> Optional[str]:
"""The current input source."""
return (self.get_map_value(map_=self._input_source_map,
key=self.get_prop_value(
prop=self._prop_input_control))
if self._prop_input_control else None)
class FeatureState(MIoTServiceEntity, MediaPlayerEntity):
"""States feature of the media player entity."""
_prop_playing_state: Optional[MIoTSpecProperty]
_playing_state_map: Optional[dict[int, str]]
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the feature class."""
self._prop_playing_state = None
self._playing_state_map = None
super().__init__(miot_device=miot_device, entity_data=entity_data)
# properties
for prop in entity_data.props:
if prop.name == 'playing-state':
if not prop.value_list:
_LOGGER.error('invalid mode value_list, %s', self.entity_id)
continue
self._playing_state_map = {}
for item in prop.value_list.items:
if item.name in {'off'}:
self._playing_state_map[
item.value] = MediaPlayerState.OFF
elif item.name in {'idle', 'stop', 'stopped'}:
self._playing_state_map[
item.value] = MediaPlayerState.IDLE
elif item.name in {'playing'}:
self._playing_state_map[
item.value] = MediaPlayerState.PLAYING
elif item.name in {'pause', 'paused'}:
self._playing_state_map[
item.value] = MediaPlayerState.PAUSED
self._prop_playing_state = prop
@property
def state(self) -> Optional[MediaPlayerState]:
"""The current state."""
return (self.get_map_value(map_=self._playing_state_map,
key=self.get_prop_value(
prop=self._prop_playing_state))
if self._prop_playing_state else MediaPlayerState.ON)
class WifiSpeaker(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay,
FeaturePause, FeatureStop, FeatureNextTrack,
FeaturePreviousTrack, FeatureSoundMode, FeatureState):
"""WiFi speaker, aka XiaoAI sound speaker."""
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the device."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
self._attr_media_content_type = MediaType.MUSIC
class Television(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay, FeaturePause,
FeatureStop, FeatureNextTrack, FeaturePreviousTrack,
FeatureSoundMode, FeatureState, FeatureSource, FeatureTurnOn,
FeatureTurnOff):
"""Television"""
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
"""Initialize the device."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_device_class = MediaPlayerDeviceClass.TV
self._attr_media_content_type = MediaType.VIDEO

View File

@ -71,10 +71,12 @@ SUPPORTED_PLATFORMS: list = [
'button', 'button',
'climate', 'climate',
'cover', 'cover',
'device_tracker',
'event', 'event',
'fan', 'fan',
'humidifier', 'humidifier',
'light', 'light',
'media_player',
'notify', 'notify',
'number', 'number',
'select', 'select',
@ -85,6 +87,11 @@ SUPPORTED_PLATFORMS: list = [
'water_heater', 'water_heater',
] ]
UNSUPPORTED_MODELS: list = [
'chuangmi.ir.v2',
'xiaomi.router.rd03'
]
DEFAULT_CLOUD_SERVER: str = 'cn' DEFAULT_CLOUD_SERVER: str = 'cn'
CLOUD_SERVERS: dict = { CLOUD_SERVERS: dict = {
'cn': '中国大陆', 'cn': '中国大陆',
@ -113,6 +120,10 @@ INTEGRATION_LANGUAGES = {
'zh-Hant': '繁體中文' 'zh-Hant': '繁體中文'
} }
DEFAULT_COVER_CLOSED_POSITION: int = 0
MIN_COVER_CLOSED_POSITION: int = 0
MAX_COVER_CLOSED_POSITION: int = 5
DEFAULT_CTRL_MODE: str = 'auto' DEFAULT_CTRL_MODE: str = 'auto'
# Registered in Xiaomi OAuth 2.0 Service # Registered in Xiaomi OAuth 2.0 Service

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} Geräte offline:** \n{message}", "device_list_offline": "\n**{count} Geräte offline:** \n{message}",
"network_status_online": "Online", "network_status_online": "Online",
"network_status_offline": "Offline", "network_status_offline": "Offline",
"central_state_changed_title": "Verbindungsstatus des Zentral-Gateways",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** Lokale Verbindungsstrecke des Zentral-Gateways: {conn_status}",
"central_state_connected": "verbunden",
"central_state_disconnected": "getrennt",
"device_exec_error": "Fehler bei der Ausführung" "device_exec_error": "Fehler bei der Ausführung"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} devices offline:** \n{message}", "device_list_offline": "\n**{count} devices offline:** \n{message}",
"network_status_online": "Online", "network_status_online": "Online",
"network_status_offline": "Offline", "network_status_offline": "Offline",
"central_state_changed_title": "Central Hub Gateway Connection Status",
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** local connection to Xiaomi central hub gateway: {conn_status}",
"central_state_connected": "Connected",
"central_state_disconnected": "Disconnected",
"device_exec_error": "Execution error" "device_exec_error": "Execution error"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}", "device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}",
"network_status_online": "En línea", "network_status_online": "En línea",
"network_status_offline": "Desconectado", "network_status_offline": "Desconectado",
"central_state_changed_title": "Estado de conexión de la puerta de enlace central",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** enlace de conexión local de la puerta de enlace central: {conn_status}",
"central_state_connected": "conectado",
"central_state_disconnected": "desconectado",
"device_exec_error": "Error de ejecución" "device_exec_error": "Error de ejecución"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} appareils hors ligne :** \n{message}", "device_list_offline": "\n**{count} appareils hors ligne :** \n{message}",
"network_status_online": "En ligne", "network_status_online": "En ligne",
"network_status_offline": "Hors ligne", "network_status_offline": "Hors ligne",
"central_state_changed_title": "État de connexion de la passerelle centrale",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** liaison de connexion locale de la passerelle centrale : {conn_status}",
"central_state_connected": "connecté",
"central_state_disconnected": "déconnecté",
"device_exec_error": "Erreur d'exécution" "device_exec_error": "Erreur d'exécution"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} dispositivi offline:** \n{message}", "device_list_offline": "\n**{count} dispositivi offline:** \n{message}",
"network_status_online": "Online", "network_status_online": "Online",
"network_status_offline": "Offline", "network_status_offline": "Offline",
"central_state_changed_title": "Stato di connessione del gateway centrale",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** collegamento locale del gateway centrale: {conn_status}",
"central_state_connected": "connesso",
"central_state_disconnected": "disconnesso",
"device_exec_error": "Errore di esecuzione" "device_exec_error": "Errore di esecuzione"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} デバイスがオフライン:** \n{message}", "device_list_offline": "\n**{count} デバイスがオフライン:** \n{message}",
"network_status_online": "オンライン", "network_status_online": "オンライン",
"network_status_offline": "オフライン", "network_status_offline": "オフライン",
"central_state_changed_title": "中枢ゲートウェイ接続ステータス",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** 中枢ゲートウェイのローカル接続リンク: {conn_status}",
"central_state_connected": "接続済み",
"central_state_disconnected": "切断されました",
"device_exec_error": "実行エラー" "device_exec_error": "実行エラー"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} apparaten offline:** \n{message}", "device_list_offline": "\n**{count} apparaten offline:** \n{message}",
"network_status_online": "Online", "network_status_online": "Online",
"network_status_offline": "Offline", "network_status_offline": "Offline",
"central_state_changed_title": "Verbindingsstatus van centrale gateway",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** Lokale verbinding van centrale gateway: {conn_status}",
"central_state_connected": "verbonden",
"central_state_disconnected": "verbinding verbroken",
"device_exec_error": "Uitvoeringsfout" "device_exec_error": "Uitvoeringsfout"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}", "device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
"network_status_online": "online", "network_status_online": "online",
"network_status_offline": "offline", "network_status_offline": "offline",
"central_state_changed_title": "Status de conexão do gateway central",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** conexão local do gateway central: {conn_status}",
"central_state_connected": "conectado",
"central_state_disconnected": "desconectado",
"device_exec_error": "Erro na execução" "device_exec_error": "Erro na execução"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} dispositivos offline**: \n{message}", "device_list_offline": "\n**{count} dispositivos offline**: \n{message}",
"network_status_online": "Online", "network_status_online": "Online",
"network_status_offline": "Offline", "network_status_offline": "Offline",
"central_state_changed_title": "Estado da ligação do gateway central",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** ligação local do gateway central: {conn_status}",
"central_state_connected": "ligado",
"central_state_disconnected": "desligado",
"device_exec_error": "Erro de execução" "device_exec_error": "Erro de execução"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} устройств недоступно:** \n{message}", "device_list_offline": "\n**{count} устройств недоступно:** \n{message}",
"network_status_online": "В сети", "network_status_online": "В сети",
"network_status_offline": "Не в сети", "network_status_offline": "Не в сети",
"central_state_changed_title": "Статус подключения центрального шлюза",
"central_state_changed": "**{nick_name}({uid}, {cloud_server})** локальное подключение центрального шлюза: {conn_status}",
"central_state_connected": "подключено",
"central_state_disconnected": "разъединено",
"device_exec_error": "Ошибка выполнения" "device_exec_error": "Ошибка выполнения"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} 个设备离线**: \n{message}", "device_list_offline": "\n**{count} 个设备离线**: \n{message}",
"network_status_online": "在线", "network_status_online": "在线",
"network_status_offline": "离线", "network_status_offline": "离线",
"central_state_changed_title": "中枢网关连接状态",
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** 中枢网关本地连接链路: {conn_status}",
"central_state_connected": "已连接",
"central_state_disconnected": "断连",
"device_exec_error": "执行错误" "device_exec_error": "执行错误"
} }
}, },

View File

@ -99,6 +99,10 @@
"device_list_offline": "\n**{count} 個設備離線:** \n{message}", "device_list_offline": "\n**{count} 個設備離線:** \n{message}",
"network_status_online": "在線", "network_status_online": "在線",
"network_status_offline": "離線", "network_status_offline": "離線",
"central_state_changed_title": "中枢網關連接狀態",
"central_state_changed":"**{nick_name}({uid}, {cloud_server})** 中枢網關本地連接鏈路: {conn_status}",
"central_state_connected": "已連接",
"central_state_disconnected": "断連",
"device_exec_error": "執行錯誤" "device_exec_error": "執行錯誤"
} }
}, },

View File

@ -63,7 +63,8 @@ from .common import MIoTMatcher, slugify_did
from .const import ( from .const import (
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN, DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL, MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL) OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL,
DEFAULT_COVER_CLOSED_POSITION)
from .miot_cloud import MIoTHttpClient, MIoTOauthClient from .miot_cloud import MIoTHttpClient, MIoTOauthClient
from .miot_error import MIoTClientError, MIoTErrorCode from .miot_error import MIoTClientError, MIoTErrorCode
from .miot_mips import ( from .miot_mips import (
@ -150,7 +151,7 @@ class MIoTClient:
# Device list update timestamp # Device list update timestamp
_device_list_update_ts: int _device_list_update_ts: int
_sub_source_list: dict[str, str] _sub_source_list: dict[str, Optional[str]]
_sub_tree: MIoTMatcher _sub_tree: MIoTMatcher
_sub_device_state: dict[str, MipsDeviceState] _sub_device_state: dict[str, MipsDeviceState]
@ -486,6 +487,11 @@ class MIoTClient:
def display_binary_bool(self) -> bool: def display_binary_bool(self) -> bool:
return self._display_binary_bool return self._display_binary_bool
@property
def cover_closed_position(self) -> int:
return self._entry_data.get('cover_closed_position',
DEFAULT_COVER_CLOSED_POSITION)
@display_devices_changed_notify.setter @display_devices_changed_notify.setter
def display_devices_changed_notify(self, value: list[str]) -> None: def display_devices_changed_notify(self, value: list[str]) -> None:
if set(value) == set(self._display_devs_notify): if set(value) == set(self._display_devs_notify):
@ -620,7 +626,7 @@ class MIoTClient:
# Priority local control # Priority local control
if self._ctrl_mode == CtrlMode.AUTO: if self._ctrl_mode == CtrlMode.AUTO:
# Gateway control # Gateway control
device_gw: dict = self._device_list_gateway.get(did, None) device_gw = self._device_list_gateway.get(did, None)
if ( if (
device_gw and device_gw.get('online', False) device_gw and device_gw.get('online', False)
and device_gw.get('specv2_access', False) and device_gw.get('specv2_access', False)
@ -629,11 +635,14 @@ class MIoTClient:
mips = self._mips_local.get(device_gw['group_id'], None) mips = self._mips_local.get(device_gw['group_id'], None)
if mips is None: if mips is None:
_LOGGER.error( _LOGGER.error(
'no gw route, %s, try control throw cloud', 'no gateway route, %s, try control through cloud',
device_gw) device_gw)
else: else:
result = await mips.set_prop_async( result = await mips.set_prop_async(
did=did, siid=siid, piid=piid, value=value) did=did, siid=siid, piid=piid, value=value)
_LOGGER.debug(
'gateway set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
rc = (result or {}).get( rc = (result or {}).get(
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) 'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
if rc in [0, 1]: if rc in [0, 1]:
@ -641,12 +650,13 @@ class MIoTClient:
raise MIoTClientError( raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Lan control # Lan control
device_lan: dict = self._device_list_lan.get(did, None) device_lan = self._device_list_lan.get(did, None)
if device_lan and device_lan.get('online', False): if device_lan and device_lan.get('online', False):
result = await self._miot_lan.set_prop_async( result = await self._miot_lan.set_prop_async(
did=did, siid=siid, piid=piid, value=value) did=did, siid=siid, piid=piid, value=value)
_LOGGER.debug( _LOGGER.debug(
'lan set prop, %s, %s, %s -> %s', did, siid, piid, result) 'lan set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
rc = (result or {}).get( rc = (result or {}).get(
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value) 'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
if rc in [0, 1]: if rc in [0, 1]:
@ -657,12 +667,12 @@ class MIoTClient:
# Cloud control # Cloud control
device_cloud = self._device_list_cloud.get(did, None) device_cloud = self._device_list_cloud.get(did, None)
if device_cloud and device_cloud.get('online', False): if device_cloud and device_cloud.get('online', False):
result: list = await self._http.set_prop_async( result = await self._http.set_prop_async(
params=[ params=[
{'did': did, 'siid': siid, 'piid': piid, 'value': value} {'did': did, 'siid': siid, 'piid': piid, 'value': value}
]) ])
_LOGGER.debug( _LOGGER.debug(
'set prop response, %s.%d.%d, %s, result, %s', 'cloud set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result) did, siid, piid, value, result)
if result and len(result) == 1: if result and len(result) == 1:
rc = result[0].get( rc = result[0].get(
@ -746,7 +756,7 @@ class MIoTClient:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
device_gw: dict = self._device_list_gateway.get(did, None) device_gw = self._device_list_gateway.get(did, None)
# Priority local control # Priority local control
if self._ctrl_mode == CtrlMode.AUTO: if self._ctrl_mode == CtrlMode.AUTO:
if ( if (
@ -782,7 +792,7 @@ class MIoTClient:
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Cloud control # Cloud control
device_cloud = self._device_list_cloud.get(did, None) device_cloud = self._device_list_cloud.get(did, None)
if device_cloud.get('online', False): if device_cloud and device_cloud.get('online', False):
result: dict = await self._http.action_async( result: dict = await self._http.action_async(
did=did, siid=siid, aiid=aiid, in_list=in_list) did=did, siid=siid, aiid=aiid, in_list=in_list)
if result: if result:
@ -798,14 +808,15 @@ class MIoTClient:
dids=[did])) dids=[did]))
raise MIoTClientError( raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Show error message # TODO: Show error message
_LOGGER.error( _LOGGER.error(
'client action failed, %s.%d.%d', did, siid, aiid) 'client action failed, %s.%d.%d', did, siid, aiid)
return None return []
def sub_prop( def sub_prop(
self, did: str, handler: Callable[[dict, Any], None], self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, piid: int = None, handler_ctx: Any = None siid: Optional[int] = None, piid: Optional[int] = None,
handler_ctx: Any = None
) -> bool: ) -> bool:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
@ -818,7 +829,9 @@ class MIoTClient:
_LOGGER.debug('client sub prop, %s', topic) _LOGGER.debug('client sub prop, %s', topic)
return True return True
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: def unsub_prop(
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
) -> bool:
topic = ( topic = (
f'{did}/p/' f'{did}/p/'
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
@ -829,7 +842,8 @@ class MIoTClient:
def sub_event( def sub_event(
self, did: str, handler: Callable[[dict, Any], None], self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, eiid: int = None, handler_ctx: Any = None siid: Optional[int] = None, eiid: Optional[int] = None,
handler_ctx: Any = None
) -> bool: ) -> bool:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
@ -841,7 +855,9 @@ class MIoTClient:
_LOGGER.debug('client sub event, %s', topic) _LOGGER.debug('client sub event, %s', topic)
return True return True
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: def unsub_event(
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
) -> bool:
topic = ( topic = (
f'{did}/e/' f'{did}/e/'
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
@ -873,16 +889,7 @@ class MIoTClient:
sub_from = self._sub_source_list.pop(did, None) sub_from = self._sub_source_list.pop(did, None)
# Unsub # Unsub
if sub_from: if sub_from:
if sub_from == 'cloud': self.__unsub_from(sub_from, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif sub_from == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Storage # Storage
await self._storage.save_async( await self._storage.save_async(
domain='miot_devices', domain='miot_devices',
@ -930,6 +937,39 @@ class MIoTClient:
delay_sec, lambda: self._main_loop.create_task( delay_sec, lambda: self._main_loop.create_task(
self.refresh_user_cert_async())) self.refresh_user_cert_async()))
@final
def __unsub_from(self, sub_from: str, did: str) -> None:
mips: Any = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
try:
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
except RuntimeError as e:
if 'Event loop is closed' in str(e):
# Ignore unsub exception when loop is closed
pass
else:
raise
@final
def __sub_from(self, sub_from: str, did: str) -> None:
mips = None
if sub_from == 'cloud':
mips = self._mips_cloud
elif sub_from == 'lan':
mips = self._miot_lan
elif sub_from in self._mips_local:
mips = self._mips_local[sub_from]
if mips is not None:
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
@final @final
def __update_device_msg_sub(self, did: str) -> None: def __update_device_msg_sub(self, did: str) -> None:
if did not in self._device_list_cache: if did not in self._device_list_cache:
@ -956,32 +996,14 @@ class MIoTClient:
and self._device_list_cloud[did].get('online', False) and self._device_list_cloud[did].get('online', False)
): ):
from_new = 'cloud' from_new = 'cloud'
if from_new == from_old: if (from_new == from_old) and (from_new=='cloud' or from_new=='lan'):
# No need to update # No need to update
return return
# Unsub old # Unsub old
if from_old: if from_old:
if from_old == 'cloud': self.__unsub_from(from_old, did)
self._mips_cloud.unsub_prop(did=did)
self._mips_cloud.unsub_event(did=did)
elif from_old == 'lan':
self._miot_lan.unsub_prop(did=did)
self._miot_lan.unsub_event(did=did)
elif from_old in self._mips_local:
mips = self._mips_local[from_old]
mips.unsub_prop(did=did)
mips.unsub_event(did=did)
# Sub new # Sub new
if from_new == 'cloud': self.__sub_from(from_new, did)
self._mips_cloud.sub_prop(did=did, handler=self.__on_prop_msg)
self._mips_cloud.sub_event(did=did, handler=self.__on_event_msg)
elif from_new == 'lan':
self._miot_lan.sub_prop(did=did, handler=self.__on_prop_msg)
self._miot_lan.sub_event(did=did, handler=self.__on_event_msg)
elif from_new in self._mips_local:
mips = self._mips_local[from_new]
mips.sub_prop(did=did, handler=self.__on_prop_msg)
mips.sub_event(did=did, handler=self.__on_event_msg)
self._sub_source_list[did] = from_new self._sub_source_list[did] = from_new
_LOGGER.info( _LOGGER.info(
'device sub changed, %s, from %s to %s', did, from_old, from_new) 'device sub changed, %s, from %s to %s', did, from_old, from_new)
@ -1081,7 +1103,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify() self.__request_show_devices_changed_notify()
@ -1091,10 +1113,12 @@ class MIoTClient:
self, group_id: str, state: bool self, group_id: str, state: bool
) -> None: ) -> None:
_LOGGER.info('local mips state changed, %s, %s', group_id, state) _LOGGER.info('local mips state changed, %s, %s', group_id, state)
mips: MipsLocalClient = self._mips_local.get(group_id, None) mips = self._mips_local.get(group_id, None)
if mips is None: if not mips:
_LOGGER.error( _LOGGER.info(
'local mips state changed, mips not exist, %s', group_id) 'local mips state changed, mips not exist, %s', group_id)
# The connection to the central hub gateway is definitely broken.
self.__show_central_state_changed_notify(False)
return return
if state: if state:
# Connected # Connected
@ -1124,10 +1148,11 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify() self.__request_show_devices_changed_notify()
self.__show_central_state_changed_notify(state)
@final @final
async def __on_miot_lan_state_change(self, state: bool) -> None: async def __on_miot_lan_state_change(self, state: bool) -> None:
@ -1171,7 +1196,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self._device_list_lan = {} self._device_list_lan = {}
@ -1201,7 +1226,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
return return
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1257,7 +1282,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
return return
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1301,9 +1326,8 @@ class MIoTClient:
async def __load_cache_device_async(self) -> None: async def __load_cache_device_async(self) -> None:
"""Load device list from cache.""" """Load device list from cache."""
cache_list: Optional[dict[str, dict]] = await self._storage.load_async( cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
domain='miot_devices', domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
name=f'{self._uid}_{self._cloud_server}', type_=dict) # type: ignore
type_=dict)
if not cache_list: if not cache_list:
self.__show_client_error_notify( self.__show_client_error_notify(
message=self._i18n.translate( message=self._i18n.translate(
@ -1339,6 +1363,11 @@ class MIoTClient:
"""Update cloud devices. """Update cloud devices.
NOTICE: This function will operate the cloud_list NOTICE: This function will operate the cloud_list
""" """
# MIoT cloud service may not publish the online state updating message
# for the BLE device. Assume that all BLE devices are online.
for did, info in cloud_list.items():
if did.startswith('blt.'):
info['online'] = True
for did, info in self._device_list_cache.items(): for did, info in self._device_list_cache.items():
if filter_dids and did not in filter_dids: if filter_dids and did not in filter_dids:
continue continue
@ -1346,7 +1375,7 @@ class MIoTClient:
cloud_state_old: Optional[bool] = self._device_list_cloud.get( cloud_state_old: Optional[bool] = self._device_list_cloud.get(
did, {}).get('online', None) did, {}).get('online', None)
cloud_state_new: Optional[bool] = None cloud_state_new: Optional[bool] = None
device_new: dict = cloud_list.pop(did, None) device_new = cloud_list.pop(did, None)
if device_new: if device_new:
cloud_state_new = device_new.get('online', None) cloud_state_new = device_new.get('online', None)
# Update cache device info # Update cache device info
@ -1371,7 +1400,7 @@ class MIoTClient:
continue continue
info['online'] = state_new info['online'] = state_new
# Call device state changed callback # Call device state changed callback
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1426,8 +1455,7 @@ class MIoTClient:
self, dids: list[str] self, dids: list[str]
) -> None: ) -> None:
_LOGGER.debug('refresh cloud device with dids, %s', dids) _LOGGER.debug('refresh cloud device with dids, %s', dids)
cloud_list: dict[str, dict] = ( cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
await self._http.get_devices_with_dids_async(dids=dids))
if cloud_list is None: if cloud_list is None:
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids) _LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
return return
@ -1466,11 +1494,9 @@ class MIoTClient:
for did, info in self._device_list_cache.items(): for did, info in self._device_list_cache.items():
if did not in filter_dids: if did not in filter_dids:
continue continue
device_old: dict = self._device_list_gateway.get(did, None) device_old = self._device_list_gateway.get(did, None)
gw_state_old = device_old.get(
'online', False) if device_old else False
gw_state_new: bool = False gw_state_new: bool = False
device_new: dict = gw_list.pop(did, None) device_new = gw_list.pop(did, None)
if device_new: if device_new:
# Update gateway device info # Update gateway device info
self._device_list_gateway[did] = { self._device_list_gateway[did] = {
@ -1482,7 +1508,7 @@ class MIoTClient:
device_old['online'] = False device_old['online'] = False
# Update cache group_id # Update cache group_id
info['group_id'] = group_id info['group_id'] = group_id
if gw_state_old == gw_state_new: if not gw_state_new:
continue continue
self.__update_device_msg_sub(did=did) self.__update_device_msg_sub(did=did)
state_old: Optional[bool] = info.get('online', None) state_old: Optional[bool] = info.get('online', None)
@ -1493,7 +1519,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
info['online'] = state_new info['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1518,7 +1544,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1533,7 +1559,7 @@ class MIoTClient:
'refresh gw devices with group_id, %s', group_id) 'refresh gw devices with group_id, %s', group_id)
# Remove timer # Remove timer
self._mips_local_state_changed_timers.pop(group_id, None) self._mips_local_state_changed_timers.pop(group_id, None)
mips: MipsLocalClient = self._mips_local.get(group_id, None) mips = self._mips_local.get(group_id, None)
if not mips: if not mips:
_LOGGER.error('mips not exist, %s', group_id) _LOGGER.error('mips not exist, %s', group_id)
return return
@ -1892,6 +1918,23 @@ class MIoTClient:
self._show_devices_changed_notify_timer = self._main_loop.call_later( self._show_devices_changed_notify_timer = self._main_loop.call_later(
delay_sec, self.__show_devices_changed_notify) delay_sec, self.__show_devices_changed_notify)
@final
def __show_central_state_changed_notify(self, connected: bool) -> None:
conn_status: str = (
self._i18n.translate('miot.client.central_state_connected')
if connected else
self._i18n.translate('miot.client.central_state_disconnected'))
self._persistence_notify(
self.__gen_notify_key('central_state_changed'),
self._i18n.translate('miot.client.central_state_changed_title'),
self._i18n.translate(key='miot.client.central_state_changed',
replace={
'nick_name': self._entry_data.get(
'nick_name', DEFAULT_NICK_NAME),
'uid': self._uid,
'cloud_server': self._cloud_server,
'conn_status': conn_status
}))
@staticmethod @staticmethod
async def get_miot_instance_async( async def get_miot_instance_async(
@ -1900,77 +1943,73 @@ async def get_miot_instance_async(
) -> MIoTClient: ) -> MIoTClient:
if entry_id is None: if entry_id is None:
raise MIoTClientError('invalid entry_id') raise MIoTClientError('invalid entry_id')
miot_client: MIoTClient = None miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None): if miot_client:
_LOGGER.info('instance exist, %s', entry_id) _LOGGER.info('instance exist, %s', entry_id)
miot_client = a return miot_client
else: # Create new instance
if entry_data is None: if not entry_data:
raise MIoTClientError('entry data is None') raise MIoTClientError('entry data is None')
# Get running loop # Get running loop
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
if loop is None: if not loop:
raise MIoTClientError('loop is None') raise MIoTClientError('loop is None')
# MIoT storage # MIoT storage
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get( storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
'miot_storage', None) 'miot_storage', None)
if not storage: if not storage:
storage = MIoTStorage( storage = MIoTStorage(
root_path=entry_data['storage_path'], loop=loop) root_path=entry_data['storage_path'], loop=loop)
hass.data[DOMAIN]['miot_storage'] = storage hass.data[DOMAIN]['miot_storage'] = storage
_LOGGER.info('create miot_storage instance') _LOGGER.info('create miot_storage instance')
global_config: dict = await storage.load_user_config_async( global_config: dict = await storage.load_user_config_async(
uid='global_config', cloud_server='all', uid='global_config', cloud_server='all',
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe']) keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
# MIoT network # MIoT network
network_detect_addr: dict = global_config.get( network_detect_addr: dict = global_config.get('network_detect_addr', {})
'network_detect_addr', {}) network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get( 'miot_network', None)
'miot_network', None) if not network:
if not network: network = MIoTNetwork(
network = MIoTNetwork( ip_addr_list=network_detect_addr.get('ip', []),
ip_addr_list=network_detect_addr.get('ip', []), url_addr_list=network_detect_addr.get('url', []),
url_addr_list=network_detect_addr.get('url', []), refresh_interval=NETWORK_REFRESH_INTERVAL,
refresh_interval=NETWORK_REFRESH_INTERVAL, loop=loop)
loop=loop) hass.data[DOMAIN]['miot_network'] = network
hass.data[DOMAIN]['miot_network'] = network await network.init_async()
await network.init_async() _LOGGER.info('create miot_network instance')
_LOGGER.info('create miot_network instance') # MIoT service
# MIoT service mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
mips_service: Optional[MipsService] = hass.data[DOMAIN].get( 'mips_service', None)
'mips_service', None) if not mips_service:
if not mips_service: aiozc = await zeroconf.async_get_async_instance(hass)
aiozc = await zeroconf.async_get_async_instance(hass) mips_service = MipsService(aiozc=aiozc, loop=loop)
mips_service = MipsService(aiozc=aiozc, loop=loop) hass.data[DOMAIN]['mips_service'] = mips_service
hass.data[DOMAIN]['mips_service'] = mips_service await mips_service.init_async()
await mips_service.init_async() _LOGGER.info('create mips_service instance')
_LOGGER.info('create mips_service instance') # MIoT lan
# MIoT lan miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get( if not miot_lan:
'miot_lan', None) miot_lan = MIoTLan(
if not miot_lan: net_ifs=global_config.get('net_interfaces', []),
miot_lan = MIoTLan(
net_ifs=global_config.get('net_interfaces', []),
network=network,
mips_service=mips_service,
enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop)
hass.data[DOMAIN]['miot_lan'] = miot_lan
_LOGGER.info('create miot_lan instance')
# MIoT client
miot_client = MIoTClient(
entry_id=entry_id,
entry_data=entry_data,
network=network, network=network,
storage=storage,
mips_service=mips_service, mips_service=mips_service,
miot_lan=miot_lan, enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop loop=loop)
) hass.data[DOMAIN]['miot_lan'] = miot_lan
miot_client.persistent_notify = persistent_notify _LOGGER.info('create miot_lan instance')
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client) # MIoT client
_LOGGER.info( miot_client = MIoTClient(
'new miot_client instance, %s, %s', entry_id, entry_data) entry_id=entry_id,
await miot_client.init_async() entry_data=entry_data,
network=network,
storage=storage,
mips_service=mips_service,
miot_lan=miot_lan,
loop=loop
)
miot_client.persistent_notify = persistent_notify
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
_LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)
await miot_client.init_async()
return miot_client return miot_client

View File

@ -59,6 +59,7 @@ import aiohttp
# pylint: disable=relative-beyond-top-level # pylint: disable=relative-beyond-top-level
from .common import calc_group_id from .common import calc_group_id
from .const import ( from .const import (
UNSUPPORTED_MODELS,
DEFAULT_OAUTH2_API_HOST, DEFAULT_OAUTH2_API_HOST,
MIHOME_HTTP_API_TIMEOUT, MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL) OAUTH2_AUTH_URL)
@ -382,7 +383,7 @@ class MIoTHttpClient:
return res_obj['data'] return res_obj['data']
async def get_central_cert_async(self, csr: str) -> Optional[str]: async def get_central_cert_async(self, csr: str) -> str:
if not isinstance(csr, str): if not isinstance(csr, str):
raise MIoTHttpError('invalid params') raise MIoTHttpError('invalid params')
@ -444,6 +445,17 @@ class MIoTHttpClient:
return home_list return home_list
async def get_separated_shared_devices_async(self) -> dict[str, dict]:
separated_shared_devices: dict = {}
device_list: dict[str, dict] = await self.__get_device_list_page_async(
dids=[], start_did=None)
for did, value in device_list.items():
if value['owner'] is not None and ('userid' in value['owner']) and (
'nickname' in value['owner']
):
separated_shared_devices.setdefault(did, value['owner'])
return separated_shared_devices
async def get_homeinfos_async(self) -> dict: async def get_homeinfos_async(self) -> dict:
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/homeroom/gethome', url_path='/app/v2/homeroom/gethome',
@ -499,19 +511,22 @@ class MIoTHttpClient:
): ):
more_list = await self.__get_dev_room_page_async( more_list = await self.__get_dev_room_page_async(
max_id=res_obj['result']['max_id']) max_id=res_obj['result']['max_id'])
for home_id, info in more_list.items(): for device_source in ['homelist', 'share_home_list']:
if home_id not in home_infos['homelist']: for home_id, info in more_list.items():
_LOGGER.info('unknown home, %s, %s', home_id, info) if home_id not in home_infos[device_source]:
continue _LOGGER.info('unknown home, %s, %s', home_id, info)
home_infos['homelist'][home_id]['dids'].extend(info['dids']) continue
for room_id, info in info['room_info'].items(): home_infos[device_source][home_id]['dids'].extend(
home_infos['homelist'][home_id]['room_info'].setdefault( info['dids'])
room_id, { for room_id, info in info['room_info'].items():
'room_id': room_id, home_infos[device_source][home_id][
'room_name': '', 'room_info'].setdefault(
'dids': []}) room_id, {
home_infos['homelist'][home_id]['room_info'][ 'room_id': room_id,
room_id]['dids'].extend(info['dids']) 'room_name': '',
'dids': []})
home_infos[device_source][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
return { return {
'uid': uid, 'uid': uid,
@ -528,6 +543,7 @@ class MIoTHttpClient:
req_data: dict = { req_data: dict = {
'limit': 200, 'limit': 200,
'get_split_device': True, 'get_split_device': True,
'get_third_device': True,
'dids': dids 'dids': dids
} }
if start_did: if start_did:
@ -559,6 +575,10 @@ class MIoTHttpClient:
# were implemented. # were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did) _LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue continue
if model in UNSUPPORTED_MODELS:
_LOGGER.info('ignore unsupported model %s, cloud, %s',
model, did)
continue
device_infos[did] = { device_infos[did] = {
'did': did, 'did': did,
'uid': device.get('uid', None), 'uid': device.get('uid', None),
@ -651,6 +671,25 @@ class MIoTHttpClient:
'room_name': room_name, 'room_name': room_name,
'group_id': group_id 'group_id': group_id
} for did in room_info.get('dids', [])}) } for did in room_info.get('dids', [])})
separated_shared_devices: dict = (
await self.get_separated_shared_devices_async())
if separated_shared_devices:
homes.setdefault('separated_shared_list', {})
for did, owner in separated_shared_devices.items():
owner_id = str(owner['userid'])
homes['separated_shared_list'].setdefault(owner_id,{
'home_name': owner['nickname'],
'uid': owner_id,
'group_id': 'NotSupport',
'room_info': {'shared_device': 'shared_device'}
})
devices.update({did: {
'home_id': owner_id,
'home_name': owner['nickname'],
'room_id': 'shared_device',
'room_name': 'shared_device',
'group_id': 'NotSupport'
}})
dids = sorted(list(devices.keys())) dids = sorted(list(devices.keys()))
results = await self.get_devices_with_dids_async(dids=dids) results = await self.get_devices_with_dids_async(dids=dids)
if results is None: if results is None:

View File

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

View File

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

View File

@ -110,7 +110,6 @@ class MipsServiceData:
version=IPVersion.V4Only) version=IPVersion.V4Only)
if not self.addresses: if not self.addresses:
raise MipsServiceError('invalid addresses') raise MipsServiceError('invalid addresses')
self.addresses.sort()
if not service_info.port: if not service_info.port:
raise MipsServiceError('invalid port') raise MipsServiceError('invalid port')
self.port = service_info.port self.port = service_info.port
@ -226,7 +225,7 @@ class MipsService:
state_change: ServiceStateChange state_change: ServiceStateChange
) -> None: ) -> None:
_LOGGER.debug( _LOGGER.debug(
'mips service state changed, %s, %s, %s', 'mdns discovery changed, %s, %s, %s',
state_change, name, service_type) state_change, name, service_type)
if state_change is ServiceStateChange.Removed: if state_change is ServiceStateChange.Removed:

View File

@ -60,6 +60,7 @@ from typing import Any, Callable, Optional, final, Coroutine
from paho.mqtt.client import ( from paho.mqtt.client import (
MQTT_ERR_SUCCESS, MQTT_ERR_SUCCESS,
MQTT_ERR_NO_CONN,
MQTT_ERR_UNKNOWN, MQTT_ERR_UNKNOWN,
Client, Client,
MQTTv5, MQTTv5,
@ -67,7 +68,7 @@ from paho.mqtt.client import (
# pylint: disable=relative-beyond-top-level # pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher from .common import MIoTMatcher
from .const import MIHOME_MQTT_KEEPALIVE from .const import UNSUPPORTED_MODELS, MIHOME_MQTT_KEEPALIVE
from .miot_error import MIoTErrorCode, MIoTMipsError from .miot_error import MIoTErrorCode, MIoTMipsError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -215,7 +216,7 @@ class _MipsClient(ABC):
MQTT_INTERVAL_S = 1 MQTT_INTERVAL_S = 1
MIPS_QOS: int = 2 MIPS_QOS: int = 2
UINT32_MAX: int = 0xFFFFFFFF UINT32_MAX: int = 0xFFFFFFFF
MIPS_RECONNECT_INTERVAL_MIN: float = 30 MIPS_RECONNECT_INTERVAL_MIN: float = 10
MIPS_RECONNECT_INTERVAL_MAX: float = 600 MIPS_RECONNECT_INTERVAL_MAX: float = 600
MIPS_SUB_PATCH: int = 300 MIPS_SUB_PATCH: int = 300
MIPS_SUB_INTERVAL: float = 1 MIPS_SUB_INTERVAL: float = 1
@ -533,7 +534,7 @@ class _MipsClient(ABC):
return return
try: try:
result, mid = self._mqtt.unsubscribe(topic=topic) result, mid = self._mqtt.unsubscribe(topic=topic)
if result == MQTT_ERR_SUCCESS: if (result == MQTT_ERR_SUCCESS) or (result == MQTT_ERR_NO_CONN):
self.log_debug( self.log_debug(
f'mips unsub internal success, {result}, {mid}, {topic}') f'mips unsub internal success, {result}, {mid}, {topic}')
return return
@ -640,6 +641,7 @@ class _MipsClient(ABC):
if not self._mqtt.is_connected(): if not self._mqtt.is_connected():
return return
self.log_info(f'mips connect, {flags}, {rc}, {props}') self.log_info(f'mips connect, {flags}, {rc}, {props}')
self.__reset_reconnect_time()
self._mqtt_state = True self._mqtt_state = True
self._internal_loop.call_soon( self._internal_loop.call_soon(
self._on_mips_connect, rc, props) self._on_mips_connect, rc, props)
@ -821,7 +823,7 @@ class _MipsClient(ABC):
self._internal_loop.stop() self._internal_loop.stop()
def __get_next_reconnect_time(self) -> float: def __get_next_reconnect_time(self) -> float:
if self._mips_reconnect_interval == 0: if self._mips_reconnect_interval < self.MIPS_RECONNECT_INTERVAL_MIN:
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
else: else:
self._mips_reconnect_interval = min( self._mips_reconnect_interval = min(
@ -829,6 +831,9 @@ class _MipsClient(ABC):
self.MIPS_RECONNECT_INTERVAL_MAX) self.MIPS_RECONNECT_INTERVAL_MAX)
return self._mips_reconnect_interval return self._mips_reconnect_interval
def __reset_reconnect_time(self) -> None:
self._mips_reconnect_interval = 0
class MipsCloudClient(_MipsClient): class MipsCloudClient(_MipsClient):
"""MIoT Pub/Sub Cloud Client.""" """MIoT Pub/Sub Cloud Client."""
@ -989,6 +994,11 @@ class MipsCloudClient(_MipsClient):
handler( handler(
did, MIoTDeviceState.ONLINE if msg['event'] == 'online' did, MIoTDeviceState.ONLINE if msg['event'] == 'online'
else MIoTDeviceState.OFFLINE, ctx) else MIoTDeviceState.OFFLINE, ctx)
if did.startswith('blt.'):
# MIoT cloud may not publish BLE device online/offline state message.
# Do not subscribe BLE device online/offline state.
return True
return self.__reg_broadcast_external( return self.__reg_broadcast_external(
topic=topic, handler=on_state_msg, handler_ctx=handler_ctx) topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)
@ -1173,7 +1183,7 @@ class MipsLocalClient(_MipsClient):
or 'piid' not in msg or 'piid' not in msg
or 'value' not in msg or 'value' not in msg
): ):
# self.log_error(f'on_prop_msg, recv unknown msg, {payload}') self.log_info('unknown prop msg, %s', payload)
return return
if handler: if handler:
self.log_debug('local, on properties_changed, %s', payload) self.log_debug('local, on properties_changed, %s', payload)
@ -1213,10 +1223,13 @@ class MipsLocalClient(_MipsClient):
or 'did' not in msg or 'did' not in msg
or 'siid' not in msg or 'siid' not in msg
or 'eiid' not in msg or 'eiid' not in msg
or 'arguments' not in msg # or 'arguments' not in msg
): ):
# self.log_error(f'on_event_msg, recv unknown msg, {payload}') self.log_info('unknown event msg, %s', payload)
return return
if 'arguments' not in msg:
self.log_info('wrong event msg, %s', payload)
msg['arguments'] = []
if handler: if handler:
self.log_debug('local, on event_occurred, %s', payload) self.log_debug('local, on event_occurred, %s', payload)
handler(msg, ctx) handler(msg, ctx)
@ -1357,6 +1370,9 @@ class MipsLocalClient(_MipsClient):
if name is None or urn is None or model is None: if name is None or urn is None or model is None:
self.log_error(f'invalid device info, {did}, {info}') self.log_error(f'invalid device info, {did}, {info}')
continue continue
if model in UNSUPPORTED_MODELS:
self.log_info(f'unsupported model, {model}, {did}')
continue
device_list[did] = { device_list[did] = {
'did': did, 'did': did,
'name': name, 'name': name,
@ -1414,7 +1430,7 @@ class MipsLocalClient(_MipsClient):
@final @final
@on_dev_list_changed.setter @on_dev_list_changed.setter
def on_dev_list_changed( def on_dev_list_changed(
self, func: Callable[[Any, list[str]], Coroutine] self, func: Optional[Callable[[Any, list[str]], Coroutine]]
) -> None: ) -> None:
"""run in main loop.""" """run in main loop."""
self._on_dev_list_changed = func self._on_dev_list_changed = func

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,266 @@
{
"urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:1": [
{
"iid": 4,
"type": "urn:miot-spec-v2:service:environment:0000780A:090615-ktf:1",
"description": "Environment",
"properties": [
{
"iid": 2,
"type": "urn:miot-spec-v2:property:temperature:00000020:090615-ktf:1",
"description": "Temperature",
"format": "float",
"access": [
"read",
"notify"
],
"unit": "celsius",
"value-range": [
-30,
100,
1
]
}
]
}
],
"urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1": [
{
"iid": 3,
"type": "urn:miot-spec-v2:service:light:00007802:hyd-lyjpro:1",
"description": "Moon Light",
"properties": [
{
"iid": 2,
"type": "urn:miot-spec-v2:property:on:00000006:hyd-lyjpro:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:1:0000C822": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:thermostat:0000784A:tofan-wk01:1",
"description": "Thermostat",
"properties": [
{
"iid": 1,
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
},
{
"iid": 2,
"type": "urn:miot-spec-v2:property:mode-a:00000008:tofan-wk01:1",
"description": "Mode",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"value-list": [
{
"value": 0,
"description": "Auto"
},
{
"value": 1,
"description": "Cool"
},
{
"value": 2,
"description": "Heat"
},
{
"value": 3,
"description": "Fan"
},
{
"value": 4,
"description": "Dry"
}
]
},
{
"iid": 3,
"type": "urn:miot-spec-v2:property:fault:00000009:tofan-wk01:1",
"description": "Device Fault",
"format": "uint8",
"access": [
"read",
"notify"
],
"value-list": [
{
"value": 0,
"description": "No Faults"
}
]
},
{
"iid": 4,
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
"description": "Target Temperature",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"unit": "celsius",
"value-range": [
16,
35,
1
]
}
],
"actions": [
{
"iid": 1,
"type": "urn:miot-spec-v2:action:toggle:00002811:tofan-wk01:1",
"description": "Toggle",
"in": [],
"out": []
}
]
},
{
"iid": 4,
"type": "urn:miot-spec-v2:service:air-conditioner:0000780F:tofan-wk01:1",
"description": "Air Conditioner",
"properties": [
{
"iid": 1,
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
},
{
"iid": 2,
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
"description": "Target Temperature",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"unit": "celsius",
"value-range": [
16,
32,
1
]
},
{
"iid": 3,
"type": "urn:miot-spec-v2:property:fan-level:00000016:tofan-wk01:1",
"description": "Fan Level",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"value-list": [
{
"value": 0,
"description": "Auto"
},
{
"value": 2,
"description": "Low"
},
{
"value": 3,
"description": "Medium"
},
{
"value": 4,
"description": "High"
}
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:viomi-m1:2": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:viomi-m1:1",
"description": "Water Heater",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:viomi-m1:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:xiaomi-yms2:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:xiaomi-yms2:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:zimi-h03:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:zimi-h03:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:zimi-h03:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
]
}

View File

@ -1,3 +1,6 @@
urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:
services:
- '4'
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
properties: properties:
- 9.* - 9.*
@ -5,6 +8,9 @@ urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
- 15.* - 15.*
services: services:
- '10' - '10'
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:
properties:
- '3.2'
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01: urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
properties: properties:
- '5.1' - '5.1'
@ -38,6 +44,7 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
services: services:
- '1' - '1'
- '5' - '5'
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03: urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
services: services:
- '*' - '2'
- '4'

View File

@ -0,0 +1,317 @@
urn:miot-spec-v2:device:air-condition-outlet:0000A045:lumi-mcn04:1:
prop.3.4:
format: uint8
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1:
prop.8.6:
unit: kWh
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:7: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgd1st:1:
prop.3.7:
value-range:
- -30
- 100
- 0.1
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5:
name: voc-density
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:1: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:2: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3:
prop.3.5:
expr: (src_value*6)
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1:
prop.2.3:
name: current-position-a
prop.2.8:
name: target-position-a
prop.2.9:
name: target-position-b
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
prop.2.3:
value-range:
- 0
- 1
- 1
urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1
urn:miot-spec-v2:device:airer:0000A00D:mrbond-m1t:1:
prop.2.3:
name: current-position-a
urn:miot-spec-v2:device:airer:0000A00D:mrbond-m33a:1:
prop.2.3:
name: current-position-a
prop.2.11:
name: current-position-b
urn:miot-spec-v2:device:bath-heater:0000A028:mike-2:1:
prop.3.1:
name: mode-a
prop.3.11:
name: mode-b
prop.3.12:
name: mode-c
urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
prop.5.2:
value-list:
- value: 1
description: low
- value: 128
description: medium
- value: 255
description: high
urn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1:
prop.4.4:
name: fan-level-ventilation
urn:miot-spec-v2:device:curtain:0000A00C:bjkcz-kczble:1:0000D031:
prop.2.2:
name: status-a
urn:miot-spec-v2:device:fan:0000A005:dmaker-p33:1:
prop.2.2:
name: fan-level-a
prop.2.6:
name: fan-level
access:
- read
- write
- notify
urn:miot-spec-v2:device:fan:0000A005:dmaker-p5:1:
prop.2.4:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p43:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:xiaomi-p69:1:0000D062:
prop.2.4:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-sa1:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-v3:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:fan:0000A005:zhimi-za4:3:
prop.2.2:
name: fan-level-a
urn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1:
prop.2.1:
access:
- read
- notify
prop.2.2:
icon: mdi:ip
prop.2.3:
access:
- read
- notify
prop.2.5:
access:
- read
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
prop.2.1:
name: access-mode
access:
- read
- notify
prop.2.2:
name: ip-address
icon: mdi:ip
prop.2.3:
name: wifi-ssid
access:
- read
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:1: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:2: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3:
prop.3.1:
name: on-ventilation
urn:miot-spec-v2:device:kettle:0000A009:yunmi-r3:1:
prop.3.1:
unit: ppm
urn:miot-spec-v2:device:light:0000A001:shhf-sfla10:1:
prop.8.9:
name: wind-reverse
urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:
prop.8.11:
name: on-a
urn:miot-spec-v2:device:light:0000A001:shhf-sflt11:1:0000C802:
prop.11.14:
name: on-power
urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1:
prop.2.1004:
name: contact-state
value-list:
- value: 0
description: open
- value: 1
description: closed
expr: src_value!=1
urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:
prop.3.2:
access:
- read
- notify
unit: mV
urn:miot-spec-v2:device:motor-controller:0000A01D:adp-adswb4:1:0000C837:
prop.2.1:
name: motor-switch
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2:
prop.2.8:
name: people-number
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824:
prop.2.6:
unit: cm
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-hb01:2:0000C824:
prop.3.3:
unit: m
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3:
prop.5.1:
expr: round(src_value*6/1000000, 3)
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
prop.2.2:
name: power-consumption
expr: round(src_value/1000, 3)
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2:
prop.2.3:
expr: round(src_value/10, 1)
prop.2.4:
unit: mA
prop.3.2:
expr: round(src_value/10, 1)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:
prop.11.1:
name: power-consumption
expr: round(src_value/100, 2)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1
urn:miot-spec-v2:device:outlet:0000A002:giot-v8icm:1:0000C816:
prop.4.1:
unit: mWh
urn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816:
prop.3.3:
unit: mV
prop.3.4:
unit: mA
urn:miot-spec-v2:device:outlet:0000A002:yutai-fsov8m:1:0000C816:
prop.4.1:
expr: round(src_value/10000, 2)
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:1:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
prop.3.1:
name: electric-power
expr: round(src_value/100, 2)
urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1:1:
prop.2.1:
name: soil-moisture
icon: mdi:watering-can
prop.2.2:
name: soil-ec
icon: mdi:sprout-outline
unit: μS/cm
urn:miot-spec-v2:device:relay:0000A03D:lumi-c2acn01:1:
prop.4.1:
unit: kWh
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
prop.2.1:
name: download-speed
icon: mdi:download
unit: B/s
prop.2.2:
name: upload-speed
icon: mdi:upload
unit: B/s
urn:miot-spec-v2:device:safe-box:0000A042:loock-v1:1:
prop.5.1:
name: contact-state
expr: src_value!=1
urn:miot-spec-v2:device:switch:0000A003:090615-x1tpm:1:0000D042:
prop.27.3:
name: light-on
prop.27.4:
name: light-fan-on
urn:miot-spec-v2:device:switch:0000A003:lxzn-cbcsmj:1:0000D00D:
prop.3.1:
expr: round(src_value/100, 2)
prop.3.2:
expr: round(src_value/1000, 2)
prop.3.3:
expr: round(src_value/10, 1)
urn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:
prop.2.3:
value-list:
- value: 1
description: '1'
- value: 2
description: '2'
- value: 3
description: '3'
- value: 4
description: '4'
- value: 5
description: '5'
- value: 6
description: '6'
- value: 7
description: '7'
- value: 8
description: '8'
- value: 9
description: '9'
- value: 10
description: '10'
- value: 11
description: '11'
- value: 12
description: '12'
- value: 13
description: '13'
- value: 14
description: '14'
- value: 15
description: '15'
- value: 16
description: '16'
urn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm
urn:miot-spec-v2:device:water-purifier:0000A013:yunmi-s20:1:
prop.4.1:
unit: ppm
prop.4.2:
unit: ppm

View File

@ -48,13 +48,13 @@ Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import SensorStateClass from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfEnergy, EntityCategory, LIGHT_LUX, UnitOfEnergy,
UnitOfPower, UnitOfPower, UnitOfElectricCurrent,
UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfTemperature,
UnitOfElectricPotential, UnitOfPressure, PERCENTAGE)
)
# pylint: disable=pointless-string-statement # pylint: disable=pointless-string-statement
"""SPEC_DEVICE_TRANS_MAP """SPEC_DEVICE_TRANS_MAP
@ -96,11 +96,11 @@ from homeassistant.const import (
} }
} }
""" """
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = { SPEC_DEVICE_TRANS_MAP: dict = {
'humidifier': { 'humidifier': {
'required': { 'required': {
'humidifier': { 'humidifier': {
'required': { 'required': {
'properties': { 'properties': {
'on': {'read', 'write'} 'on': {'read', 'write'}
} }
@ -112,7 +112,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
}, },
'optional': { 'optional': {
'environment': { 'environment': {
'required': { 'required': {
'properties': { 'properties': {
'relative-humidity': {'read'} 'relative-humidity': {'read'}
} }
@ -124,7 +124,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'dehumidifier': { 'dehumidifier': {
'required': { 'required': {
'dehumidifier': { 'dehumidifier': {
'required': { 'required': {
'properties': { 'properties': {
'on': {'read', 'write'} 'on': {'read', 'write'}
} }
@ -132,11 +132,11 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'optional': { 'optional': {
'properties': {'mode', 'target-humidity'} 'properties': {'mode', 'target-humidity'}
} }
}, }
}, },
'optional': { 'optional': {
'environment': { 'environment': {
'required': { 'required': {
'properties': { 'properties': {
'relative-humidity': {'read'} 'relative-humidity': {'read'}
} }
@ -148,33 +148,30 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'vacuum': { 'vacuum': {
'required': { 'required': {
'vacuum': { 'vacuum': {
'required': { 'required': {
'actions': {'start-sweep', 'stop-sweeping'}, 'actions': {'start-sweep', 'stop-sweeping'},
}, },
'optional': { 'optional': {
'properties': {'status', 'fan-level'}, 'properties': {'status', 'fan-level'},
'actions': { 'actions': {
'pause-sweeping', 'pause-sweeping', 'continue-sweep', 'stop-and-gocharge'
'continue-sweep',
'stop-and-gocharge'
} }
}, }
} }
}, },
'optional': { 'optional': {
'identify': { 'identify': {
'required': { 'required': {
'actions': {'identify'} 'actions': {'identify'}
} }
}, },
'battery': { 'battery': {
'required': { 'required': {
'properties': { 'properties': {
'battery-level': {'read'} 'battery-level': {'read'}
}, }
} }
}, }
}, },
'entity': 'vacuum' 'entity': 'vacuum'
}, },
@ -190,7 +187,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
}, },
'optional': { 'optional': {
'properties': {'target-humidity'} 'properties': {'target-humidity'}
}, }
} }
}, },
'optional': { 'optional': {
@ -198,10 +195,9 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'required': {}, 'required': {},
'optional': { 'optional': {
'properties': { 'properties': {
'on', 'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
'fan-level', }
'horizontal-swing', }
'vertical-swing'}}
}, },
'environment': { 'environment': {
'required': {}, 'required': {},
@ -219,6 +215,31 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'entity': 'air-conditioner' 'entity': 'air-conditioner'
}, },
'air-condition-outlet': 'air-conditioner', 'air-condition-outlet': 'air-conditioner',
'thermostat': {
'required': {
'thermostat': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'target-temperature', 'mode', 'fan-level', 'temperature'
}
}
}
},
'optional': {
'environment': {
'required': {},
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
}
},
'entity': 'thermostat'
},
'heater': { 'heater': {
'required': { 'required': {
'heater': { 'heater': {
@ -229,7 +250,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
}, },
'optional': { 'optional': {
'properties': {'target-temperature', 'heat-level'} 'properties': {'target-temperature', 'heat-level'}
}, }
} }
}, },
'optional': { 'optional': {
@ -238,9 +259,140 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'optional': { 'optional': {
'properties': {'temperature', 'relative-humidity'} 'properties': {'temperature', 'relative-humidity'}
} }
}, }
}, },
'entity': 'heater' 'entity': 'heater'
},
'bath-heater': {
'required': {
'ptc-bath-heater': {
'required': {
'properties': {
'mode': {'read', 'write'}
}
},
'optional': {
'properties': {'target-temperature', 'temperature'}
}
}
},
'optional': {
'fan-control': {
'required': {},
'optional': {
'properties': {
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
}
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature'}
}
}
},
'entity': 'bath-heater',
},
'electric-blanket': {
'required': {
'electric-blanket': {
'required': {
'properties': {
'on': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'temperature'}
}
}
},
'optional': {},
'entity': 'electric-blanket'
},
'speaker': {
'required': {
'speaker': {
'required': {
'properties': {
'volume': {'read', 'write'}
}
},
'optional': {
'properties': {'mute'}
}
},
'play-control': {
'required': {
'actions': {'play'}
},
'optional': {
'properties': {'playing-state'},
'actions': {'pause', 'stop', 'next', 'previous'}
}
}
},
'optional': {},
'entity': 'wifi-speaker'
},
'television': {
'required': {
'speaker': {
'required': {
'properties': {
'volume': {'read', 'write'}
}
},
'optional': {
'properties': {'mute'}
}
},
'television': {
'required': {
'actions': {'turn-off'}
},
'optional': {
'properties': {'input-control'},
'actions': {'turn-on'}
}
}
},
'optional': {
'play-control': {
'required': {},
'optional': {
'properties': {'playing-state'},
'actions': {'play', 'pause', 'stop', 'next', 'previous'}
}
}
},
'entity': 'television'
},
'watch': {
'required': {
'watch': {
'required': {
'properties': {
'longitude': {'read'},
'latitude': {'read'}
}
},
'optional': {
'properties': {'area-id'}
}
}
},
'optional': {
'battery': {
'required': {
'properties': {
'battery-level': {'read'}
}
}
}
},
'entity': 'device_tracker'
} }
} }
@ -259,11 +411,12 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'events': set<event instance name: str>, 'events': set<event instance name: str>,
'actions': set<action instance name: str> 'actions': set<action instance name: str>
}, },
'entity': str 'entity': str,
'entity_category'?: str
} }
} }
""" """
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { SPEC_SERVICE_TRANS_MAP: dict = {
'light': { 'light': {
'required': { 'required': {
'properties': { 'properties': {
@ -271,16 +424,28 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
} }
}, },
'optional': { 'optional': {
'properties': { 'properties': {'mode', 'brightness', 'color', 'color-temperature'}
'mode', 'brightness', 'color', 'color-temperature'
}
}, },
'entity': 'light' 'entity': 'light'
}, },
'indicator-light': 'light',
'ambient-light': 'light', 'ambient-light': 'light',
'night-light': 'light', 'night-light': 'light',
'white-light': 'light', 'white-light': 'light',
'indicator-light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode',
'brightness',
}
},
'entity': 'light',
'entity_category': EntityCategory.CONFIG
},
'fan': { 'fan': {
'required': { 'required': {
'properties': { 'properties': {
@ -295,6 +460,8 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
}, },
'fan-control': 'fan', 'fan-control': 'fan',
'ceiling-fan': 'fan', 'ceiling-fan': 'fan',
'air-fresh': 'fan',
'air-purifier': 'fan',
'water-heater': { 'water-heater': {
'required': { 'required': {
'properties': { 'properties': {
@ -302,24 +469,37 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
} }
}, },
'optional': { 'optional': {
'properties': {'on', 'temperature', 'target-temperature', 'mode'} 'properties': {'temperature', 'target-temperature', 'mode'}
}, },
'entity': 'water_heater' 'entity': 'water_heater'
}, },
'curtain': { 'curtain': {
'required': { 'required': {
'properties': { 'properties': {
'motor-control': {'write'} 'motor-control': {'write'}
} }
}, },
'optional': { 'optional': {
'properties': { 'properties': {'status', 'current-position', 'target-position'}
'motor-control', 'status', 'current-position', 'target-position'
}
}, },
'entity': 'cover' 'entity': 'cover'
}, },
'window-opener': 'curtain' 'window-opener': 'curtain',
'motor-controller': 'curtain',
'airer': 'curtain',
'air-conditioner': {
'required': {
'properties': {
'on': {'read', 'write'},
'mode': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'target-humidity'}
},
'entity': 'air-conditioner'
}
} }
"""SPEC_PROP_TRANS_MAP """SPEC_PROP_TRANS_MAP
@ -334,121 +514,145 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
'<property instance name>':{ '<property instance name>':{
'device_class': str, 'device_class': str,
'entity': str, 'entity': str,
'optional':{ 'state_class'?: str,
'state_class': str, 'unit_of_measurement'?: str
'unit_of_measurement': str
}
} }
} }
} }
""" """
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { SPEC_PROP_TRANS_MAP: dict = {
'entities': { 'entities': {
'sensor': { 'sensor': {
'format': {'int', 'float'}, 'format': {'int', 'float'},
'access': {'read'} 'access': {'read'}
}, },
'binary_sensor': {
'format': {'bool', 'int'},
'access': {'read'}
},
'switch': { 'switch': {
'format': {'bool'}, 'format': {'bool'},
'access': {'read', 'write'} 'access': {'read', 'write'}
} }
}, },
'properties': { 'properties': {
'submersion-state': {
'device_class': BinarySensorDeviceClass.MOISTURE,
'entity': 'binary_sensor'
},
'contact-state': {
'device_class': BinarySensorDeviceClass.DOOR,
'entity': 'binary_sensor'
},
'occupancy-status': {
'device_class': BinarySensorDeviceClass.OCCUPANCY,
'entity': 'binary_sensor',
},
'temperature': { 'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE, 'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfTemperature.CELSIUS
}, },
'relative-humidity': { 'relative-humidity': {
'device_class': SensorDeviceClass.HUMIDITY, 'device_class': SensorDeviceClass.HUMIDITY,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
}, },
'air-quality-index': { 'air-quality-index': {
'device_class': SensorDeviceClass.AQI, 'device_class': SensorDeviceClass.AQI,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
}, },
'pm2.5-density': { 'pm2.5-density': {
'device_class': SensorDeviceClass.PM25, 'device_class': SensorDeviceClass.PM25,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'pm10-density': { 'pm10-density': {
'device_class': SensorDeviceClass.PM10, 'device_class': SensorDeviceClass.PM10,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'pm1': { 'pm1': {
'device_class': SensorDeviceClass.PM1, 'device_class': SensorDeviceClass.PM1,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'atmospheric-pressure': { 'atmospheric-pressure': {
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE, 'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPressure.PA
}, },
'tvoc-density': { 'tvoc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, 'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
},
'voc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
}, },
'voc-density': 'tvoc-density',
'battery-level': { 'battery-level': {
'device_class': SensorDeviceClass.BATTERY, 'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
}, },
'voltage': { 'voltage': {
'device_class': SensorDeviceClass.VOLTAGE, 'device_class': SensorDeviceClass.VOLTAGE,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfElectricPotential.VOLT
'unit_of_measurement': UnitOfElectricPotential.VOLT },
} 'electric-current': {
'device_class': SensorDeviceClass.CURRENT,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
}, },
'illumination': { 'illumination': {
'device_class': SensorDeviceClass.ILLUMINANCE, 'device_class': SensorDeviceClass.ILLUMINANCE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': LIGHT_LUX
}, },
'no-one-determine-time': { 'no-one-determine-time': {
'device_class': SensorDeviceClass.DURATION, 'device_class': SensorDeviceClass.DURATION,
'entity': 'sensor' 'entity': 'sensor'
}, },
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time',
'electric-power': { 'electric-power': {
'device_class': SensorDeviceClass.POWER, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfPower.WATT
}
}, },
'electric-current': { 'surge-power': {
'device_class': SensorDeviceClass.CURRENT, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
}
}, },
'power-consumption': { 'power-consumption': {
'device_class': SensorDeviceClass.ENERGY, 'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.TOTAL_INCREASING,
'state_class': SensorStateClass.TOTAL_INCREASING, 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
}
}, },
'power': { 'power': {
'device_class': SensorDeviceClass.POWER, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfPower.WATT }
}
},
'total-battery': {
'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor',
'optional': {
'state_class': SensorStateClass.TOTAL_INCREASING,
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
}
},
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time'
} }
} }
@ -466,6 +670,4 @@ SPEC_EVENT_TRANS_MAP: dict[str, str] = {
'doorbell-ring': EventDeviceClass.DOORBELL 'doorbell-ring': EventDeviceClass.DOORBELL
} }
SPEC_ACTION_TRANS_MAP = { SPEC_ACTION_TRANS_MAP = {}
}

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"step": { "step": {
"eula": { "eula": {
"title": "Risikohinweis", "title": "Risikohinweis",
"description": "1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\r\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\r\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet. \r\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\r\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.", "description": "1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\r\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\r\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet.\r\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\r\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.",
"data": { "data": {
"eula": "Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration." "eula": "Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration."
} }
@ -124,7 +124,8 @@
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen", "display_devices_changed_notify": "Gerätestatusänderungen anzeigen",
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren", "update_trans_rules": "Entitätskonvertierungsregeln aktualisieren",
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren", "update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren",
"network_detect_config": "Integrierte Netzwerkkonfiguration" "network_detect_config": "Integrierte Netzwerkkonfiguration",
"cover_closed_position": "Die Position der geschlossenen Vorhänge"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Bestätigen Sie die Konfiguration", "title": "Bestätigen Sie die Konfiguration",
"description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln", "description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nDie Position der geschlossenen Vorhänge:\t{cover_pos_new}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln",
"data": { "data": {
"confirm": "Änderungen bestätigen" "confirm": "Änderungen bestätigen"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Display device status change notifications", "display_devices_changed_notify": "Display device status change notifications",
"update_trans_rules": "Update entity conversion rules", "update_trans_rules": "Update entity conversion rules",
"update_lan_ctrl_config": "Update LAN control configuration", "update_lan_ctrl_config": "Update LAN control configuration",
"network_detect_config": "Integrated Network Configuration" "network_detect_config": "Integrated network configuration",
"cover_closed_position": "Cover closed position"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirm Configuration", "title": "Confirm Configuration",
"description": "Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\r\nThe integration will reload using the updated configuration.\r\n\r\nIntegration Language: \t{lang_new}\r\nNickname: \t{nick_name_new}\r\nDebug mode for action: \t{action_debug}\r\nHide non-standard created entities: \t{hide_non_standard_entities}\r\nDisplay device status change notifications:\t{display_devices_changed_notify}\r\nDevice Changes: \tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\r\nTransformation rules change: \tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules", "description": "Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\r\nThe integration will reload using the updated configuration.\r\n\r\nIntegration Language:\t{lang_new}\r\nNickname:\t{nick_name_new}\r\nDebug mode for action:\t{action_debug}\r\nHide non-standard created entities:\t{hide_non_standard_entities}\r\nCover closed position:\t{cover_pos_new}\r\nDisplay device status change notifications:\t{display_devices_changed_notify}\r\nDevice Changes:\tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\r\nTransformation rules change:\tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules",
"data": { "data": {
"confirm": "Confirm the change" "confirm": "Confirm the change"
} }

View File

@ -4,7 +4,7 @@
"step": { "step": {
"eula": { "eula": {
"title": "Aviso de riesgo", "title": "Aviso de riesgo",
"description": "1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\r\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\r\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\r\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**. \r\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.", "description": "1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\r\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\r\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\r\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**.\r\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.",
"data": { "data": {
"eula": "He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración." "eula": "He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración."
} }
@ -124,7 +124,8 @@
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo", "display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo",
"update_trans_rules": "Actualizar reglas de conversión de entidad", "update_trans_rules": "Actualizar reglas de conversión de entidad",
"update_lan_ctrl_config": "Actualizar configuración de control LAN", "update_lan_ctrl_config": "Actualizar configuración de control LAN",
"network_detect_config": "Configuración de Red Integrada" "network_detect_config": "Configuración de Red Integrada",
"cover_closed_position": "La posición de las cortinas cerradas"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirmar configuración", "title": "Confirmar configuración",
"description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas", "description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nLa posición de las cortinas cerradas:\t{cover_pos_new}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas",
"data": { "data": {
"confirm": "Confirmar modificación" "confirm": "Confirmar modificación"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil", "display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil",
"update_trans_rules": "Mettre à jour les règles de conversion d'entités", "update_trans_rules": "Mettre à jour les règles de conversion d'entités",
"update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN", "update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN",
"network_detect_config": "Configuration Réseau Intégrée" "network_detect_config": "Configuration Réseau Intégrée",
"cover_closed_position": "La position des rideaux fermés"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirmer la configuration", "title": "Confirmer la configuration",
"description": "**{nick_name}** Bonjour ! Veuillez confirmer les dernières informations de configuration et cliquer sur \"Soumettre\".\r\nL'intégration rechargera avec la nouvelle configuration.\r\n\r\nLangue d'intégration : {lang_new}\r\nPseudo utilisateur : {nick_name_new}\r\nMode de débogage d'action : {action_debug}\r\nMasquer les entités générées non standard : {hide_non_standard_entities}\r\nAfficher les notifications de changement d'état de l'appareil:\t{display_devices_changed_notify}\r\nModifications des appareils : Ajouter **{devices_add}** appareils, supprimer **{devices_remove}** appareils\r\nModifications des règles de conversion : **{trans_rules_count}** règles au total, mise à jour de **{trans_rules_count_success}** règles", "description": "**{nick_name}** Bonjour ! Veuillez confirmer les dernières informations de configuration et cliquer sur \"Soumettre\".\r\nL'intégration rechargera avec la nouvelle configuration.\r\n\r\nLangue d'intégration : {lang_new}\r\nPseudo utilisateur : {nick_name_new}\r\nMode de débogage d'action : {action_debug}\r\nMasquer les entités générées non standard : {hide_non_standard_entities}\r\nLa position des rideaux fermés:\t{cover_pos_new}\r\nAfficher les notifications de changement d'état de l'appareil:\t{display_devices_changed_notify}\r\nModifications des appareils : Ajouter **{devices_add}** appareils, supprimer **{devices_remove}** appareils\r\nModifications des règles de conversion : **{trans_rules_count}** règles au total, mise à jour de **{trans_rules_count_success}** règles",
"data": { "data": {
"confirm": "Confirmer la modification" "confirm": "Confirmer la modification"
} }

View File

@ -113,7 +113,7 @@
}, },
"config_options": { "config_options": {
"title": "Opzioni di Configurazione", "title": "Opzioni di Configurazione",
"description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.", "description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\nID istanza di integrazione: {instance_id}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.",
"data": { "data": {
"integration_language": "Lingua dell'Integrazione", "integration_language": "Lingua dell'Integrazione",
"update_user_info": "Aggiorna le informazioni dell'utente", "update_user_info": "Aggiorna le informazioni dell'utente",
@ -124,7 +124,8 @@
"display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo", "display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo",
"update_trans_rules": "Aggiorna le regole di conversione delle entità", "update_trans_rules": "Aggiorna le regole di conversione delle entità",
"update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN", "update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN",
"network_detect_config": "Configurazione di Rete Integrata" "network_detect_config": "Configurazione di Rete Integrata",
"cover_closed_position": "La posizione delle tende chiuse"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Conferma Configurazione", "title": "Conferma Configurazione",
"description": "Ciao **{nick_name}**, si prega di confermare le informazioni di configurazione più recenti e poi fare clic su INVIA.\r\nL'integrazione verrà ricaricata utilizzando la configurazione aggiornata.\r\n\r\nLingua dell'Integrazione: \t{lang_new}\r\nSoprannome: \t{nick_name_new}\r\nModalità di debug per azione: \t{action_debug}\r\nNascondi entità create non standard: \t{hide_non_standard_entities}\r\nMostra notifiche di cambio stato del dispositivo:\t{display_devices_changed_notify}\r\nCambiamenti del Dispositivo: \tAggiungi **{devices_add}** dispositivi, Rimuovi **{devices_remove}** dispositivi\r\nCambiamenti delle regole di trasformazione: \tCi sono un totale di **{trans_rules_count}** regole, e aggiornate **{trans_rules_count_success}** regole", "description": "Ciao **{nick_name}**, si prega di confermare le informazioni di configurazione più recenti e poi fare clic su INVIA.\r\nL'integrazione verrà ricaricata utilizzando la configurazione aggiornata.\r\n\r\nLingua dell'Integrazione:\t{lang_new}\r\nSoprannome:\t{nick_name_new}\r\nModalità di debug per azione:\t{action_debug}\r\nNascondi entità create non standard:\t{hide_non_standard_entities}\r\nLa posizione delle tende chiuse:\t{cover_pos_new}\r\nMostra notifiche di cambio stato del dispositivo:\t{display_devices_changed_notify}\r\nCambiamenti del Dispositivo:\tAggiungi **{devices_add}** dispositivi, Rimuovi **{devices_remove}** dispositivi\r\nCambiamenti delle regole di trasformazione:\tCi sono un totale di **{trans_rules_count}** regole, e aggiornate **{trans_rules_count_success}** regole",
"data": { "data": {
"confirm": "Conferma la modifica" "confirm": "Conferma la modifica"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "デバイスの状態変化通知を表示", "display_devices_changed_notify": "デバイスの状態変化通知を表示",
"update_trans_rules": "エンティティ変換ルールを更新する", "update_trans_rules": "エンティティ変換ルールを更新する",
"update_lan_ctrl_config": "LAN制御構成を更新する", "update_lan_ctrl_config": "LAN制御構成を更新する",
"network_detect_config": "統合ネットワーク構成" "network_detect_config": "統合ネットワーク構成",
"cover_closed_position": "カーテンを閉じた位置"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "構成を確認する", "title": "構成を確認する",
"description": "**{nick_name}** さん、こんにちは! 最新の構成情報を確認してください。[送信] をクリックして、更新された構成を使用して再度読み込みます。\r\n\r\n統合言語\t{lang_new}\r\nユーザー名\t{nick_name_new}\r\nAction デバッグモード:\t{action_debug}\r\n非標準生成エンティティを非表示にする\t{hide_non_standard_entities}\r\nデバイスの状態変化通知を表示:\t{display_devices_changed_notify}\r\nデバイス変更\t追加 **{devices_add}** 個のデバイス、削除 **{devices_remove}** 個のデバイス\r\n変換ルール変更\t合計 **{trans_rules_count}** 個の規則、更新 **{trans_rules_count_success}** 個の規則", "description": "**{nick_name}** さん、こんにちは! 最新の構成情報を確認してください。[送信] をクリックして、更新された構成を使用して再度読み込みます。\r\n\r\n統合言語\t{lang_new}\r\nユーザー名\t{nick_name_new}\r\nAction デバッグモード:\t{action_debug}\r\n非標準生成エンティティを非表示にする\t{hide_non_standard_entities}\r\nカーテンを閉じた位置:\t{cover_pos_new}\r\nデバイスの状態変化通知を表示:\t{display_devices_changed_notify}\r\nデバイス変更\t追加 **{devices_add}** 個のデバイス、削除 **{devices_remove}** 個のデバイス\r\n変換ルール変更\t合計 **{trans_rules_count}** 個の規則、更新 **{trans_rules_count_success}** 個の規則",
"data": { "data": {
"confirm": "変更を確認する" "confirm": "変更を確認する"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven", "display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven",
"update_trans_rules": "Werk entiteitsconversieregels bij", "update_trans_rules": "Werk entiteitsconversieregels bij",
"update_lan_ctrl_config": "Werk LAN controleconfiguratie bij", "update_lan_ctrl_config": "Werk LAN controleconfiguratie bij",
"network_detect_config": "Geïntegreerde Netwerkconfiguratie" "network_detect_config": "Geïntegreerde Netwerkconfiguratie",
"cover_closed_position": "De positie van de gesloten gordijnen"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Bevestig Configuratie", "title": "Bevestig Configuratie",
"description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal: \t{lang_new}\r\nBijnaam: \t{nick_name_new}\r\nDebugmodus voor actie: \t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten: \t{hide_non_standard_entities}\r\nApparaatstatuswijzigingen weergeven:\t{display_devices_changed_notify}\r\nWijzigingen in apparaten: \tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels: \tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt", "description": "Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\r\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\r\n\r\nIntegratietaal:\t{lang_new}\r\nBijnaam:\t{nick_name_new}\r\nDebugmodus voor actie:\t{action_debug}\r\nVerberg niet-standaard gemaakte entiteiten:\t{hide_non_standard_entities}\r\nDe positie van de gesloten gordijnen:\t{cover_pos_new}\r\nApparaatstatuswijzigingen weergeven:\t{display_devices_changed_notify}\r\nWijzigingen in apparaten:\tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\r\nWijzigingen in transformateregels:\tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt",
"data": { "data": {
"confirm": "Bevestig de wijziging" "confirm": "Bevestig de wijziging"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
"update_trans_rules": "Atualizar regras de conversão de entidades", "update_trans_rules": "Atualizar regras de conversão de entidades",
"update_lan_ctrl_config": "Atualizar configuração de controle LAN", "update_lan_ctrl_config": "Atualizar configuração de controle LAN",
"network_detect_config": "Configuração de Rede Integrada" "network_detect_config": "Configuração de Rede Integrada",
"cover_closed_position": "A posição das cortinas fechadas"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirmar Configuração", "title": "Confirmar Configuração",
"description": "Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nApelido:\t{nick_name_new}\r\nModo de depuração para ação:\t{action_debug}\r\nOcultar entidades não padrão criadas:\t{hide_non_standard_entities}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações de Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração nas Regras de Transformação:\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas", "description": "Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nApelido:\t{nick_name_new}\r\nModo de depuração para ação:\t{action_debug}\r\nOcultar entidades não padrão criadas:\t{hide_non_standard_entities}\r\nA posição das cortinas fechadas:\t{cover_pos_new}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações de Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração nas Regras de Transformação:\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas",
"data": { "data": {
"confirm": "Confirmar a mudança" "confirm": "Confirmar a mudança"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo", "display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
"update_trans_rules": "Atualizar regras de conversão de entidades", "update_trans_rules": "Atualizar regras de conversão de entidades",
"update_lan_ctrl_config": "Atualizar configuração de controlo LAN", "update_lan_ctrl_config": "Atualizar configuração de controlo LAN",
"network_detect_config": "Configuração de Rede Integrada" "network_detect_config": "Configuração de Rede Integrada",
"cover_closed_position": "A posição das cortinas fechadas"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirmar Configuração", "title": "Confirmar Configuração",
"description": "Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nAlcunha:\t{nick_name_new}\r\nModo de depuração de ação:\t{action_debug}\r\nOcultar entidades não padrão:\t{hide_non_standard_entities}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações aos Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração das Regras de Transformação:\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas", "description": "Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\r\nA integração será recarregada com a configuração atualizada.\r\n\r\nIdioma da Integração:\t{lang_new}\r\nAlcunha:\t{nick_name_new}\r\nModo de depuração de ação:\t{action_debug}\r\nOcultar entidades não padrão:\t{hide_non_standard_entities}\r\nA posição das cortinas fechadas:\t{cover_pos_new}\r\nExibir notificações de mudança de status do dispositivo:\t{display_devices_changed_notify}\r\nAlterações aos Dispositivos:\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\r\nAlteração das Regras de Transformação:\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas",
"data": { "data": {
"confirm": "Confirmar a alteração" "confirm": "Confirmar a alteração"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства", "display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства",
"update_trans_rules": "Обновить правила преобразования сущностей", "update_trans_rules": "Обновить правила преобразования сущностей",
"update_lan_ctrl_config": "Обновить конфигурацию управления LAN", "update_lan_ctrl_config": "Обновить конфигурацию управления LAN",
"network_detect_config": "Интегрированная Сетевая Конфигурация" "network_detect_config": "Интегрированная Сетевая Конфигурация",
"cover_closed_position": "Положение закрытых штор"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Подтверждение настройки", "title": "Подтверждение настройки",
"description": "**{nick_name}** Здравствуйте! Подтвердите последнюю информацию о настройке и нажмите «Отправить». Интеграция будет перезагружена с использованием обновленных настроек.\r\n\r\nЯзык интеграции:\t{lang_new}\r\nИмя пользователя:\t{nick_name_new}\r\nРежим отладки Action:\t{action_debug}\r\nСкрыть непроизводственные сущности:\t{hide_non_standard_entities}\r\nОтображать уведомления о изменении состояния устройства:\t{display_devices_changed_notify}\r\nИзменение устройства:\tДобавлено **{devices_add}** устройство, удалено **{devices_remove}** устройства\r\nИзменение правил преобразования:\tВсего **{trans_rules_count}** правил, обновлено **{trans_rules_count_success}** правил", "description": "**{nick_name}** Здравствуйте! Подтвердите последнюю информацию о настройке и нажмите «Отправить». Интеграция будет перезагружена с использованием обновленных настроек.\r\n\r\nЯзык интеграции:\t{lang_new}\r\nИмя пользователя:\t{nick_name_new}\r\nРежим отладки Action:\t{action_debug}\r\nСкрыть непроизводственные сущности:\t{hide_non_standard_entities}\r\nПоложение закрытых штор:\t{cover_pos_new}\r\nОтображать уведомления о изменении состояния устройства:\t{display_devices_changed_notify}\r\nИзменение устройства:\tДобавлено **{devices_add}** устройство, удалено **{devices_remove}** устройства\r\nИзменение правил преобразования:\tВсего **{trans_rules_count}** правил, обновлено **{trans_rules_count_success}** правил",
"data": { "data": {
"confirm": "Подтвердить изменения" "confirm": "Подтвердить изменения"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "显示设备状态变化通知", "display_devices_changed_notify": "显示设备状态变化通知",
"update_trans_rules": "更新实体转换规则", "update_trans_rules": "更新实体转换规则",
"update_lan_ctrl_config": "更新局域网控制配置", "update_lan_ctrl_config": "更新局域网控制配置",
"network_detect_config": "集成网络配置" "network_detect_config": "集成网络配置",
"cover_closed_position": "窗帘关闭位置"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "确认配置", "title": "确认配置",
"description": "**{nick_name}** 您好!请确认最新的配置信息,然后点击“提交”。\r\n集成将会使用更新后的配置重新载入。\r\n\r\n集成语言\t{lang_new}\r\n用户昵称\t{nick_name_new}\r\nAction 调试模式:\t{action_debug}\r\n隐藏非标准生成实体\t{hide_non_standard_entities}\r\n显示设备状态变化通知\t{display_devices_changed_notify}\r\n设备变化\t新增 **{devices_add}** 个设备,移除 **{devices_remove}** 个设备\r\n转换规则变化\t共条 **{trans_rules_count}** 规则,更新 **{trans_rules_count_success}** 条规则", "description": "**{nick_name}** 您好!请确认最新的配置信息,然后点击“提交”。\r\n集成将会使用更新后的配置重新载入。\r\n\r\n集成语言\t{lang_new}\r\n用户昵称\t{nick_name_new}\r\nAction 调试模式:\t{action_debug}\r\n隐藏非标准生成实体\t{hide_non_standard_entities}\r\n窗帘关闭位置:\t{cover_pos_new}\r\n显示设备状态变化通知:\t{display_devices_changed_notify}\r\n设备变化\t新增 **{devices_add}** 个设备,移除 **{devices_remove}** 个设备\r\n转换规则变化\t共条 **{trans_rules_count}** 规则,更新 **{trans_rules_count_success}** 条规则",
"data": { "data": {
"confirm": "确认修改" "confirm": "确认修改"
} }

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "顯示設備狀態變化通知", "display_devices_changed_notify": "顯示設備狀態變化通知",
"update_trans_rules": "更新實體轉換規則", "update_trans_rules": "更新實體轉換規則",
"update_lan_ctrl_config": "更新局域網控制配置", "update_lan_ctrl_config": "更新局域網控制配置",
"network_detect_config": "集成網絡配置" "network_detect_config": "集成網絡配置",
"cover_closed_position": "窗簾關閉位置"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "確認配置", "title": "確認配置",
"description": "**{nick_name}** 您好!請確認最新的配置信息,然後點擊“提交”。\r\n集成將會使用更新後的配置重新載入。\r\n\r\n集成語言\t{lang_new}\r\n用戶暱稱\t{nick_name_new}\r\nAction 調試模式:\t{action_debug}\r\n隱藏非標準生成實體\t{hide_non_standard_entities}\r\n顯示設備狀態變化通知\t{display_devices_changed_notify}\r\n設備變化\t新增 **{devices_add}** 個設備,移除 **{devices_remove}** 個設備\r\n轉換規則變化\t共條 **{trans_rules_count}** 規則,更新 **{trans_rules_count_success}** 條規則", "description": "**{nick_name}** 您好!請確認最新的配置信息,然後點擊“提交”。\r\n集成將會使用更新後的配置重新載入。\r\n\r\n集成語言\t{lang_new}\r\n用戶暱稱\t{nick_name_new}\r\nAction 調試模式:\t{action_debug}\r\n隱藏非標準生成實體\t{hide_non_standard_entities}\r\n窗簾關閉位置:\t{cover_pos_new}\r\n顯示設備狀態變化通知:\t{display_devices_changed_notify}\r\n設備變化\t新增 **{devices_add}** 個設備,移除 **{devices_remove}** 個設備\r\n轉換規則變化\t共條 **{trans_rules_count}** 規則,更新 **{trans_rules_count_success}** 條規則",
"data": { "data": {
"confirm": "確認修改" "confirm": "確認修改"
} }

View File

@ -47,29 +47,26 @@ Vacuum entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional from typing import Any, Optional
import re
import logging import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (StateVacuumEntity,
StateVacuumEntity, VacuumEntityFeature)
VacuumEntityFeature
)
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
from .miot.miot_spec import ( from .miot.miot_spec import (MIoTSpecAction, MIoTSpecProperty)
MIoTSpecAction,
MIoTSpecProperty)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id] config_entry.entry_id]
@ -99,10 +96,12 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
_status_map: Optional[dict[int, str]] _status_map: Optional[dict[int, str]]
_fan_level_map: Optional[dict[int, str]] _fan_level_map: Optional[dict[int, str]]
def __init__( _device_name: str
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
) -> None: def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._device_name = miot_device.name
self._attr_supported_features = VacuumEntityFeature(0) self._attr_supported_features = VacuumEntityFeature(0)
self._prop_status = None self._prop_status = None
@ -121,21 +120,21 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'status': if prop.name == 'status':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('invalid status value_list, %s',
'invalid status value_list, %s', self.entity_id) self.entity_id)
continue continue
self._status_map = prop.value_list.to_map() self._status_map = prop.value_list.to_map()
self._attr_supported_features |= VacuumEntityFeature.STATE
self._prop_status = prop self._prop_status = prop
elif prop.name == 'fan-level': elif prop.name == 'fan-level':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('invalid fan-level value_list, %s',
'invalid fan-level value_list, %s', self.entity_id) self.entity_id)
continue continue
self._fan_level_map = prop.value_list.to_map() self._fan_level_map = prop.value_list.to_map()
self._attr_fan_speed_list = list(self._fan_level_map.values()) self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop self._prop_fan_level = prop
elif prop.name == 'battery-level': elif prop.name == 'battery-level':
self._attr_supported_features |= VacuumEntityFeature.BATTERY self._attr_supported_features |= VacuumEntityFeature.BATTERY
self._prop_battery_level = prop self._prop_battery_level = prop
@ -155,16 +154,24 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
elif action.name == 'stop-and-gocharge': elif action.name == 'stop-and-gocharge':
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
self._action_stop_and_gocharge = action self._action_stop_and_gocharge = action
elif action.name == 'identify': elif action.name == 'identify':
self._attr_supported_features |= VacuumEntityFeature.LOCATE self._attr_supported_features |= VacuumEntityFeature.LOCATE
self._action_identify = action self._action_identify = action
async def async_start(self) -> None: async def async_start(self) -> None:
"""Start or resume the cleaning task.""" """Start or resume the cleaning task."""
if self.state.lower() in ['paused', '暂停中']: try: # VacuumActivity is introduced in HA core 2025.1.0
await self.action_async(action=self._action_continue_sweep) # pylint: disable=import-outside-toplevel
return from homeassistant.components.vacuum import VacuumActivity
if (self.activity
== VacuumActivity.PAUSED) and self._action_continue_sweep:
await self.action_async(action=self._action_continue_sweep)
return
except ImportError:
if self.state and (self.state in {'paused', 'pause'
}) and self._action_continue_sweep:
await self.action_async(action=self._action_continue_sweep)
return
await self.action_async(action=self._action_start_sweep) await self.action_async(action=self._action_start_sweep)
async def async_stop(self, **kwargs: Any) -> None: async def async_stop(self, **kwargs: Any) -> None:
@ -179,31 +186,92 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
"""Set the vacuum cleaner to return to the dock.""" """Set the vacuum cleaner to return to the dock."""
await self.action_async(action=self._action_stop_and_gocharge) await self.action_async(action=self._action_stop_and_gocharge)
async def async_clean_spot(self, **kwargs: Any) -> None:
"""Perform a spot clean-up."""
async def async_locate(self, **kwargs: Any) -> None: async def async_locate(self, **kwargs: Any) -> None:
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""
await self.action_async(action=self._action_identify) await self.action_async(action=self._action_identify)
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set fan speed.""" """Set fan speed."""
fan_level_value = self.get_map_key(map_=self._fan_level_map,
value=fan_speed)
await self.set_property_async(prop=self._prop_fan_level,
value=fan_level_value)
@property
def name(self) -> Optional[str]:
"""Name of the vacuum entity."""
return self._device_name
@property @property
def state(self) -> Optional[str]: def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner.""" """Return the current state of the vacuum cleaner.
return self.get_map_value(
map_=self._status_map, To fix the HA warning below:
key=self.get_prop_value(prop=self._prop_status)) Detected that custom integration 'xiaomi_home' is setting state
directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum
.Vacuum'>)should implement the 'activity' property and return
its state using the VacuumActivity enum.This will stop working in
Home Assistant 2026.1.
Refer to
https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property
There are only 6 states in VacuumActivity enum. To be compatible with
more constants, try get matching VacuumActivity enum first, return state
string as before if there is no match. In Home Assistant 2026.1, every
state should map to a VacuumActivity enum.
"""
return self.activity
@property
def activity(self) -> Optional[str]:
"""The current vacuum activity."""
status = self.get_prop_value(prop=self._prop_status)
if status is None:
return None
status_value = self.get_map_value(map_=self._status_map, key=status)
if status_value is None:
return None
try:
# pylint: disable=import-outside-toplevel
from homeassistant.components.vacuum import VacuumActivity
status_value = status_value.lower()
status_str = re.sub(r'[^a-z]', '', status_value)
if status_str in {
'charging', 'charged', 'chargingcompleted', 'fullcharge',
'fullpower', 'findchargerpause', 'drying', 'washing',
'wash', 'inthewash', 'inthedry', 'stationworking',
'dustcollecting', 'upgrade', 'upgrading', 'updating'
}:
return VacuumActivity.DOCKED
if status_str in {'paused', 'pause'}:
return VacuumActivity.PAUSED
if status_str in {
'gocharging', 'cleancompletegocharging', 'findchargewash',
'backtowashmop', 'gowash', 'gowashing', 'summon'
}:
return VacuumActivity.RETURNING
if (status_str.find('sweeping')
!= -1) or (status_str.find('mopping')
!= -1) or (status_str in {
'cleaning', 'remoteclean', 'continuesweep',
'busy', 'building', 'buildingmap', 'mapping'
}):
return VacuumActivity.CLEANING
if status_str in {'error', 'breakcharging', 'gochargebreak'}:
return VacuumActivity.ERROR
return VacuumActivity.IDLE
except ImportError:
return status_value
@property @property
def battery_level(self) -> Optional[int]: def battery_level(self) -> Optional[int]:
"""Return the current battery level of the vacuum cleaner.""" """The current battery level of the vacuum cleaner."""
return self.get_prop_value(prop=self._prop_battery_level) return self.get_prop_value(prop=self._prop_battery_level)
@property @property
def fan_speed(self) -> Optional[str]: def fan_speed(self) -> Optional[str]:
"""Return the current fan speed of the vacuum cleaner.""" """The current fan speed of the vacuum cleaner."""
return self.get_map_value( return self.get_map_value(
map_=self._fan_level_map, map_=self._fan_level_map,
key=self.get_prop_value(prop=self._prop_fan_level)) key=self.get_prop_value(prop=self._prop_fan_level))

View File

@ -52,25 +52,22 @@ from typing import Any, Optional
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (STATE_ON, STATE_OFF,
STATE_ON, ATTR_TEMPERATURE,
STATE_OFF, WaterHeaterEntity,
ATTR_TEMPERATURE, WaterHeaterEntityFeature)
WaterHeaterEntity,
WaterHeaterEntityFeature
)
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity from .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
@ -79,8 +76,8 @@ async def async_setup_entry(
new_entities = [] new_entities = []
for miot_device in device_list: for miot_device in device_list:
for data in miot_device.entity_list.get('water_heater', []): for data in miot_device.entity_list.get('water_heater', []):
new_entities.append(WaterHeater( new_entities.append(
miot_device=miot_device, entity_data=data)) WaterHeater(miot_device=miot_device, entity_data=data))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -95,9 +92,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
_mode_map: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(self, miot_device: MIoTDevice,
self, miot_device: MIoTDevice, entity_data: MIoTEntityData entity_data: MIoTEntityData) -> None:
) -> None:
"""Initialize the Water heater.""" """Initialize the Water heater."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_temperature_unit = None self._attr_temperature_unit = None
@ -112,20 +108,17 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
for prop in entity_data.props: for prop in entity_data.props:
# on # on
if prop.name == 'on': if prop.name == 'on':
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
self._prop_on = prop self._prop_on = prop
# temperature # temperature
if prop.name == 'temperature': if prop.name == 'temperature':
if prop.value_range: if not prop.value_range:
if ( _LOGGER.error('invalid temperature value_range format, %s',
self._attr_temperature_unit is None self.entity_id)
and prop.external_unit continue
): if prop.external_unit:
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._prop_temp = prop self._prop_temp = prop
else:
_LOGGER.error(
'invalid temperature value_range format, %s',
self.entity_id)
# target-temperature # target-temperature
if prop.name == 'target-temperature': if prop.name == 'target-temperature':
if not prop.value_range: if not prop.value_range:
@ -135,7 +128,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
continue continue
self._attr_min_temp = prop.value_range.min_ self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_ self._attr_max_temp = prop.value_range.max_
self._attr_precision = prop.value_range.step self._attr_target_temperature_step = prop.value_range.step
if self._attr_temperature_unit is None and prop.external_unit: if self._attr_temperature_unit is None and prop.external_unit:
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= ( self._attr_supported_features |= (
@ -144,17 +137,15 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
# mode # mode
if prop.name == 'mode': if prop.name == 'mode':
if not prop.value_list: if not prop.value_list:
_LOGGER.error( _LOGGER.error('mode value_list is None, %s', self.entity_id)
'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values()) self._attr_operation_list = list(self._mode_map.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop self._prop_mode = prop
if not self._attr_operation_list: if not self._attr_operation_list:
self._attr_operation_list = [STATE_ON] self._attr_operation_list = [STATE_ON]
self._attr_operation_list.append(STATE_OFF) self._attr_operation_list.append(STATE_OFF)
self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the water heater on.""" """Turn the water heater on."""
@ -165,14 +156,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature the water heater should heat water to.""" """Set the target temperature."""
await self.set_property_async( await self.set_property_async(prop=self._prop_target_temp,
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE]) value=kwargs[ATTR_TEMPERATURE])
async def async_set_operation_mode(self, operation_mode: str) -> None: async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set the operation mode of the water heater. """Set the operation mode of the water heater."""
Must be in the operation_list.
"""
if operation_mode == STATE_OFF: if operation_mode == STATE_OFF:
await self.set_property_async(prop=self._prop_on, value=False) await self.set_property_async(prop=self._prop_on, value=False)
return return
@ -180,35 +169,32 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
await self.set_property_async(prop=self._prop_on, value=True) await self.set_property_async(prop=self._prop_on, value=True)
return return
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async( await self.set_property_async(prop=self._prop_on,
prop=self._prop_on, value=True, update=False) value=True,
await self.set_property_async( write_ha_state=False)
prop=self._prop_mode, await self.set_property_async(prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, map_=self._mode_map,
value=operation_mode)) value=operation_mode))
async def async_turn_away_mode_on(self) -> None:
"""Set the water heater to away mode."""
await self.hass.async_add_executor_job(self.turn_away_mode_on)
@property @property
def current_temperature(self) -> Optional[float]: def current_temperature(self) -> Optional[float]:
"""Return the current temperature.""" """The current temperature."""
return self.get_prop_value(prop=self._prop_temp) return (None if self._prop_temp is None else self.get_prop_value(
prop=self._prop_temp))
@property @property
def target_temperature(self) -> Optional[float]: def target_temperature(self) -> Optional[float]:
"""Return the target temperature.""" """The target temperature."""
return self.get_prop_value(prop=self._prop_target_temp) return (None if self._prop_target_temp is None else self.get_prop_value(
prop=self._prop_target_temp))
@property @property
def current_operation(self) -> Optional[str]: def current_operation(self) -> Optional[str]:
"""Return the current mode.""" """The current mode."""
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
return STATE_OFF return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on): if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON return STATE_ON
return self.get_map_value( return (None if self._prop_mode is None else self.get_map_value(
map_=self._mode_map, map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)))
key=self.get_prop_value(prop=self._prop_mode))

View File

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

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### 方法 2: [HACS](https://hacs.xyz/) ### 方法 2: [HACS](https://hacs.xyz/)
HACS > 右上角三个点 > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > 点击 HACS 的 New 或 Available for download 分类下的 Xiaomi Home ,进入集成详情页 > DOWNLOAD 一键从 HACS 安装米家集成:
> 米家集成暂未添加到 HACS 商店,敬请期待。 [![打开您的 Home Assistant 实例并打开 Home Assistant 社区商店内的米家集成。](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)
或者HACS > 在搜索框中输入 **Xiaomi Home** > 点击 **Xiaomi Home** ,进入集成详情页 > DOWNLOAD
### 方法 3通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装 ### 方法 3通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装
@ -47,7 +49,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
[设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录 [设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home) [![打开您的 Home Assistant 实例并开始配置一个新的米家集成实例。](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)
### 添加 MIoT 设备 ### 添加 MIoT 设备
@ -59,7 +61,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录 方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录
[![Open your Home Assistant instance and show an integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) [![打开您的 Home Assistant 实例并显示米家集成。](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)
### 修改配置项 ### 修改配置项
@ -155,6 +157,8 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
转换后的实体为 Event事件参数同时传递给实体的 `_trigger_event` 转换后的实体为 Event事件参数同时传递给实体的 `_trigger_event`
MIoT-Spec-V2 事件的 arguments 字段是事件的参数列表,列表元素代表同服务下属性的 piid 。例如,小米智能无线开关(双开)的 [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057)的 siid=2 无线开关服务下包含 eiid=1014 长按事件,该事件触发时会携带一个 piid=2 的按键类型属性作为事件参数, debug 等级日志会打印 `长按, attributes: {'按键类型': 1}` (日志示例,按键类型为 1 表示右键触发了长按事件)。
- 方法Action - 方法Action
| in输入参数列表 | 转换后的实体 | | in输入参数列表 | 转换后的实体 |
@ -289,39 +293,37 @@ event instance name 下的值表示转换后实体所用的 `_attr_device_class`
### MIoT-Spec-V2 过滤规则 ### MIoT-Spec-V2 过滤规则
`spec_filter.json` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。 `spec_filter.yaml` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。
`spec_filter.json`的格式如下: `spec_filter.yaml`的格式如下:
``` ```yaml
{ <MIoT-Spec-V2 device instance urn without the version field>:
"<MIoT-Spec-V2 device instance>":{ services: list<service_iid: str>
"services": list<service_iid: str>, properties: list<service_iid.property_iid: str>
"properties": list<service_iid.property_iid: str>, events: list<service_iid.event_iid: str>
"events": list<service_iid.event_iid: str>, actions: list<service_iid.action_iid: str>
"actions": list<service_iid.action_iid: str>,
}
}
``` ```
`spec_filter.json` 的键值为 MIoT-Spec-V2 设备实例的 urn 不含版本号“version”字段。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.json` 的键值不需要指定设备实例的版本号。 `spec_filter.yaml` 的键值为 MIoT-Spec-V2 设备实例的 urn 不含版本号“version”字段。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.yaml` 的键值不需要指定设备实例的版本号。
设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。 设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。
示例: 示例:
``` ```yaml
{ urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{ services:
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance. - '*' # 排除所有服务,相当于排除拥有该 MIoT-Spec-V2 的设备。
}, urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { services:
"services": ["3"], # Filter out the service whose iid=3. - '3' # 排除 siid=3 的服务。
"properties": ["4.*"] # Filter out all properties in the service whose iid=4. properties:
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service. - '4.*' # 排除 siid=4 服务的所有属性。
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service. events:
} - '4.1' # 排除 siid=4 服务的 eiid=1 的事件。
} actions:
- '4.1' # 排除 siid=4 服务的 aiid=1 的方法。
``` ```
所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。 所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。
@ -353,7 +355,7 @@ instance code 为 MIoT-Spec-V2 实例代码,格式如下:
``` ```
service:<siid> # 服务 service:<siid> # 服务
service:<siid>:property:<piid> # 属性 service:<siid>:property:<piid> # 属性
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的值 service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引
service:<siid>:event:<eiid> # 事件 service:<siid>:event:<eiid> # 事件
service:<siid>:action:<aiid> # 方法 service:<siid>:action:<aiid> # 方法
``` ```
@ -376,7 +378,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
} }
``` ```
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/translations/` 路径下的 `specv2entity.py`、`spec_filter.json`、`multi_lang.json` 文件的内容,需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则 > 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的任何文件(`spec_filter.yaml`、`spec_modify.yaml`、`multi_lang.json`等),需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则
## 文档 ## 文档

View File

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

View File

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