Compare commits

...

6 Commits

Author SHA1 Message Date
Gavin
8f02af3fe1
Merge f5bc880c62 into d4ac7a935e 2026-01-05 21:01:55 -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
7 changed files with 425 additions and 42 deletions

View File

@ -47,11 +47,12 @@ Light entities for Xiaomi Home.
"""
from __future__ import annotations
import logging
from typing import Any, Optional
from typing import Any, Optional, List, Dict
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
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 (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
@ -86,7 +87,7 @@ async def async_setup_entry(
for miot_device in device_list:
for data in miot_device.entity_list.get('light', []):
new_entities.append(
Light(miot_device=miot_device, entity_data=data))
Light(miot_device=miot_device, entity_data=data, hass=hass))
if new_entities:
async_add_entities(new_entities)
@ -106,10 +107,13 @@ class Light(MIoTServiceEntity, LightEntity):
_mode_map: Optional[dict[Any, Any]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
self, miot_device: MIoTDevice, entity_data: MIoTEntityData,hass: HomeAssistant
) -> None:
"""Initialize the Light."""
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_supported_color_modes = set()
self._attr_supported_features = LightEntityFeature(0)
@ -252,42 +256,178 @@ class Light(MIoTServiceEntity, LightEntity):
"""
# on
# Dirty logic for lumi.gateway.mgl03 indicator light
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
# Determine whether the device sends the light-on properties in batches or one by one
# Search entityid through unique_id to avoid the user modifying entityid and causing command_send_mode to not match
# 获取开灯模式
if self._command_send_mode_entity_id is None:
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:
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()
brightness_new = kwargs[ATTR_BRIGHTNESS]
brightness_old = self.brightness
if brightness_old and brightness_new <= brightness_old:
send_brightness_first = True
# 开始发送开灯命令
if command_send_mode and command_send_mode.state == "Send Together":
set_properties_list: List[Dict[str, Any]] = []
# 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
})
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:
"""Turn the light off."""

View File

@ -46,7 +46,7 @@ off Xiaomi or its affiliates' products.
MIoT client instance.
"""
from copy import deepcopy
from typing import Any, Callable, Optional, final
from typing import Any, Callable, Optional, final, Dict, List
import asyncio
import json
import logging
@ -710,6 +710,84 @@ class MIoTClient:
f'{self._i18n.translate("miot.client.device_exec_error")}, '
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(
self, did: str, siid: int, piid: int
) -> None:

View File

@ -825,6 +825,22 @@ class MIoTHttpClient:
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(
self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict:

View File

@ -47,7 +47,7 @@ MIoT device instance.
"""
import asyncio
from abc import abstractmethod
from typing import Any, Callable, Optional
from typing import Any, Callable, Optional, Dict, List
import logging
from homeassistant.helpers.entity import Entity
@ -1082,6 +1082,49 @@ class MIoTServiceEntity(Entity):
self.async_write_ha_state()
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:
if not prop:
_LOGGER.error(

View File

@ -58,7 +58,7 @@ import secrets
import socket
import struct
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 import padding
from cryptography.hazmat.backends import default_backend
@ -857,6 +857,29 @@ class MIoTLan:
return result_obj
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
async def action_async(
self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -56,7 +56,7 @@ import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
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 (
MQTT_ERR_SUCCESS,
@ -1342,6 +1342,41 @@ class MipsLocalClient(_MipsClient):
'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,
'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
async def action_async(
self, did: str, siid: int, aiid: int, in_list: list,

View File

@ -52,6 +52,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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.miot_device import MIoTDevice, MIoTPropertyEntity
@ -75,6 +77,17 @@ async def async_setup_entry(
if 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):
"""Select entities for Xiaomi Home."""
@ -94,3 +107,38 @@ class Select(MIoTPropertyEntity, SelectEntity):
def current_option(self) -> Optional[str]:
"""Return the current selected option."""
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