33 Commits

Author SHA1 Message Date
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
25 changed files with 1711 additions and 429 deletions

View File

@ -1,4 +1,52 @@
# CHANGELOG
## 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)。
@ -7,7 +55,6 @@ CAUTION: v0.3.0 changes the unique_id of some entities. If you check the option
- 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)

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### Method 2: [HACS](https://hacs.xyz/)
HACS > Overflow Menu > Custom repositories > Repository: https://github.com/XiaoMi/ha_xiaomi_home.git & Category or Type: Integration > ADD > Xiaomi Home in New or Available for download section of HACS > DOWNLOAD
One-click installation from HACS:
> Xiaomi Home has not been added to the HACS store as a default yet. It's coming soon.
[![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)
@ -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
[![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
@ -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
[![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
@ -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'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
| 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
`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.
```
{
"<MIoT-Spec-V2 device instance>":{
"services": list<service_iid: str>,
"properties": list<service_iid.property_iid: str>,
"events": list<service_iid.event_iid: str>,
"actions": list<service_iid.action_iid: str>,
}
}
```yaml
<MIoT-Spec-V2 device instance urn without the version field>:
services: list<service_iid: str>
properties: list<service_iid.property_iid: str>
events: list<service_iid.event_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.
Example:
```
{
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance.
},
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"services": ["3"], # Filter out the service whose iid=3.
"properties": ["4.*"] # Filter out all properties in the service whose iid=4.
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service.
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service.
}
}
```yaml
urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
services:
- '*' # 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:
services:
- '3' # Filter out the siid=3 service.
properties:
- '4.*' # Filter out all properties in the siid=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.
@ -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

View File

@ -46,8 +46,9 @@ off Xiaomi or its affiliates' products.
Cover entities for Xiaomi Home.
"""
from __future__ import annotations
import logging
from typing import Any, Optional
import re
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@ -97,7 +98,6 @@ class Cover(MIoTServiceEntity, CoverEntity):
_prop_status: Optional[MIoTSpecProperty]
_prop_status_opening: Optional[list[int]]
_prop_status_closing: Optional[list[int]]
_prop_status_stop: Optional[list[int]]
_prop_status_closed: Optional[list[int]]
_prop_current_position: Optional[MIoTSpecProperty]
_prop_target_position: Optional[MIoTSpecProperty]
@ -122,7 +122,6 @@ class Cover(MIoTServiceEntity, CoverEntity):
self._prop_status = None
self._prop_status_opening = []
self._prop_status_closing = []
self._prop_status_stop = []
self._prop_status_closed = []
self._prop_current_position = None
self._prop_target_position = None
@ -159,13 +158,21 @@ class Cover(MIoTServiceEntity, CoverEntity):
self.entity_id)
continue
for item in prop.value_list.items:
if item.name in {'opening', 'open', 'up'}:
item_str: str = item.name
item_name: str = re.sub(r'[^a-z]', '', item_str)
if item_name in {
'opening', 'open', 'up', 'uping', 'rise', 'rising'
}:
self._prop_status_opening.append(item.value)
elif item.name in {'closing', 'close', 'down'}:
elif item_name in {
'closing', 'close', 'down', 'dowm', 'falling',
'dropping', 'downing', 'lower'
}:
self._prop_status_closing.append(item.value)
elif item.name in {'stop', 'stopped', 'pause'}:
self._prop_status_stop.append(item.value)
elif item.name in {'closed'}:
elif item_name in {
'stopatlowest', 'stoplowerlimit', 'lowerlimitstop',
'floor', 'lowerlimit'
}:
self._prop_status_closed.append(item.value)
self._prop_status = prop
elif prop.name == 'current-position':

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

@ -221,7 +221,7 @@ class Fan(MIoTServiceEntity, FanEntity):
# preset_mode
if preset_mode:
await self.set_property_async(
self._prop_mode,
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=preset_mode))
@ -236,6 +236,9 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan speed."""
if percentage > 0:
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
if self._speed_names:
await self.set_property_async(
prop=self._prop_fan_level,
@ -249,16 +252,13 @@ class Fan(MIoTServiceEntity, FanEntity):
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
else:
await self.set_property_async(prop=self._prop_on, value=False)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
await self.set_property_async(
self._prop_mode,
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=preset_mode))

View File

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

View File

@ -25,7 +25,7 @@
"cryptography",
"psutil"
],
"version": "v0.3.0",
"version": "v0.3.4",
"zeroconf": [
"_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',
'climate',
'cover',
'device_tracker',
'event',
'fan',
'humidifier',
'light',
'media_player',
'notify',
'number',
'select',
@ -85,6 +87,11 @@ SUPPORTED_PLATFORMS: list = [
'water_heater',
]
UNSUPPORTED_MODELS: list = [
'chuangmi.ir.v2',
'xiaomi.router.rd03'
]
DEFAULT_CLOUD_SERVER: str = 'cn'
CLOUD_SERVERS: dict = {
'cn': '中国大陆',

View File

@ -629,11 +629,14 @@ class MIoTClient:
mips = self._mips_local.get(device_gw['group_id'], None)
if mips is None:
_LOGGER.error(
'no gw route, %s, try control throw cloud',
'no gateway route, %s, try control through cloud',
device_gw)
else:
result = await mips.set_prop_async(
did=did, siid=siid, piid=piid, value=value)
_LOGGER.debug(
'gateway set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
rc = (result or {}).get(
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
if rc in [0, 1]:
@ -646,7 +649,8 @@ class MIoTClient:
result = await self._miot_lan.set_prop_async(
did=did, siid=siid, piid=piid, value=value)
_LOGGER.debug(
'lan set prop, %s, %s, %s -> %s', did, siid, piid, result)
'lan set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
rc = (result or {}).get(
'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
if rc in [0, 1]:
@ -662,7 +666,7 @@ class MIoTClient:
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
])
_LOGGER.debug(
'set prop response, %s.%d.%d, %s, result, %s',
'cloud set prop, %s.%d.%d, %s -> %s',
did, siid, piid, value, result)
if result and len(result) == 1:
rc = result[0].get(
@ -1350,6 +1354,11 @@ class MIoTClient:
"""Update cloud devices.
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():
if filter_dids and did not in filter_dids:
continue

View File

@ -59,6 +59,7 @@ import aiohttp
# pylint: disable=relative-beyond-top-level
from .common import calc_group_id
from .const import (
UNSUPPORTED_MODELS,
DEFAULT_OAUTH2_API_HOST,
MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL)
@ -573,6 +574,10 @@ class MIoTHttpClient:
# were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue
if model in UNSUPPORTED_MODELS:
_LOGGER.info('ignore unsupported model %s, cloud, %s',
model, did)
continue
device_infos[did] = {
'did': did,
'uid': device.get('uid', None),

View File

@ -779,8 +779,10 @@ class MIoTDevice:
# pylint: disable=import-outside-toplevel
from homeassistant.const import UnitOfConductivity # type: ignore
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR
except Exception: # pylint: disable=broad-except
unit_map['μS/cm'] = 'μS/cm'
unit_map['mWh'] = 'mWh'
return unit_map.get(spec_unit, None)
@ -1205,10 +1207,9 @@ class MIoTPropertyEntity(Entity):
self._attr_available = miot_device.online
_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,
spec.device_class, self.entity_id, self._value_range,
self._value_list)
spec.device_class, self.entity_id)
@property
def device_info(self) -> Optional[DeviceInfo]:

View File

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

View File

@ -60,6 +60,7 @@ from typing import Any, Callable, Optional, final, Coroutine
from paho.mqtt.client import (
MQTT_ERR_SUCCESS,
MQTT_ERR_NO_CONN,
MQTT_ERR_UNKNOWN,
Client,
MQTTv5,
@ -67,7 +68,7 @@ from paho.mqtt.client import (
# pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher
from .const import MIHOME_MQTT_KEEPALIVE
from .const import UNSUPPORTED_MODELS, MIHOME_MQTT_KEEPALIVE
from .miot_error import MIoTErrorCode, MIoTMipsError
_LOGGER = logging.getLogger(__name__)
@ -215,7 +216,7 @@ class _MipsClient(ABC):
MQTT_INTERVAL_S = 1
MIPS_QOS: int = 2
UINT32_MAX: int = 0xFFFFFFFF
MIPS_RECONNECT_INTERVAL_MIN: float = 30
MIPS_RECONNECT_INTERVAL_MIN: float = 10
MIPS_RECONNECT_INTERVAL_MAX: float = 600
MIPS_SUB_PATCH: int = 300
MIPS_SUB_INTERVAL: float = 1
@ -533,7 +534,7 @@ class _MipsClient(ABC):
return
try:
result, mid = self._mqtt.unsubscribe(topic=topic)
if result == MQTT_ERR_SUCCESS:
if (result == MQTT_ERR_SUCCESS) or (result == MQTT_ERR_NO_CONN):
self.log_debug(
f'mips unsub internal success, {result}, {mid}, {topic}')
return
@ -640,6 +641,7 @@ class _MipsClient(ABC):
if not self._mqtt.is_connected():
return
self.log_info(f'mips connect, {flags}, {rc}, {props}')
self.__reset_reconnect_time()
self._mqtt_state = True
self._internal_loop.call_soon(
self._on_mips_connect, rc, props)
@ -821,7 +823,7 @@ class _MipsClient(ABC):
self._internal_loop.stop()
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
else:
self._mips_reconnect_interval = min(
@ -829,6 +831,9 @@ class _MipsClient(ABC):
self.MIPS_RECONNECT_INTERVAL_MAX)
return self._mips_reconnect_interval
def __reset_reconnect_time(self) -> None:
self._mips_reconnect_interval = 0
class MipsCloudClient(_MipsClient):
"""MIoT Pub/Sub Cloud Client."""
@ -1173,7 +1178,7 @@ class MipsLocalClient(_MipsClient):
or 'piid' 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
if handler:
self.log_debug('local, on properties_changed, %s', payload)
@ -1360,6 +1365,9 @@ class MipsLocalClient(_MipsClient):
if name is None or urn is None or model is None:
self.log_error(f'invalid device info, {did}, {info}')
continue
if model in UNSUPPORTED_MODELS:
self.log_info(f'unsupported model, {model}, {did}')
continue
device_list[did] = {
'did': did,
'name': name,

View File

@ -601,7 +601,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
if value is None:
return None
if self.format_ == int:
return int(value)
return int(round(value))
if self.format_ == float:
return round(value, self.precision)
if self.format_ == bool:
@ -1195,6 +1195,9 @@ class _SpecModify:
def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='unit')
def get_prop_format(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='format')
def get_prop_expr(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='expr')
@ -1518,6 +1521,10 @@ class MIoTSpecParser:
siid=service['iid'], piid=property_['iid'])
if custom_access:
spec_prop.access = custom_access
custom_format = self._spec_modify.get_prop_format(
siid=service['iid'], piid=property_['iid'])
if custom_format:
spec_prop.format_ = custom_format
custom_range = self._spec_modify.get_prop_value_range(
siid=service['iid'], piid=property_['iid'])
if custom_range:

View File

@ -1,4 +1,10 @@
{
"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",
@ -155,6 +161,33 @@
"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",
@ -169,19 +202,89 @@
"service:017:action:001": "右键确认"
}
},
"urn:miot-spec-v2:device:bath-heater:0000A028:yeelink-v10": {
"en": {
"service:003:property:001:valuelist:000": "Idle",
"service:003:property:001:valuelist:001": "Dry"
"urn:miot-spec-v2:device: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:plant-monitor:0000A030:hhcc-v1": {
"en": {
"service:002:property:001": "Soil Moisture"
"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": "минимальная температура цели "
}
},
"zh-Hans": {
"service:002:property:001": "土壤湿度",
"service:002:property:003": "光照强度"
"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

@ -1,4 +1,29 @@
{
"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,
@ -18,5 +43,204 @@
}
]
}
],
"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:1:0000C822": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:thermostat:0000784A:tofan-wk01:1",
"description": "Thermostat",
"properties": [
{
"iid": 1,
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
},
{
"iid": 2,
"type": "urn:miot-spec-v2:property:mode-a:00000008:tofan-wk01:1",
"description": "Mode",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"value-list": [
{
"value": 0,
"description": "Auto"
},
{
"value": 1,
"description": "Cool"
},
{
"value": 2,
"description": "Heat"
},
{
"value": 3,
"description": "Fan"
},
{
"value": 4,
"description": "Dry"
}
]
},
{
"iid": 3,
"type": "urn:miot-spec-v2:property:fault:00000009:tofan-wk01:1",
"description": "Device Fault",
"format": "uint8",
"access": [
"read",
"notify"
],
"value-list": [
{
"value": 0,
"description": "No Faults"
}
]
},
{
"iid": 4,
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
"description": "Target Temperature",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"unit": "celsius",
"value-range": [
16,
35,
1
]
}
],
"actions": [
{
"iid": 1,
"type": "urn:miot-spec-v2:action:toggle:00002811:tofan-wk01:1",
"description": "Toggle",
"in": [],
"out": []
}
]
},
{
"iid": 4,
"type": "urn:miot-spec-v2:service:air-conditioner:0000780F:tofan-wk01:1",
"description": "Air Conditioner",
"properties": [
{
"iid": 1,
"type": "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
},
{
"iid": 2,
"type": "urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1",
"description": "Target Temperature",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"unit": "celsius",
"value-range": [
16,
32,
1
]
},
{
"iid": 3,
"type": "urn:miot-spec-v2:property:fan-level:00000016:tofan-wk01:1",
"description": "Fan Level",
"format": "uint8",
"access": [
"read",
"write",
"notify"
],
"value-list": [
{
"value": 0,
"description": "Auto"
},
{
"value": 2,
"description": "Low"
},
{
"value": 3,
"description": "Medium"
},
{
"value": 4,
"description": "High"
}
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:xiaomi-yms2:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:xiaomi-yms2:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:zimi-h03:1": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:zimi-h03:1",
"description": "Switch",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:zimi-h03:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
]
}

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:
properties:
- 9.*
@ -41,6 +44,7 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
services:
- '1'
- '5'
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
services:
- '*'
- '2'
- '4'

View File

@ -1,3 +1,18 @@
urn:miot-spec-v2:device:air-condition-outlet:0000A045:lumi-mcn04:1:
prop.3.4:
format: uint8
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
@ -6,9 +21,19 @@ urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:d
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:7: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5:
name: voc-density
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:1: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:2: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3:
prop.3.5:
expr: (src_value*6)
urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1:
prop.2.3:
name: current-position-a
@ -23,6 +48,9 @@ urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:
- 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
@ -47,11 +75,38 @@ urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
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.6:
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:
@ -84,12 +139,28 @@ urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:1: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:2: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3
urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3:
prop.3.1:
name: on-ventilation
urn:miot-spec-v2:device:kettle:0000A009:yunmi-r3:1:
prop.3.1:
unit: ppm
urn:miot-spec-v2:device:light:0000A001:shhf-sfla10:1:
prop.8.9:
name: wind-reverse
urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:
prop.8.11:
name: on-a
urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1: # linp.magnet.m1
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:
@ -97,6 +168,10 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:
- read
- notify
unit: mV
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2:
prop.2.8:
name: people-number
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824:
prop.2.6:
unit: cm
@ -112,6 +187,10 @@ 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:
@ -125,11 +204,18 @@ urn:miot-spec-v2:device:outlet:0000A002:cuco-v3: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
@ -154,10 +240,22 @@ urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
name: upload-speed
icon: mdi:upload
unit: B/s
urn:miot-spec-v2:device:safe-box:0000A042:loock-v1:1: # loock.safe.v1
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:

View File

@ -50,18 +50,11 @@ from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory,
LIGHT_LUX,
UnitOfEnergy,
UnitOfPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfTemperature,
UnitOfPressure,
PERCENTAGE
)
from homeassistant.const import (CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
EntityCategory, LIGHT_LUX, UnitOfEnergy,
UnitOfPower, UnitOfElectricCurrent,
UnitOfElectricPotential, UnitOfTemperature,
UnitOfPressure, PERCENTAGE)
# pylint: disable=pointless-string-statement
"""SPEC_DEVICE_TRANS_MAP
@ -161,9 +154,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'optional': {
'properties': {'status', 'fan-level'},
'actions': {
'pause-sweeping',
'continue-sweep',
'stop-and-gocharge'
'pause-sweeping', 'continue-sweep', 'stop-and-gocharge'
}
}
}
@ -204,10 +195,9 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'required': {},
'optional': {
'properties': {
'on',
'fan-level',
'horizontal-swing',
'vertical-swing'}}
'on', 'fan-level', 'horizontal-swing', 'vertical-swing'
}
}
},
'environment': {
'required': {},
@ -235,8 +225,8 @@ SPEC_DEVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {
'target-temperature', 'mode', 'fan-level',
'temperature'}
'target-temperature', 'mode', 'fan-level', 'temperature'
}
}
}
},
@ -278,7 +268,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'ptc-bath-heater': {
'required': {
'properties': {
'mode':{'read', 'write'}
'mode': {'read', 'write'}
}
},
'optional': {
@ -321,6 +311,89 @@ SPEC_DEVICE_TRANS_MAP: dict = {
'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'
}
}
"""SPEC_SERVICE_TRANS_MAP
@ -351,9 +424,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}
},
'optional': {
'properties': {
'mode', 'brightness', 'color', 'color-temperature'
}
'properties': {'mode', 'brightness', 'color', 'color-temperature'}
},
'entity': 'light'
},
@ -368,7 +439,8 @@ SPEC_SERVICE_TRANS_MAP: dict = {
},
'optional': {
'properties': {
'mode', 'brightness',
'mode',
'brightness',
}
},
'entity': 'light',
@ -389,6 +461,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
'fan-control': 'fan',
'ceiling-fan': 'fan',
'air-fresh': 'fan',
'air-purifier': 'fan',
'water-heater': {
'required': {
'properties': {
@ -396,7 +469,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}
},
'optional': {
'properties': {'on', 'temperature', 'target-temperature', 'mode'}
'properties': {'temperature', 'target-temperature', 'mode'}
},
'entity': 'water_heater'
},
@ -407,9 +480,7 @@ SPEC_SERVICE_TRANS_MAP: dict = {
}
},
'optional': {
'properties': {
'status', 'current-position', 'target-position'
}
'properties': {'status', 'current-position', 'target-position'}
},
'entity': 'cover'
},
@ -599,6 +670,4 @@ SPEC_EVENT_TRANS_MAP: dict[str, str] = {
'doorbell-ring': EventDeviceClass.DOORBELL
}
SPEC_ACTION_TRANS_MAP = {
}
SPEC_ACTION_TRANS_MAP = {}

View File

@ -113,7 +113,7 @@
},
"config_options": {
"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": {
"integration_language": "Lingua dell'Integrazione",
"update_user_info": "Aggiorna le informazioni dell'utente",

View File

@ -47,21 +47,18 @@ Vacuum entities for Xiaomi Home.
"""
from __future__ import annotations
from typing import Any, Optional
import re
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumEntityFeature
)
from homeassistant.components.vacuum import (StateVacuumEntity,
VacuumEntityFeature)
from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
from .miot.miot_spec import (
MIoTSpecAction,
MIoTSpecProperty)
from .miot.miot_spec import (MIoTSpecAction, MIoTSpecProperty)
_LOGGER = logging.getLogger(__name__)
@ -99,10 +96,12 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
_status_map: Optional[dict[int, str]]
_fan_level_map: Optional[dict[int, str]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
) -> None:
_device_name: str
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._device_name = miot_device.name
self._attr_supported_features = VacuumEntityFeature(0)
self._prop_status = None
@ -121,21 +120,21 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
for prop in entity_data.props:
if prop.name == 'status':
if not prop.value_list:
_LOGGER.error(
'invalid status value_list, %s', self.entity_id)
_LOGGER.error('invalid status value_list, %s',
self.entity_id)
continue
self._status_map = prop.value_list.to_map()
self._attr_supported_features |= VacuumEntityFeature.STATE
self._prop_status = prop
elif prop.name == 'fan-level':
if not prop.value_list:
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
_LOGGER.error('invalid fan-level value_list, %s',
self.entity_id)
continue
self._fan_level_map = prop.value_list.to_map()
self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop
elif prop.name == 'battery-level':
self._attr_supported_features |= VacuumEntityFeature.BATTERY
self._prop_battery_level = prop
@ -155,14 +154,22 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
elif action.name == 'stop-and-gocharge':
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
self._action_stop_and_gocharge = action
elif action.name == 'identify':
self._attr_supported_features |= VacuumEntityFeature.LOCATE
self._action_identify = action
async def async_start(self) -> None:
"""Start or resume the cleaning task."""
if self.state.lower() in ['paused', '暂停中']:
try: # VacuumActivity is introduced in HA core 2025.1.0
# pylint: disable=import-outside-toplevel
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)
@ -179,31 +186,92 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
"""Set the vacuum cleaner to return to the dock."""
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:
"""Locate the vacuum cleaner."""
await self.action_async(action=self._action_identify)
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""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
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
return self.get_map_value(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
"""Return the current state of the vacuum cleaner.
To fix the HA warning below:
Detected that custom integration 'xiaomi_home' is setting state
directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum
.Vacuum'>)should implement the 'activity' property and return
its state using the VacuumActivity enum.This will stop working in
Home Assistant 2026.1.
Refer to
https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property
There are only 6 states in VacuumActivity enum. To be compatible with
more constants, try get matching VacuumActivity enum first, return state
string as before if there is no match. In Home Assistant 2026.1, every
state should map to a VacuumActivity enum.
"""
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
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)
@property
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(
map_=self._fan_level_map,
key=self.get_prop_value(prop=self._prop_fan_level))

View File

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

View File

@ -33,9 +33,11 @@ git checkout v1.0.0
### 方法 2: [HACS](https://hacs.xyz/)
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) 手动安装
@ -47,7 +49,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
[设置 > 设备与服务 > 添加集成](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 设备
@ -59,7 +61,7 @@ HACS > 右上角三个点 > Custom repositories > Repository: https://github.com
方法:[设置 > 设备与服务 > 已配置 > 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`
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
| in输入参数列表 | 转换后的实体 |
@ -289,39 +293,37 @@ event instance name 下的值表示转换后实体所用的 `_attr_device_class`
### 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`的格式如下:
```
{
"<MIoT-Spec-V2 device instance>":{
"services": list<service_iid: str>,
"properties": list<service_iid.property_iid: str>,
"events": list<service_iid.event_iid: str>,
"actions": list<service_iid.action_iid: str>,
}
}
```yaml
<MIoT-Spec-V2 device instance urn without the version field>:
services: list<service_iid: str>
properties: list<service_iid.property_iid: str>
events: list<service_iid.event_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 )。支持通配符匹配。
示例:
```
{
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance.
},
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"services": ["3"], # Filter out the service whose iid=3.
"properties": ["4.*"] # Filter out all properties in the service whose iid=4.
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service.
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service.
}
}
```yaml
urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
services:
- '*' # 排除所有服务,相当于排除拥有该 MIoT-Spec-V2 的设备。
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
services:
- '3' # 排除 siid=3 的服务。
properties:
- '4.*' # 排除 siid=4 服务的所有属性。
events:
- '4.1' # 排除 siid=4 服务的 eiid=1 的事件。
actions:
- '4.1' # 排除 siid=4 服务的 aiid=1 的方法。
```
所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。
@ -353,7 +355,7 @@ instance code 为 MIoT-Spec-V2 实例代码,格式如下:
```
service:<siid> # 服务
service:<siid>:property:<piid> # 属性
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的值
service:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引
service:<siid>:event:<eiid> # 事件
service:<siid>:action:<aiid> # 方法
```
@ -376,7 +378,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
}
```
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的 `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

@ -18,6 +18,8 @@ SPEC_BOOL_TRANS_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
SPEC_FILTER_FILE = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
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(
@ -38,7 +40,7 @@ def load_json_file(file_path: str) -> Optional[dict]:
def save_json_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
json.dump(data, file, ensure_ascii=False, indent=2)
def load_yaml_file(file_path: str) -> Optional[dict]:
@ -140,6 +142,14 @@ def bool_trans(d: dict) -> bool:
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):
@ -304,6 +314,10 @@ def sort_spec_add(file_path: str):
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'
@ -326,6 +340,14 @@ def test_spec_filter():
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_multi_lang():
data = load_json_file(SPEC_MULTI_LANG_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_MULTI_LANG_FILE} failed'
assert multi_lang(data), f'{SPEC_MULTI_LANG_FILE} format error'
@pytest.mark.github
def test_spec_add():
data = load_json_file(SPEC_ADD_FILE)
@ -418,6 +440,12 @@ def test_miot_data_sort():
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(
load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(
sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (
f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(
sort_spec_add(file_path=SPEC_ADD_FILE)), (
f'{SPEC_ADD_FILE} not sorted, goto project root path'
@ -438,6 +466,8 @@ def test_sort_spec_data():
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
sort_data = sort_spec_add(file_path=SPEC_ADD_FILE)
save_json_file(file_path=SPEC_ADD_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_ADD_FILE)