Compare commits

...

8 Commits

Author SHA1 Message Date
Gavin
7142283370
Merge f5bc880c62 into 001af5384a 2026-01-09 21:03:20 +08:00
Li Shuzhen
001af5384a
docs: update changelog and version to v0.4.7 (#1599)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2026-01-07 10:45:07 +08:00
Li Shuzhen
c8d48625e9
Fix specs (#1567)
* fix: the playing-state property's access field of xiaomi.wifispeaker.lx04 and xiaomi.wifispeaker.lx06 (#1566)

* fix: the playing-state property's access field of xiaomi.wifispeaker.x08c

* feat: remove xiaomi.router.rd03 from UNSUPPORTED_MODELS and add era.airp.cwb03 into it

* fix: xiaomi.airc.h09h00 humidity-range unit (#1589)

* fix: add k0918.toothbrush.t700 into UNSUPPORTED_MODELS (#1585)

* fix: the playing-state property's access field of xiaomi.wifispeaker.l04m (#1596)
2026-01-07 10:04:42 +08:00
GavinIves
f5bc880c62 fixbug 2025-12-28 15:24:34 +00:00
GavinIves
4c7b6d570d sync main 2025-12-28 15:10:02 +00:00
GavinIves
b95a2c18ee review 2025-12-28 14:29:55 +00:00
GavinIves
5dc6f400c2 fomatted code 2025-12-28 13:46:30 +00:00
GavinIves
006a445e3a Added command sending mode for lights to optimize the lighting effect 2025-12-28 13:35:02 +00:00
11 changed files with 518 additions and 91 deletions

View File

@ -1,4 +1,16 @@
# CHANGELOG # CHANGELOG
## v0.4.7
### Added
- Add turkish language in multi_lang.json. [#1593](https://github.com/XiaoMi/ha_xiaomi_home/pull/1593)
### Changed
- Remove unused info getting from central hub gateway. [#1574](https://github.com/XiaoMi/ha_xiaomi_home/pull/1574)
- Remove xiaomi.router.rd03 from `UNSUPPORTED_MODELS` and add era.airp.cwb03, k0918.toothbrush.t700 into it. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)
### Fixed
- Update the BLE mesh device online state from the central hub gateway. Partially fix the BLE mesh device online state. [#1579](https://github.com/XiaoMi/ha_xiaomi_home/pull/1579)
- Add unit for xiaomi.toothbrush.p001 brush-head-left-level property. [#1588](https://github.com/XiaoMi/ha_xiaomi_home/pull/1588)
- Fix the playing-state property's access field of xiaomi.wifispeaker.lx04, xiaomi.wifispeaker.lx06, xiaomi.wifispeaker.x08c and xiaomi.wifispeaker.l04m. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)
- Fix the MIoT-Spec-V2 of xiaomi.airc.h09h00 humidity-range unit. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)
## v0.4.6 ## v0.4.6
### Added ### Added
- Add tv-box device as the media player entity. [#1562](https://github.com/XiaoMi/ha_xiaomi_home/pull/1562) - Add tv-box device as the media player entity. [#1562](https://github.com/XiaoMi/ha_xiaomi_home/pull/1562)
@ -12,7 +24,7 @@
- Catch paho-mqtt subscribe error properly. [#1551](https://github.com/XiaoMi/ha_xiaomi_home/pull/1551) - Catch paho-mqtt subscribe error properly. [#1551](https://github.com/XiaoMi/ha_xiaomi_home/pull/1551)
- After the network resumes, keep retrying to fetch the device list until it succeeds. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555) - After the network resumes, keep retrying to fetch the device list until it succeeds. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555)
- Catch the http post error properly. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555) - Catch the http post error properly. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555)
- Fixed the format and the access field of daikin.aircondition.k2 and fix: daikin.airfresh.k33 string value properties. [#1561](https://github.com/XiaoMi/ha_xiaomi_home/pull/1561) - Fix the format and the access field of daikin.aircondition.k2 and daikin.airfresh.k33 string value properties. [#1561](https://github.com/XiaoMi/ha_xiaomi_home/pull/1561)
## v0.4.5 ## v0.4.5
### Changed ### Changed

View File

@ -47,11 +47,12 @@ Light entities for Xiaomi Home.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, Optional from typing import Any, Optional, List, Dict
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.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN, ATTR_COLOR_TEMP_KELVIN,
@ -86,7 +87,7 @@ async def async_setup_entry(
for miot_device in device_list: for miot_device in device_list:
for data in miot_device.entity_list.get('light', []): for data in miot_device.entity_list.get('light', []):
new_entities.append( new_entities.append(
Light(miot_device=miot_device, entity_data=data)) Light(miot_device=miot_device, entity_data=data, hass=hass))
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
@ -106,10 +107,13 @@ class Light(MIoTServiceEntity, LightEntity):
_mode_map: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData,hass: HomeAssistant
) -> None: ) -> None:
"""Initialize the Light.""" """Initialize the Light."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self.hass = hass
self.miot_device = miot_device
self._command_send_mode_entity_id = None
self._attr_color_mode = None self._attr_color_mode = None
self._attr_supported_color_modes = set() self._attr_supported_color_modes = set()
self._attr_supported_features = LightEntityFeature(0) self._attr_supported_features = LightEntityFeature(0)
@ -252,42 +256,178 @@ class Light(MIoTServiceEntity, LightEntity):
""" """
# on # on
# Dirty logic for lumi.gateway.mgl03 indicator light # Dirty logic for lumi.gateway.mgl03 indicator light
if self._prop_on: # Determine whether the device sends the light-on properties in batches or one by one
value_on = True if self._prop_on.format_ == bool else 1 # Search entityid through unique_id to avoid the user modifying entityid and causing command_send_mode to not match
await self.set_property_async( # 获取开灯模式
prop=self._prop_on, value=value_on) if self._command_send_mode_entity_id is None:
# brightness entity_registry = async_get_entity_registry(self.hass)
device_id = list(
self.miot_device.device_info.get("identifiers"))[0][1]
self._command_send_mode_entity_id = entity_registry.async_get_entity_id(
"select", DOMAIN, f"select.light_{device_id}_command_send_mode")
if self._command_send_mode_entity_id is None:
_LOGGER.error(
"light command_send_mode not found, %s",
self.entity_id,
)
return
command_send_mode = self.hass.states.get(
self._command_send_mode_entity_id)
# 判断是先发送亮度还是先发送色温
send_brightness_first = False
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value( brightness_new = kwargs[ATTR_BRIGHTNESS]
self._brightness_scale, kwargs[ATTR_BRIGHTNESS]) brightness_old = self.brightness
await self.set_property_async( if brightness_old and brightness_new <= brightness_old:
prop=self._prop_brightness, value=brightness, send_brightness_first = True
write_ha_state=False)
# color-temperature # 开始发送开灯命令
if ATTR_COLOR_TEMP_KELVIN in kwargs: if command_send_mode and command_send_mode.state == "Send Together":
await self.set_property_async( set_properties_list: List[Dict[str, Any]] = []
prop=self._prop_color_temp, # mode
value=kwargs[ATTR_COLOR_TEMP_KELVIN], if ATTR_EFFECT in kwargs:
write_ha_state=False) set_properties_list.append({
self._attr_color_mode = ColorMode.COLOR_TEMP "prop":self._prop_mode,
# rgb color "value":self.get_map_key(
if ATTR_RGB_COLOR in kwargs: map_=self._mode_map,value=kwargs[ATTR_EFFECT]),
r = kwargs[ATTR_RGB_COLOR][0] })
g = kwargs[ATTR_RGB_COLOR][1] # brightness
b = kwargs[ATTR_RGB_COLOR][2] if send_brightness_first and ATTR_BRIGHTNESS in kwargs:
rgb = (r << 16) | (g << 8) | b brightness = brightness_to_value(
await self.set_property_async( self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
prop=self._prop_color, value=rgb, set_properties_list.append({
write_ha_state=False) "prop": self._prop_brightness,
self._attr_color_mode = ColorMode.RGB "value": brightness
# mode })
if ATTR_EFFECT in kwargs: # color-temperature
await self.set_property_async( if ATTR_COLOR_TEMP_KELVIN in kwargs:
prop=self._prop_mode, set_properties_list.append({
value=self.get_map_key( "prop": self._prop_color_temp,
map_=self._mode_map, value=kwargs[ATTR_EFFECT]), "value": kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False) })
self.async_write_ha_state() self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
set_properties_list.append({
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB
# brightness
if not send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
set_properties_list.append({
"prop": self._prop_on,
"value": value_on
})
await self.set_properties_async(set_properties_list,write_ha_state=False)
self.async_write_ha_state()
elif command_send_mode and command_send_mode.state == "Send Turn On First":
set_properties_list: List[Dict[str, Any]] = []
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
set_properties_list.append({
"prop": self._prop_on,
"value": value_on
})
# mode
if ATTR_EFFECT in kwargs:
set_properties_list.append({
"prop":
self._prop_mode,
"value":
self.get_map_key(
map_=self._mode_map,value=kwargs[ATTR_EFFECT]),
})
# brightness
if send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
# color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
set_properties_list.append({
"prop": self._prop_color_temp,
"value": kwargs[ATTR_COLOR_TEMP_KELVIN],
})
self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
set_properties_list.append({
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB
# brightness
if not send_brightness_first and ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale,kwargs[ATTR_BRIGHTNESS])
set_properties_list.append({
"prop": self._prop_brightness,
"value": brightness
})
await self.set_properties_async(set_properties_list,write_ha_state=False)
self.async_write_ha_state()
else:
if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1
await self.set_property_async(
prop=self._prop_on, value=value_on)
# brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(
self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
await self.set_property_async(
prop=self._prop_brightness, value=brightness,
write_ha_state=False)
# color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs:
await self.set_property_async(
prop=self._prop_color_temp,
value=kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False)
self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color
if ATTR_RGB_COLOR in kwargs:
r = kwargs[ATTR_RGB_COLOR][0]
g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b
await self.set_property_async(
prop=self._prop_color, value=rgb,
write_ha_state=False)
self._attr_color_mode = ColorMode.RGB
# mode
if ATTR_EFFECT in kwargs:
await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
write_ha_state=False)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off.""" """Turn the light off."""

View File

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

View File

@ -90,8 +90,9 @@ SUPPORTED_PLATFORMS: list = [
UNSUPPORTED_MODELS: list = [ UNSUPPORTED_MODELS: list = [
'chuangmi.ir.v2', 'chuangmi.ir.v2',
'era.airp.cwb03',
'hmpace.motion.v6nfc', 'hmpace.motion.v6nfc',
'xiaomi.router.rd03' 'k0918.toothbrush.t700'
] ]
DEFAULT_CLOUD_SERVER: str = 'cn' DEFAULT_CLOUD_SERVER: str = 'cn'

View File

@ -46,7 +46,7 @@ off Xiaomi or its affiliates' products.
MIoT client instance. MIoT client instance.
""" """
from copy import deepcopy from copy import deepcopy
from typing import Any, Callable, Optional, final from typing import Any, Callable, Optional, final, Dict, List
import asyncio import asyncio
import json import json
import logging import logging
@ -710,6 +710,84 @@ class MIoTClient:
f'{self._i18n.translate("miot.client.device_exec_error")}, ' f'{self._i18n.translate("miot.client.device_exec_error")}, '
f'{self._i18n.translate("error.common.-10007")}') f'{self._i18n.translate("error.common.-10007")}')
async def set_props_async(
self, props_list: List[Dict[str, Any]],
) -> bool:
# props_list = [{'did': str, 'siid': int, 'piid': int, 'value': Any}......]
# 判断是不是只有一个did
did_set = {prop["did"] for prop in props_list}
if len(did_set) != 1:
raise MIoTClientError(f"more than one or no did once, {did_set}")
did = did_set.pop()
if did not in self._device_list_cache:
raise MIoTClientError(f"did not exist, {did}")
# Priority local control
if self._ctrl_mode == CtrlMode.AUTO:
# Gateway control
device_gw = self._device_list_gateway.get(did, None)
if (
device_gw and device_gw.get("online", False)
and device_gw.get("specv2_access", False) and "group_id" in device_gw
):
mips = self._mips_local.get(device_gw["group_id"], None)
if mips is None:
_LOGGER.error(
"no gateway route, %s, try control through cloud",
device_gw)
else:
result = await mips.set_props_async(
did=did,props_list=props_list)
_LOGGER.debug("gateway set props, %s -> %s", props_list, result)
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
else:
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Lan control
device_lan = self._device_list_lan.get(did, None)
if device_lan and device_lan.get("online", False):
result = await self._miot_lan.set_props_async(
did=did, props_list=props_list)
_LOGGER.debug("lan set props, %s -> %s", props_list, result)
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
else:
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Cloud control
device_cloud = self._device_list_cloud.get(did, None)
if device_cloud and device_cloud.get("online", False):
result = await self._http.set_props_async(params=props_list)
_LOGGER.debug(
"cloud set props, %s, result, %s",
props_list, result)
if result and len(result) == len(props_list):
rc = [(r or {}).get("code",
MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)
for r in result]
if all(t in [0, 1] for t in rc):
return True
if any(t in [-704010000, -704042011] for t in rc):
# Device remove or offline
_LOGGER.error("device may be removed or offline, %s", did)
self._main_loop.create_task(
await
self.__refresh_cloud_device_with_dids_async(dids=[did]))
raise MIoTClientError(
self.__get_exec_error_with_rc(rc=next(x for x in rc if x not in (0, 1))))
# Show error message
raise MIoTClientError(
f'{self._i18n.translate("miot.client.device_exec_error")}, '
f'{self._i18n.translate("error.common.-10007")}')
def request_refresh_prop( def request_refresh_prop(
self, did: str, siid: int, piid: int self, did: str, siid: int, piid: int
) -> None: ) -> None:

View File

@ -825,6 +825,22 @@ class MIoTHttpClient:
return res_obj['result'] return res_obj['result']
async def set_props_async(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
"""
res_obj = await self.__mihome_api_post_async(
url_path='/app/v2/miotspec/prop/set',
data={
'params': params
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list[dict] self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict: ) -> dict:

View File

@ -47,7 +47,7 @@ MIoT device instance.
""" """
import asyncio import asyncio
from abc import abstractmethod from abc import abstractmethod
from typing import Any, Callable, Optional from typing import Any, Callable, Optional, Dict, List
import logging import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -682,58 +682,66 @@ class MIoTDevice:
def unit_convert(self, spec_unit: str) -> Optional[str]: def unit_convert(self, spec_unit: str) -> Optional[str]:
"""Convert MIoT unit to Home Assistant unit. """Convert MIoT unit to Home Assistant unit.
25/01/20: All online prop unit statistical tables: unit, quantity. 2026/01/06: property unit statistics of the latest released
MIoT-Spec-V2 for all device models: unit, quantity.
{ {
"no_unit": 148499, "no_unit": 148499,
"percentage": 10042, "percentage": 12074,
"kelvin": 1895, "none": 11857,
"rgb": 772, // color "minutes": 5707,
"celsius": 5762, "celsius": 5767,
"none": 16106, "seconds": 3062,
"hours": 1540, "kelvin": 2511,
"minutes": 5061, "hours": 1380,
"ms": 27, "days": 615,
"watt": 216, "rgb": 752, // color
"arcdegrees": 159, "L": 379,
"ppm": 177, "mg/m3": 335,
"μg/m3": 106, "ppm": 182,
"days": 571, "watt": 246,
"seconds": 2749, "arcdegrees": 130,
"μg/m3": 117,
"kWh": 149,
"ms": 108,
"pascal": 108,
"lux": 100,
"V": 59,
"m": 45,
"A": 36,
"mL": 30,
"arcdegress": 25,
"mA": 26,
"bpm": 21, // realtime-heartrate
"B/s": 21, "B/s": 21,
"pascal": 110, "weeks": 18,
"mg/m3": 339, "dB": 17,
"lux": 125, "calorie": 18, // 1 cal = 4.184 J
"kWh": 124, "metre": 15,
"mv": 2, "hour": 11,
"V": 38, "cm": 12,
"A": 29, "gram": 8,
"mV": 4, "km/h": 8,
"L": 352, "mV": 9,
"m": 37, "times": 4, // exercise-count
"毫摩尔每升": 2, // blood-sugar, cholesterol "kCal": 4,
"mmol/L": 1, // urea
"weeks": 26,
"meter": 3,
"dB": 26,
"hour": 14,
"calorie": 19, // 1 cal = 4.184 J
"ppb": 3,
"arcdegress": 30,
"bpm": 4, // realtime-heartrate
"gram": 7,
"km/h": 9,
"W": 1,
"m3/h": 2,
"kilopascal": 1,
"mL": 4,
"mmHg": 4, "mmHg": 4,
"pcs": 3,
"meter": 3,
"kW": 2,
"KByte/s": 2,
"毫摩尔每升": 2, // blood-sugar, cholesterol
"m3/h": 2,
"ppb": 2,
"mv": 2,
"w": 1, "w": 1,
"bar": 1,
"megapascal": 1,
"kB": 1,
"mmol/L": 1, // urea
"min/km": 1,
"kilopascal": 1,
"liter": 1, "liter": 1,
"cm": 3, "W": 1
"mA": 2,
"kilogram": 2,
"kcal/d": 2, // basal-metabolism
"times": 1 // exercise-count
} }
""" """
unit_map = { unit_map = {
@ -1082,6 +1090,49 @@ class MIoTServiceEntity(Entity):
self.async_write_ha_state() self.async_write_ha_state()
return True return True
async def set_properties_async(
self, set_properties_list: List[Dict[str, Any]],
update_value: bool = True, write_ha_state: bool = True,
) -> bool:
# set_properties_list = [{'prop': Optional[MIoTSpecProperty],
# 'value': Any}....]
for set_property in set_properties_list:
prop = set_property.get("prop")
value = set_property.get("value")
if not prop:
raise RuntimeError(
f'set property failed, property is None, '
f'{self.entity_id}, {self.name}')
value = prop.value_format(value)
value = prop.value_precision(value)
# 因为下面还有判断在这个循环里 所以这里要赋值回去
set_property["value"] = value
if prop not in self.entity_data.props:
raise RuntimeError(
f'set property failed, unknown property, '
f'{self.entity_id}, {self.name}, {prop.name}')
if not prop.writable:
raise RuntimeError(
f'set property failed, not writable, '
f'{self.entity_id}, {self.name}, {prop.name}')
try:
await self.miot_device.miot_client.set_props_async([{
"did": self.miot_device.did,
"siid": set_property["prop"].service.iid,
"piid": set_property["prop"].iid,
"value": set_property["value"],
} for set_property in set_properties_list])
except MIoTClientError as e:
raise RuntimeError(
f"{e}, {self.entity_id}, {self.name}, {'&'.join([set_property['prop'].name for set_property in set_properties_list])}") from e
if update_value:
for set_property in set_properties_list:
self._prop_value_map[
set_property["prop"]] = set_property["value"]
if write_ha_state:
self.async_write_ha_state()
return True
async def get_property_async(self, prop: MIoTSpecProperty) -> Any: async def get_property_async(self, prop: MIoTSpecProperty) -> Any:
if not prop: if not prop:
_LOGGER.error( _LOGGER.error(

View File

@ -58,7 +58,7 @@ import secrets
import socket import socket
import struct import struct
import threading import threading
from typing import Any, Callable, Coroutine, Optional, final from typing import Any, Callable, Coroutine, Optional, final, Dict, List
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -857,6 +857,29 @@ class MIoTLan:
return result_obj return result_obj
raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR) raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)
@final
async def set_props_async(
self,did: str,props_list: List[Dict[str, Any]],
timeout_ms: int = 10000) -> dict:
# props_list = [{'did': did, 'siid': siid, 'piid': piid, 'value': value}......]
self.__assert_service_ready()
result_obj = await self.__call_api_async(
did=did, msg={
'method': 'set_properties',
'params': props_list,
}, timeout_ms=timeout_ms)
if result_obj:
if (
'result' in result_obj and
len(result_obj['result']) == len(props_list)
and result_obj['result'][0].get('did') == did
and all('code' in item for item in result_obj['result'])
):
return result_obj['result']
if 'code' in result_obj:
return result_obj
raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)
@final @final
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list, self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -56,7 +56,7 @@ import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import Any, Callable, Optional, final, Coroutine from typing import Any, Callable, Optional, final, Coroutine, Dict, List
from paho.mqtt.client import ( from paho.mqtt.client import (
MQTT_ERR_SUCCESS, MQTT_ERR_SUCCESS,
@ -1342,6 +1342,41 @@ class MipsLocalClient(_MipsClient):
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value, 'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
'message': 'Invalid result'} 'message': 'Invalid result'}
@final
async def set_props_async(
self, did: str, props_list: List[Dict[str, Any]],
timeout_ms: int = 10000
) -> dict:
# props_list= [{
# 'did': did,
# 'siid': siid,
# 'piid': piid,
# 'value': value
# }]
payload_obj: dict = {
"did": did,
"rpc": {
"id": self.__gen_mips_id,
"method": "set_properties",
"params": props_list,
}
}
result_obj = await self.__request_async(
topic="proxy/rpcReq",
payload=json.dumps(payload_obj),
timeout_ms=timeout_ms)
if result_obj:
if ("result" in result_obj and
len(result_obj["result"]) == len(props_list) and
result_obj["result"][0].get("did") == did and
all("code" in item for item in result_obj["result"])):
return result_obj["result"]
if "error" in result_obj:
return result_obj["error"]
return {
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
'message': 'Invalid result'}
@final @final
async def action_async( async def action_async(
self, did: str, siid: int, aiid: int, in_list: list, self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -54,6 +54,9 @@ urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1:
prop.10.6: prop.10.6:
unit: none unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1 urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h09h00:4:
prop.10.6:
unit: none
urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h40h00:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h40h00:1:
prop.10.6: prop.10.6:
unit: none unit: none
@ -499,6 +502,26 @@ urn:miot-spec-v2:device:safe-box:0000A042:loock-v1:1:
prop.5.1: prop.5.1:
name: contact-state name: contact-state
expr: (src_value!=1) expr: (src_value!=1)
urn:miot-spec-v2:device:speaker:0000A015:xiaomi-l04m:2:
prop.3.1:
access:
- read
- notify
urn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx04:2:
prop.3.1:
access:
- read
- notify
urn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx06:2:
prop.3.1:
access:
- read
- notify
urn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08c:2:
prop.2.1:
access:
- read
- notify
urn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08e:1: urn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08e:1:
prop.3.1: prop.3.1:
name: playing-state-a name: playing-state-a

View File

@ -52,6 +52,8 @@ 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.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.restore_state import RestoreEntity
from .miot.const import DOMAIN from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTPropertyEntity from .miot.miot_device import MIoTDevice, MIoTPropertyEntity
@ -75,6 +77,17 @@ async def async_setup_entry(
if new_entities: if new_entities:
async_add_entities(new_entities) async_add_entities(new_entities)
# create select for light
new_light_select_entities = []
for miot_device in device_list:
# Add it to all devices with light entities, because some bathroom heaters and clothes drying racks also have lights.
# if "device:light" in miot_device.spec_instance.urn:
if miot_device.entity_list.get("light", []):
device_id = list(miot_device.device_info.get("identifiers"))[0][1]
new_light_select_entities.append(
LightCommandSendMode(hass=hass, device_id=device_id))
if new_light_select_entities:
async_add_entities(new_light_select_entities)
class Select(MIoTPropertyEntity, SelectEntity): class Select(MIoTPropertyEntity, SelectEntity):
"""Select entities for Xiaomi Home.""" """Select entities for Xiaomi Home."""
@ -94,3 +107,38 @@ class Select(MIoTPropertyEntity, SelectEntity):
def current_option(self) -> Optional[str]: def current_option(self) -> Optional[str]:
"""Return the current selected option.""" """Return the current selected option."""
return self.get_vlist_description(value=self._value) return self.get_vlist_description(value=self._value)
class LightCommandSendMode(SelectEntity, RestoreEntity):
"""To control whether to turn on the light, you need to send the light-on command first and
then send other color temperatures and brightness or send them all at the same time.
The default is to send one by one."""
def __init__(self, hass: HomeAssistant, device_id: str):
super().__init__()
self.hass = hass
self._device_id = device_id
self._attr_name = "Command Send Mode"
self.entity_id = f"select.light_{device_id}_command_send_mode"
self._attr_unique_id = self.entity_id
self._attr_options = [
"Send One by One", "Send Turn On First", "Send Together"
]
self._attr_device_info = {"identifiers": {(DOMAIN, device_id)}}
self._attr_current_option = self._attr_options[0]
self._attr_entity_category = EntityCategory.CONFIG
async def async_select_option(self, option: str):
if option in self._attr_options:
self._attr_current_option = option
self.async_write_ha_state()
async def async_added_to_hass(self):
await super().async_added_to_hass()
if (last_state := await self.async_get_last_state()
) and last_state.state in self._attr_options:
self._attr_current_option = last_state.state
@property
def current_option(self):
return self._attr_current_option