23 Commits

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

* fix: i18n langurage support

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

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

* docs: modify README

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

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

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

* fix: translations

* feat: set max cover closed position as 5

* docs: modify README

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

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

* fix: airer status

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

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

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

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

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

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

* fix: environment temperature siid and piid
2025-06-27 17:52:39 +08:00
45 changed files with 1162 additions and 185 deletions

View File

@ -1,9 +1,39 @@
# CHANGELOG # CHANGELOG
## v0.4.0
### Added
- Add the watch as the device tracker entity. [#1189](https://github.com/XiaoMi/ha_xiaomi_home/pull/1189)
- Add the wifi speaker and the television as the media player entity. [#706](https://github.com/XiaoMi/ha_xiaomi_home/pull/706)
- Add an option in CONFIGURE to set the cover closed position. [#1242](https://github.com/XiaoMi/ha_xiaomi_home/pull/1242)
- Add notifications to show the status of the local connection to the central hub gateway. [#1280](https://github.com/XiaoMi/ha_xiaomi_home/pull/1280)
- Import the device from the third party cloud. [#1258](https://github.com/XiaoMi/ha_xiaomi_home/pull/1258)
### Changed
- Add an alongside switch entity for viomi.waterheater.m1. [#1255](https://github.com/XiaoMi/ha_xiaomi_home/pull/1255)
- Do not subscribe BLE device online/offline state message. [#1264](https://github.com/XiaoMi/ha_xiaomi_home/pull/1264)
### Fixed
- Keep the first element of the discovered ip address list as the recently added address when getting mdns result. [#1250](https://github.com/XiaoMi/ha_xiaomi_home/pull/1250)
- Subscribe local topics every time when connected to the central hub gateway. [#1266](https://github.com/XiaoMi/ha_xiaomi_home/pull/1266)
- Record the "closing" and "closed" status that occur frequently in the motor-controller, the window-opener and the curtain service. [#1262](https://github.com/XiaoMi/ha_xiaomi_home/pull/1262)
- Fix xiaomi.aircondition.c24 total power consumption unit, adp.motor.adswb4 motor switch, cgllc.airm.cgd1st environment temperature, and shhf.light.sflt11 fan switch status. [#1256](https://github.com/XiaoMi/ha_xiaomi_home/pull/1256)
## v0.3.4
### Added
- Exclude the unsupported device models. [#1205](https://github.com/XiaoMi/ha_xiaomi_home/pull/1205)
### Changed
- Subscribe the BLE device upstream messages even though the device is offline. [#1207](https://github.com/XiaoMi/ha_xiaomi_home/pull/1207)
- Record "opening", "closing" and "closed" status of the airer service that occur frequently and do not record "stop" status for the cover entity. [#1235](https://github.com/XiaoMi/ha_xiaomi_home/pull/1235)
- Modify README about spec_filter.yaml and the event attributes. [#1237](https://github.com/XiaoMi/ha_xiaomi_home/pull/1237)
### Fixed
- Fix the reconnect delay time to be reset when the client is connected to the broker. [#1200](https://github.com/XiaoMi/ha_xiaomi_home/pull/1200)
- Fix the HA warning in the logs related to vacuum state setting. [#694](https://github.com/XiaoMi/ha_xiaomi_home/pull/694)
- Fix the operation mode when the device does not have a mode property. [#1199](https://github.com/XiaoMi/ha_xiaomi_home/pull/1199)
- Fix 090615.aircondition.ktf environment temperature. [#1210](https://github.com/XiaoMi/ha_xiaomi_home/pull/1210)
- Fix a missing variable in translation it.json. [#1215](https://github.com/XiaoMi/ha_xiaomi_home/pull/1215)
- Fix yutai.plug.fsov8m power consumption and ignore bjkcz.curtain.kczble curtain status. [#1236](https://github.com/XiaoMi/ha_xiaomi_home/pull/1236)
## v0.3.3 ## v0.3.3
### Changed ### Changed
- Change the log level of error "mips unsub internal error, 4, None". [#1135](https://github.com/XiaoMi/ha_xiaomi_home/pull/1135) - 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) - Add necessary logs for distinguishing the set_properties command source. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)
### Fixed ### Fixed
- Fix tofan.airrtc.wk01 thermostat and air conditioner service. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160) - 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 mrbond.airer.m1t closing status. [#1134](https://github.com/XiaoMi/ha_xiaomi_home/pull/1134)

View File

@ -83,9 +83,9 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
## FAQ ## FAQ
- Does Xiaomi Home Integration support all Xiaomi Home devices? - Does Xiaomi Home Integration support all Xiaomi smart devices?
Xiaomi Home Integration currently supports most categories of Home device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device. Xiaomi Home Integration currently supports most categories of the smart device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.
- Does Xiaomi Home Integration support multiple Xiaomi accounts? - Does Xiaomi Home Integration support multiple Xiaomi accounts?
@ -93,7 +93,7 @@ Xiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi
- Does Xiaomi Home Integration support local control? - Does Xiaomi Home Integration support local control?
Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.4.0_0000 above) or Xiaomi home devices with built-in central hub gateway (software version 0.8.0 above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans. Local control is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with built-in central hub gateway (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local control feature has not been released yet. Please refer to MIoT team's notification for upgrade plans.
Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available. Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.
@ -155,6 +155,8 @@ In MIoT-Spec-V2 protocol, a product is defined as a device. A device contains se
MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`. MIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.
MIoT-Spec-V2 event's arguments field is the list of parameters of the event. The list elements represent the piid of the property in the same service. For example, the [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057) of the Xiaomi Wireless Double-key Switch contains the siid=2 Switch Sensor service. The eiid=1014 Long Press event of the service is triggered when a button is long pressed. The debug level log will print `Press and hold, attributes: {'Button Type': 1}`. This is an example log that the button type is 1, which means the right button is long pressed.
- Action - Action
| in | Entity in Home Assistant | | in | Entity in Home Assistant |
@ -289,39 +291,37 @@ The value of the event instance name indicates `_attr_device_class` of the Home
### MIoT-Spec-V2 Filter ### MIoT-Spec-V2 Filter
`spec_filter.json` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity. `spec_filter.yaml` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.
The format of `spec_filter.json` is as follows. The format of `spec_filter.yaml` is as follows.
``` ```yaml
{ <MIoT-Spec-V2 device instance urn without the version field>:
"<MIoT-Spec-V2 device instance>":{ services: list<service_iid: str>
"services": list<service_iid: str>, properties: list<service_iid.property_iid: str>
"properties": list<service_iid.property_iid: str>, events: list<service_iid.event_iid: str>
"events": list<service_iid.event_iid: str>, actions: list<service_iid.action_iid: str>
"actions": list<service_iid.action_iid: str>,
}
}
``` ```
The key of `spec_filter.json` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.json` does not need to specify the version number of MIoT-Spec-V2 device instance. The key of `spec_filter.yaml` dictionary is the urn excluding the "version" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.yaml` does not need to specify the version number of MIoT-Spec-V2 device instance.
The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported. The value of "services", "properties", "events" or "actions" fields under "device instance" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.
Example: Example:
``` ```yaml
{ urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{ services:
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance. - '*' # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2.
}, urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { services:
"services": ["3"], # Filter out the service whose iid=3. - '3' # Filter out the siid=3 service.
"properties": ["4.*"] # Filter out all properties in the service whose iid=4. properties:
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service. - '4.*' # Filter out all properties in the siid=4 service.
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service. events:
} - '4.1' # Filter out the eiid=1 event in the siid=4 service.
} actions:
- '4.1' # Filter out the aiid=1 action in the siid=4 service.
``` ```
Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity. Device information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.
@ -376,7 +376,7 @@ Example:
} }
``` ```
> If you edit any files in the `custom_components/xiaomi_home/miot/specs` directory (`spec_filter.py`, `spec_modify.json`, `multi_lang.json`, etc.) in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update entity conversion rules > If you edit any files in the `custom_components/xiaomi_home/miot/specs` directory (`spec_filter.yaml`, `spec_modify.yaml`, `multi_lang.json`, etc.) in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update entity conversion rules
## Documents ## Documents

View File

@ -75,6 +75,9 @@ from .miot.const import (
DEFAULT_CLOUD_SERVER, DEFAULT_CLOUD_SERVER,
DEFAULT_CTRL_MODE, DEFAULT_CTRL_MODE,
DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_INTEGRATION_LANGUAGE,
DEFAULT_COVER_CLOSED_POSITION,
MIN_COVER_CLOSED_POSITION,
MAX_COVER_CLOSED_POSITION,
DEFAULT_NICK_NAME, DEFAULT_NICK_NAME,
DEFAULT_OAUTH2_API_HOST, DEFAULT_OAUTH2_API_HOST,
DOMAIN, DOMAIN,
@ -129,6 +132,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_cloud_server: str _cloud_server: str
_integration_language: str _integration_language: str
_cover_closed_position: int
_auth_info: dict _auth_info: dict
_nick_name: str _nick_name: str
_home_selected: dict _home_selected: dict
@ -151,6 +155,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._main_loop = asyncio.get_running_loop() self._main_loop = asyncio.get_running_loop()
self._cloud_server = DEFAULT_CLOUD_SERVER self._cloud_server = DEFAULT_CLOUD_SERVER
self._integration_language = DEFAULT_INTEGRATION_LANGUAGE self._integration_language = DEFAULT_INTEGRATION_LANGUAGE
self._cover_closed_position = DEFAULT_COVER_CLOSED_POSITION
self._storage_path = '' self._storage_path = ''
self._virtual_did = '' self._virtual_did = ''
self._uid = '' self._uid = ''
@ -951,6 +956,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
'action_debug': self._action_debug, 'action_debug': self._action_debug,
'hide_non_standard_entities': 'hide_non_standard_entities':
self._hide_non_standard_entities, self._hide_non_standard_entities,
'cover_closed_position': self._cover_closed_position,
'display_binary_mode': self._display_binary_mode, 'display_binary_mode': self._display_binary_mode,
'display_devices_changed_notify': 'display_devices_changed_notify':
self._display_devices_changed_notify self._display_devices_changed_notify
@ -995,6 +1001,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_hide_non_standard_entities: bool _hide_non_standard_entities: bool
_display_binary_mode: list[str] _display_binary_mode: list[str]
_display_devs_notify: list[str] _display_devs_notify: list[str]
_cover_closed_position: int
_oauth_redirect_url_full: str _oauth_redirect_url_full: str
_auth_info: dict _auth_info: dict
@ -1015,6 +1022,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_opt_lan_ctrl_cfg: bool _opt_lan_ctrl_cfg: bool
_opt_network_detect_cfg: bool _opt_network_detect_cfg: bool
_opt_check_network_deps: bool _opt_check_network_deps: bool
_cover_pos_new: int
_trans_rules_count: int _trans_rules_count: int
_trans_rules_count_success: int _trans_rules_count_success: int
@ -1043,6 +1051,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE) self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)
self._integration_language = self._entry_data.get( self._integration_language = self._entry_data.get(
'integration_language', DEFAULT_INTEGRATION_LANGUAGE) 'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
self._cover_closed_position = self._entry_data.get(
'cover_closed_position', DEFAULT_COVER_CLOSED_POSITION)
self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME) self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)
self._action_debug = self._entry_data.get('action_debug', False) self._action_debug = self._entry_data.get('action_debug', False)
self._hide_non_standard_entities = self._entry_data.get( self._hide_non_standard_entities = self._entry_data.get(
@ -1068,6 +1078,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._action_debug_new = False self._action_debug_new = False
self._hide_non_standard_entities_new = False self._hide_non_standard_entities_new = False
self._display_binary_mode_new = [] self._display_binary_mode_new = []
self._cover_pos_new = self._cover_closed_position
self._update_user_info = False self._update_user_info = False
self._update_devices = False self._update_devices = False
self._update_trans_rules = False self._update_trans_rules = False
@ -1340,6 +1351,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
): cv.multi_select( ): cv.multi_select(
self._miot_i18n.translate( self._miot_i18n.translate(
'config.binary_mode')), # type: ignore 'config.binary_mode')), # type: ignore
vol.Optional(
'cover_closed_position',
default=self._cover_closed_position # type: ignore
): vol.All(vol.Coerce(int), vol.Range(
min=MIN_COVER_CLOSED_POSITION,
max=MAX_COVER_CLOSED_POSITION)),
vol.Required( vol.Required(
'update_trans_rules', 'update_trans_rules',
default=self._update_trans_rules # type: ignore default=self._update_trans_rules # type: ignore
@ -1378,6 +1395,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'update_lan_ctrl_config', self._opt_lan_ctrl_cfg) 'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)
self._opt_network_detect_cfg = user_input.get( self._opt_network_detect_cfg = user_input.get(
'network_detect_config', self._opt_network_detect_cfg) 'network_detect_config', self._opt_network_detect_cfg)
self._cover_pos_new = user_input.get(
'cover_closed_position', self._cover_closed_position)
return await self.async_step_update_user_info() return await self.async_step_update_user_info()
@ -1926,6 +1945,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'nick_name': self._nick_name, 'nick_name': self._nick_name,
'lang_new': INTEGRATION_LANGUAGES[self._lang_new], 'lang_new': INTEGRATION_LANGUAGES[self._lang_new],
'nick_name_new': self._nick_name_new, 'nick_name_new': self._nick_name_new,
'cover_pos_new': self._cover_pos_new,
'devices_add': len(self._devices_add), 'devices_add': len(self._devices_add),
'devices_remove': len(self._devices_remove), 'devices_remove': len(self._devices_remove),
'trans_rules_count': self._trans_rules_count, 'trans_rules_count': self._trans_rules_count,
@ -1952,6 +1972,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if self._lang_new != self._integration_language: if self._lang_new != self._integration_language:
self._entry_data['integration_language'] = self._lang_new self._entry_data['integration_language'] = self._lang_new
self._need_reload = True self._need_reload = True
if self._cover_pos_new != self._cover_closed_position:
self._entry_data['cover_closed_position'] = self._cover_pos_new
self._need_reload = True
if self._update_user_info: if self._update_user_info:
self._entry_data['nick_name'] = self._nick_name_new self._entry_data['nick_name'] = self._nick_name_new
if self._update_devices: if self._update_devices:

View File

@ -46,8 +46,9 @@ off Xiaomi or its affiliates' products.
Cover entities for Xiaomi Home. Cover entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any, Optional from typing import Any, Optional
import re
import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -90,6 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
class Cover(MIoTServiceEntity, CoverEntity): class Cover(MIoTServiceEntity, CoverEntity):
"""Cover entities for Xiaomi Home.""" """Cover entities for Xiaomi Home."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
_cover_closed_position: int
_prop_motor_control: Optional[MIoTSpecProperty] _prop_motor_control: Optional[MIoTSpecProperty]
_prop_motor_value_open: Optional[int] _prop_motor_value_open: Optional[int]
_prop_motor_value_close: Optional[int] _prop_motor_value_close: Optional[int]
@ -97,7 +99,6 @@ class Cover(MIoTServiceEntity, CoverEntity):
_prop_status: Optional[MIoTSpecProperty] _prop_status: Optional[MIoTSpecProperty]
_prop_status_opening: Optional[list[int]] _prop_status_opening: Optional[list[int]]
_prop_status_closing: Optional[list[int]] _prop_status_closing: Optional[list[int]]
_prop_status_stop: Optional[list[int]]
_prop_status_closed: Optional[list[int]] _prop_status_closed: Optional[list[int]]
_prop_current_position: Optional[MIoTSpecProperty] _prop_current_position: Optional[MIoTSpecProperty]
_prop_target_position: Optional[MIoTSpecProperty] _prop_target_position: Optional[MIoTSpecProperty]
@ -115,6 +116,9 @@ class Cover(MIoTServiceEntity, CoverEntity):
self._attr_supported_color_modes = set() self._attr_supported_color_modes = set()
self._attr_supported_features = CoverEntityFeature(0) self._attr_supported_features = CoverEntityFeature(0)
self._cover_closed_position = (
miot_device.miot_client.cover_closed_position)
self._prop_motor_control = None self._prop_motor_control = None
self._prop_motor_value_open = None self._prop_motor_value_open = None
self._prop_motor_value_close = None self._prop_motor_value_close = None
@ -122,7 +126,6 @@ class Cover(MIoTServiceEntity, CoverEntity):
self._prop_status = None self._prop_status = None
self._prop_status_opening = [] self._prop_status_opening = []
self._prop_status_closing = [] self._prop_status_closing = []
self._prop_status_stop = []
self._prop_status_closed = [] self._prop_status_closed = []
self._prop_current_position = None self._prop_current_position = None
self._prop_target_position = None self._prop_target_position = None
@ -159,13 +162,22 @@ class Cover(MIoTServiceEntity, CoverEntity):
self.entity_id) self.entity_id)
continue continue
for item in prop.value_list.items: for item in prop.value_list.items:
if item.name in {'opening', 'open', '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) self._prop_status_opening.append(item.value)
elif item.name in {'closing', 'close', 'down', 'dowm'}: elif item_name in {
'closing', 'close', 'down', 'dowm', 'falling',
'fallin', 'dropping', 'downing', 'lower'
}:
self._prop_status_closing.append(item.value) self._prop_status_closing.append(item.value)
elif item.name in {'stop', 'stopped', 'pause'}: elif item_name in {
self._prop_status_stop.append(item.value) 'closed', 'closeover', 'stopatlowest',
elif item.name in {'closed'}: 'stoplowerlimit', 'lowerlimitstop', 'floor',
'lowerlimit'
}:
self._prop_status_closed.append(item.value) self._prop_status_closed.append(item.value)
self._prop_status = prop self._prop_status = prop
elif prop.name == 'current-position': elif prop.name == 'current-position':
@ -290,7 +302,7 @@ class Cover(MIoTServiceEntity, CoverEntity):
def is_closed(self) -> Optional[bool]: def is_closed(self) -> Optional[bool]:
"""Return if the cover is closed.""" """Return if the cover is closed."""
if self.current_cover_position is not None: if self.current_cover_position is not None:
return self.current_cover_position == 0 return self.current_cover_position <= self._cover_closed_position
# The current position is prior to the status when determining # The current position is prior to the status when determining
# whether the cover is closed. # whether the cover is closed.
if self._prop_status and self._prop_status_closed: if self._prop_status and self._prop_status_closed:

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 # preset_mode
if preset_mode: if preset_mode:
await self.set_property_async( await self.set_property_async(
self._prop_mode, prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, value=preset_mode)) map_=self._mode_map, value=preset_mode))
@ -258,7 +258,7 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode.""" """Set the preset mode."""
await self.set_property_async( await self.set_property_async(
self._prop_mode, prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, value=preset_mode)) map_=self._mode_map, value=preset_mode))

View File

@ -25,7 +25,7 @@
"cryptography", "cryptography",
"psutil" "psutil"
], ],
"version": "v0.3.3", "version": "v0.4.0",
"zeroconf": [ "zeroconf": [
"_miot-central._tcp.local." "_miot-central._tcp.local."
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,8 @@ from .common import MIoTMatcher, slugify_did
from .const import ( from .const import (
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN, DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL, MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL) OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL,
DEFAULT_COVER_CLOSED_POSITION)
from .miot_cloud import MIoTHttpClient, MIoTOauthClient from .miot_cloud import MIoTHttpClient, MIoTOauthClient
from .miot_error import MIoTClientError, MIoTErrorCode from .miot_error import MIoTClientError, MIoTErrorCode
from .miot_mips import ( from .miot_mips import (
@ -486,6 +487,11 @@ class MIoTClient:
def display_binary_bool(self) -> bool: def display_binary_bool(self) -> bool:
return self._display_binary_bool return self._display_binary_bool
@property
def cover_closed_position(self) -> int:
return self._entry_data.get('cover_closed_position',
DEFAULT_COVER_CLOSED_POSITION)
@display_devices_changed_notify.setter @display_devices_changed_notify.setter
def display_devices_changed_notify(self, value: list[str]) -> None: def display_devices_changed_notify(self, value: list[str]) -> None:
if set(value) == set(self._display_devs_notify): if set(value) == set(self._display_devs_notify):
@ -990,7 +996,7 @@ class MIoTClient:
and self._device_list_cloud[did].get('online', False) and self._device_list_cloud[did].get('online', False)
): ):
from_new = 'cloud' from_new = 'cloud'
if from_new == from_old: if (from_new == from_old) and (from_new=='cloud' or from_new=='lan'):
# No need to update # No need to update
return return
# Unsub old # Unsub old
@ -1109,8 +1115,10 @@ class MIoTClient:
_LOGGER.info('local mips state changed, %s, %s', group_id, state) _LOGGER.info('local mips state changed, %s, %s', group_id, state)
mips = self._mips_local.get(group_id, None) mips = self._mips_local.get(group_id, None)
if not mips: if not mips:
_LOGGER.error( _LOGGER.info(
'local mips state changed, mips not exist, %s', group_id) 'local mips state changed, mips not exist, %s', group_id)
# The connection to the central hub gateway is definitely broken.
self.__show_central_state_changed_notify(False)
return return
if state: if state:
# Connected # Connected
@ -1144,6 +1152,7 @@ class MIoTClient:
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify() self.__request_show_devices_changed_notify()
self.__show_central_state_changed_notify(state)
@final @final
async def __on_miot_lan_state_change(self, state: bool) -> None: async def __on_miot_lan_state_change(self, state: bool) -> None:
@ -1354,6 +1363,11 @@ class MIoTClient:
"""Update cloud devices. """Update cloud devices.
NOTICE: This function will operate the cloud_list NOTICE: This function will operate the cloud_list
""" """
# MIoT cloud service may not publish the online state updating message
# for the BLE device. Assume that all BLE devices are online.
for did, info in cloud_list.items():
if did.startswith('blt.'):
info['online'] = True
for did, info in self._device_list_cache.items(): for did, info in self._device_list_cache.items():
if filter_dids and did not in filter_dids: if filter_dids and did not in filter_dids:
continue continue
@ -1481,8 +1495,6 @@ class MIoTClient:
if did not in filter_dids: if did not in filter_dids:
continue continue
device_old = self._device_list_gateway.get(did, None) device_old = self._device_list_gateway.get(did, None)
gw_state_old = device_old.get(
'online', False) if device_old else False
gw_state_new: bool = False gw_state_new: bool = False
device_new = gw_list.pop(did, None) device_new = gw_list.pop(did, None)
if device_new: if device_new:
@ -1496,7 +1508,7 @@ class MIoTClient:
device_old['online'] = False device_old['online'] = False
# Update cache group_id # Update cache group_id
info['group_id'] = group_id info['group_id'] = group_id
if gw_state_old == gw_state_new: if not gw_state_new:
continue continue
self.__update_device_msg_sub(did=did) self.__update_device_msg_sub(did=did)
state_old: Optional[bool] = info.get('online', None) state_old: Optional[bool] = info.get('online', None)
@ -1906,6 +1918,23 @@ class MIoTClient:
self._show_devices_changed_notify_timer = self._main_loop.call_later( self._show_devices_changed_notify_timer = self._main_loop.call_later(
delay_sec, self.__show_devices_changed_notify) delay_sec, self.__show_devices_changed_notify)
@final
def __show_central_state_changed_notify(self, connected: bool) -> None:
conn_status: str = (
self._i18n.translate('miot.client.central_state_connected')
if connected else
self._i18n.translate('miot.client.central_state_disconnected'))
self._persistence_notify(
self.__gen_notify_key('central_state_changed'),
self._i18n.translate('miot.client.central_state_changed_title'),
self._i18n.translate(key='miot.client.central_state_changed',
replace={
'nick_name': self._entry_data.get(
'nick_name', DEFAULT_NICK_NAME),
'uid': self._uid,
'cloud_server': self._cloud_server,
'conn_status': conn_status
}))
@staticmethod @staticmethod
async def get_miot_instance_async( async def get_miot_instance_async(

View File

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

View File

@ -1207,10 +1207,9 @@ class MIoTPropertyEntity(Entity):
self._attr_available = miot_device.online self._attr_available = miot_device.online
_LOGGER.info( _LOGGER.info(
'new miot property entity, %s, %s, %s, %s, %s, %s, %s', 'new miot property entity, %s, %s, %s, %s, %s',
self.miot_device.name, self._attr_name, spec.platform, self.miot_device.name, self._attr_name, spec.platform,
spec.device_class, self.entity_id, self._value_range, spec.device_class, self.entity_id)
self._value_list)
@property @property
def device_info(self) -> Optional[DeviceInfo]: def device_info(self) -> Optional[DeviceInfo]:

View File

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

View File

@ -68,7 +68,7 @@ from paho.mqtt.client import (
# pylint: disable=relative-beyond-top-level # pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher from .common import MIoTMatcher
from .const import MIHOME_MQTT_KEEPALIVE from .const import UNSUPPORTED_MODELS, MIHOME_MQTT_KEEPALIVE
from .miot_error import MIoTErrorCode, MIoTMipsError from .miot_error import MIoTErrorCode, MIoTMipsError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -216,7 +216,7 @@ class _MipsClient(ABC):
MQTT_INTERVAL_S = 1 MQTT_INTERVAL_S = 1
MIPS_QOS: int = 2 MIPS_QOS: int = 2
UINT32_MAX: int = 0xFFFFFFFF UINT32_MAX: int = 0xFFFFFFFF
MIPS_RECONNECT_INTERVAL_MIN: float = 30 MIPS_RECONNECT_INTERVAL_MIN: float = 10
MIPS_RECONNECT_INTERVAL_MAX: float = 600 MIPS_RECONNECT_INTERVAL_MAX: float = 600
MIPS_SUB_PATCH: int = 300 MIPS_SUB_PATCH: int = 300
MIPS_SUB_INTERVAL: float = 1 MIPS_SUB_INTERVAL: float = 1
@ -641,6 +641,7 @@ class _MipsClient(ABC):
if not self._mqtt.is_connected(): if not self._mqtt.is_connected():
return return
self.log_info(f'mips connect, {flags}, {rc}, {props}') self.log_info(f'mips connect, {flags}, {rc}, {props}')
self.__reset_reconnect_time()
self._mqtt_state = True self._mqtt_state = True
self._internal_loop.call_soon( self._internal_loop.call_soon(
self._on_mips_connect, rc, props) self._on_mips_connect, rc, props)
@ -822,7 +823,7 @@ class _MipsClient(ABC):
self._internal_loop.stop() self._internal_loop.stop()
def __get_next_reconnect_time(self) -> float: def __get_next_reconnect_time(self) -> float:
if self._mips_reconnect_interval == 0: if self._mips_reconnect_interval < self.MIPS_RECONNECT_INTERVAL_MIN:
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
else: else:
self._mips_reconnect_interval = min( self._mips_reconnect_interval = min(
@ -830,6 +831,9 @@ class _MipsClient(ABC):
self.MIPS_RECONNECT_INTERVAL_MAX) self.MIPS_RECONNECT_INTERVAL_MAX)
return self._mips_reconnect_interval return self._mips_reconnect_interval
def __reset_reconnect_time(self) -> None:
self._mips_reconnect_interval = 0
class MipsCloudClient(_MipsClient): class MipsCloudClient(_MipsClient):
"""MIoT Pub/Sub Cloud Client.""" """MIoT Pub/Sub Cloud Client."""
@ -990,6 +994,11 @@ class MipsCloudClient(_MipsClient):
handler( handler(
did, MIoTDeviceState.ONLINE if msg['event'] == 'online' did, MIoTDeviceState.ONLINE if msg['event'] == 'online'
else MIoTDeviceState.OFFLINE, ctx) else MIoTDeviceState.OFFLINE, ctx)
if did.startswith('blt.'):
# MIoT cloud may not publish BLE device online/offline state message.
# Do not subscribe BLE device online/offline state.
return True
return self.__reg_broadcast_external( return self.__reg_broadcast_external(
topic=topic, handler=on_state_msg, handler_ctx=handler_ctx) topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)
@ -1174,7 +1183,7 @@ class MipsLocalClient(_MipsClient):
or 'piid' not in msg or 'piid' not in msg
or 'value' not in msg or 'value' not in msg
): ):
# self.log_error(f'on_prop_msg, recv unknown msg, {payload}') self.log_info('unknown prop msg, %s', payload)
return return
if handler: if handler:
self.log_debug('local, on properties_changed, %s', payload) self.log_debug('local, on properties_changed, %s', payload)
@ -1361,6 +1370,9 @@ class MipsLocalClient(_MipsClient):
if name is None or urn is None or model is None: if name is None or urn is None or model is None:
self.log_error(f'invalid device info, {did}, {info}') self.log_error(f'invalid device info, {did}, {info}')
continue continue
if model in UNSUPPORTED_MODELS:
self.log_info(f'unsupported model, {model}, {did}')
continue
device_list[did] = { device_list[did] = {
'did': did, 'did': did,
'name': name, 'name': name,

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": [ "urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1": [
{ {
"iid": 3, "iid": 3,
@ -178,6 +203,26 @@
] ]
} }
], ],
"urn:miot-spec-v2:device:water-heater:0000A02A:viomi-m1:2": [
{
"iid": 2,
"type": "urn:miot-spec-v2:service:switch:0000780C:viomi-m1:1",
"description": "Water Heater",
"properties": [
{
"iid": 6,
"type": "urn:miot-spec-v2:property:on:00000006:viomi-m1:1",
"description": "Switch Status",
"format": "bool",
"access": [
"read",
"write",
"notify"
]
}
]
}
],
"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [ "urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1": [
{ {
"iid": 2, "iid": 2,

View File

@ -1,3 +1,6 @@
urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:
services:
- '4'
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
properties: properties:
- 9.* - 9.*
@ -41,9 +44,6 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
services: services:
- '1' - '1'
- '5' - '5'
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
services:
- '*'
urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01: urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
services: services:
- '2' - '2'

View File

@ -6,6 +6,8 @@ urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1:
unit: none 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-c20:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1:
prop.8.6:
unit: kWh
prop.10.6: prop.10.6:
unit: none 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-c24:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1
@ -26,6 +28,12 @@ urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1:
prop.10.6: prop.10.6:
unit: none 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-conditioner:0000A004:xiaomi-mt0:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgd1st:1:
prop.3.7:
value-range:
- -30
- 100
- 0.1
urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1: urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:
prop.2.5: prop.2.5:
name: voc-density name: voc-density
@ -75,6 +83,9 @@ urn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:
urn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1: urn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1:
prop.4.4: prop.4.4:
name: fan-level-ventilation 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: urn:miot-spec-v2:device:fan:0000A005:dmaker-p33:1:
prop.2.2: prop.2.2:
name: fan-level-a name: fan-level-a
@ -150,6 +161,9 @@ urn:miot-spec-v2:device:light:0000A001:shhf-sfla10:1:
urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1: urn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:
prop.8.11: prop.8.11:
name: on-a name: on-a
urn:miot-spec-v2:device:light:0000A001:shhf-sflt11:1:0000C802:
prop.11.14:
name: on-power
urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1: urn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1:
prop.2.1004: prop.2.1004:
name: contact-state name: contact-state
@ -165,6 +179,9 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:
- read - read
- notify - notify
unit: mV unit: mV
urn:miot-spec-v2:device:motor-controller:0000A01D:adp-adswb4:1:0000C837:
prop.2.1:
name: motor-switch
urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2 urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2
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: prop.2.8:
@ -209,6 +226,9 @@ urn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816:
unit: mV unit: mV
prop.3.4: prop.3.4:
unit: mA 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:1:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2: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: prop.3.1:

View File

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

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen", "display_devices_changed_notify": "Gerätestatusänderungen anzeigen",
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren", "update_trans_rules": "Entitätskonvertierungsregeln aktualisieren",
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren", "update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren",
"network_detect_config": "Integrierte Netzwerkkonfiguration" "network_detect_config": "Integrierte Netzwerkkonfiguration",
"cover_closed_position": "Die Position der geschlossenen Vorhänge"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Bestätigen Sie die Konfiguration", "title": "Bestätigen Sie die Konfiguration",
"description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln", "description": "**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \"Senden\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\r\n\r\nIntegrationsprache:\t{lang_new}\r\nBenutzername:\t{nick_name_new}\r\nAction-Debug-Modus:\t{action_debug}\r\nVerstecke Nicht-Standard-Entitäten:\t{hide_non_standard_entities}\r\nDie Position der geschlossenen Vorhänge:\t{cover_pos_new}\r\nGerätestatusänderungen anzeigen:\t{display_devices_changed_notify}\r\nGeräteänderungen:\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\r\nKonvertierungsregeländerungen:\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln",
"data": { "data": {
"confirm": "Änderungen bestätigen" "confirm": "Änderungen bestätigen"
} }

View File

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

View File

@ -124,7 +124,8 @@
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo", "display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo",
"update_trans_rules": "Actualizar reglas de conversión de entidad", "update_trans_rules": "Actualizar reglas de conversión de entidad",
"update_lan_ctrl_config": "Actualizar configuración de control LAN", "update_lan_ctrl_config": "Actualizar configuración de control LAN",
"network_detect_config": "Configuración de Red Integrada" "network_detect_config": "Configuración de Red Integrada",
"cover_closed_position": "La posición de las cortinas cerradas"
} }
}, },
"update_user_info": { "update_user_info": {
@ -183,7 +184,7 @@
}, },
"config_confirm": { "config_confirm": {
"title": "Confirmar configuración", "title": "Confirmar configuración",
"description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas", "description": "¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \"Enviar\" para finalizar la configuración.\r\nLa integración se volverá a cargar con la nueva configuración.\r\n\r\nIdioma de la integración:\t{lang_new}\r\nApodo de usuario:\t{nick_name_new}\r\nModo de depuración de Action:\t{action_debug}\r\nOcultar entidades generadas no estándar:\t{hide_non_standard_entities}\r\nLa posición de las cortinas cerradas:\t{cover_pos_new}\r\nMostrar notificaciones de cambio de estado del dispositivo:\t{display_devices_changed_notify}\r\nCambios de dispositivos:\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\r\nCambios en las reglas de conversión:\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas",
"data": { "data": {
"confirm": "Confirmar modificación" "confirm": "Confirmar modificación"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -141,12 +141,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
continue continue
self._mode_map = prop.value_list.to_map() self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values()) self._attr_operation_list = list(self._mode_map.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop self._prop_mode = prop
if not self._attr_operation_list: if not self._attr_operation_list:
self._attr_operation_list = [STATE_ON] self._attr_operation_list = [STATE_ON]
self._attr_operation_list.append(STATE_OFF) self._attr_operation_list.append(STATE_OFF)
self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the water heater on.""" """Turn the water heater on."""
@ -197,5 +196,5 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return STATE_OFF return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on): if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON return STATE_ON
return self.get_map_value(map_=self._mode_map, return (None if self._prop_mode is None else self.get_map_value(
key=self.get_prop_value(prop=self._prop_mode)) map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)))

View File

@ -157,6 +157,8 @@ git checkout v1.0.0
转换后的实体为 Event事件参数同时传递给实体的 `_trigger_event` 转换后的实体为 Event事件参数同时传递给实体的 `_trigger_event`
MIoT-Spec-V2 事件的 arguments 字段是事件的参数列表,列表元素代表同服务下属性的 piid 。例如,小米智能无线开关(双开)的 [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057)的 siid=2 无线开关服务下包含 eiid=1014 长按事件,该事件触发时会携带一个 piid=2 的按键类型属性作为事件参数, debug 等级日志会打印 `长按, attributes: {'按键类型': 1}` (日志示例,按键类型为 1 表示右键触发了长按事件)。
- 方法Action - 方法Action
| in输入参数列表 | 转换后的实体 | | in输入参数列表 | 转换后的实体 |
@ -291,39 +293,37 @@ event instance name 下的值表示转换后实体所用的 `_attr_device_class`
### MIoT-Spec-V2 过滤规则 ### MIoT-Spec-V2 过滤规则
`spec_filter.json` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。 `spec_filter.yaml` 用于过滤掉不需要的 MIoT-Spec-V2 实例,过滤掉的实例不会转换成 Home Assistant 实体。
`spec_filter.json`的格式如下: `spec_filter.yaml`的格式如下:
``` ```yaml
{ <MIoT-Spec-V2 device instance urn without the version field>:
"<MIoT-Spec-V2 device instance>":{ services: list<service_iid: str>
"services": list<service_iid: str>, properties: list<service_iid.property_iid: str>
"properties": list<service_iid.property_iid: str>, events: list<service_iid.event_iid: str>
"events": list<service_iid.event_iid: str>, actions: list<service_iid.action_iid: str>
"actions": list<service_iid.action_iid: str>,
}
}
``` ```
`spec_filter.json` 的键值为 MIoT-Spec-V2 设备实例的 urn 不含版本号“version”字段。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.json` 的键值不需要指定设备实例的版本号。 `spec_filter.yaml` 的键值为 MIoT-Spec-V2 设备实例的 urn 不含版本号“version”字段。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时,高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此, `spec_filter.yaml` 的键值不需要指定设备实例的版本号。
设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。 设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号( iid ,即 instance id )。支持通配符匹配。
示例: 示例:
``` ```yaml
{ urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:
"urn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1":{ services:
"services": ["*"] # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2 device instance. - '*' # 排除所有服务,相当于排除拥有该 MIoT-Spec-V2 的设备。
}, urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": { services:
"services": ["3"], # Filter out the service whose iid=3. - '3' # 排除 siid=3 的服务。
"properties": ["4.*"] # Filter out all properties in the service whose iid=4. properties:
"events": ["4.1"], # Filter out the iid=1 event in the iid=4 service. - '4.*' # 排除 siid=4 服务的所有属性。
"actions": ["4.1"] # Filter out the iid=1 action in the iid=4 service. events:
} - '4.1' # 排除 siid=4 服务的 eiid=1 的事件。
} actions:
- '4.1' # 排除 siid=4 服务的 aiid=1 的方法。
``` ```
所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。 所有设备的设备信息服务( urn:miot-spec-v2:service:device-information:00007801 )均不会生成 Home Assistant 实体。
@ -378,7 +378,7 @@ siid、piid、eiid、aiid、value 均为十进制三位整数。
} }
``` ```
> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的任何文件(`spec_filter.py`、`spec_modify.json`、`multi_lang.json`等),需要在集成配置中更新实体转换规则才能生效。方法:[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则 > 在 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) > 配置 > 更新实体转换规则
## 文档 ## 文档