fomatted code

This commit is contained in:
GavinIves 2025-06-04 09:05:45 +00:00
parent 83899f8084
commit 3d50562eec
7 changed files with 1496 additions and 1538 deletions

View File

@ -77,14 +77,14 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][ device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][config_entry.entry_id]
config_entry.entry_id]
new_entities = [] new_entities = []
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, hass=hass)) 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)
@ -104,8 +104,9 @@ class Light(MIoTServiceEntity, LightEntity):
_brightness_scale: Optional[tuple[int, int]] _brightness_scale: Optional[tuple[int, int]]
_mode_map: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData, def __init__(
hass: HomeAssistant) -> None: self, miot_device: MIoTDevice, entity_data: MIoTEntityData, hass: HomeAssistant
) -> 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.hass = hass
@ -144,8 +145,7 @@ class Light(MIoTServiceEntity, LightEntity):
self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop self._prop_mode = prop
else: else:
_LOGGER.info("invalid brightness format, %s", _LOGGER.info("invalid brightness format, %s", self.entity_id)
self.entity_id)
continue continue
# color-temperature # color-temperature
if prop.name == "color-temperature": if prop.name == "color-temperature":
@ -172,9 +172,13 @@ class Light(MIoTServiceEntity, LightEntity):
mode_list = prop.value_list.to_map() mode_list = prop.value_list.to_map()
elif prop.value_range: elif prop.value_range:
mode_list = {} mode_list = {}
if (int((prop.value_range.max_ - prop.value_range.min_) / if (
prop.value_range.step) int(
> self._VALUE_RANGE_MODE_COUNT_MAX): (prop.value_range.max_ - prop.value_range.min_)
/ prop.value_range.step
)
> self._VALUE_RANGE_MODE_COUNT_MAX
):
_LOGGER.error( _LOGGER.error(
"too many mode values, %s, %s, %s", "too many mode values, %s, %s, %s",
self.entity_id, self.entity_id,
@ -183,9 +187,9 @@ class Light(MIoTServiceEntity, LightEntity):
) )
else: else:
for value in range( for value in range(
prop.value_range.min_, prop.value_range.min_,
prop.value_range.max_, prop.value_range.max_,
prop.value_range.step, prop.value_range.step,
): ):
mode_list[value] = f"mode {value}" mode_list[value] = f"mode {value}"
if mode_list: if mode_list:
@ -241,8 +245,9 @@ class Light(MIoTServiceEntity, LightEntity):
@property @property
def effect(self) -> Optional[str]: def effect(self) -> Optional[str]:
"""Return the current mode.""" """Return the current mode."""
return self.get_map_value(map_=self._mode_map, return 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)
)
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None:
"""Turn the light on. """Turn the light on.
@ -258,24 +263,24 @@ class Light(MIoTServiceEntity, LightEntity):
set_properties_list: List[Dict[str, Any]] = [] set_properties_list: List[Dict[str, Any]] = []
if self._prop_on: if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721
set_properties_list.append({ set_properties_list.append({"prop": self._prop_on, "value": value_on})
"prop": self._prop_on,
"value": value_on
})
# brightness # brightness
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(self._brightness_scale, brightness = brightness_to_value(
kwargs[ATTR_BRIGHTNESS]) self._brightness_scale, kwargs[ATTR_BRIGHTNESS]
set_properties_list.append({ )
"prop": self._prop_brightness, set_properties_list.append(
"value": brightness {"prop": self._prop_brightness, "value": brightness}
}) )
# color-temperature # color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs: if ATTR_COLOR_TEMP_KELVIN in kwargs:
set_properties_list.append({ set_properties_list.append(
"prop": self._prop_color_temp, {
"value": kwargs[ATTR_COLOR_TEMP_KELVIN], "prop": self._prop_color_temp,
}) "value": kwargs[ATTR_COLOR_TEMP_KELVIN],
}
)
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color # rgb color
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
@ -283,46 +288,42 @@ class Light(MIoTServiceEntity, LightEntity):
g = kwargs[ATTR_RGB_COLOR][1] g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2] b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b rgb = (r << 16) | (g << 8) | b
set_properties_list.append({ set_properties_list.append({"prop": self._prop_color, "value": rgb})
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB self._attr_color_mode = ColorMode.RGB
# mode # mode
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
set_properties_list.append({ set_properties_list.append(
"prop": {
self._prop_mode, "prop": self._prop_mode,
"value": "value": self.get_map_key(
self.get_map_key(map_=self._mode_map, map_=self._mode_map, value=kwargs[ATTR_EFFECT]
value=kwargs[ATTR_EFFECT]), ),
}) }
)
await self.set_properties_async(set_properties_list) await self.set_properties_async(set_properties_list)
self.async_write_ha_state() self.async_write_ha_state()
elif command_send_mode and command_send_mode.state == "Send Turn On First": elif command_send_mode and command_send_mode.state == "Send Turn On First":
set_properties_list: List[Dict[str, Any]] = [] set_properties_list: List[Dict[str, Any]] = []
if self._prop_on: if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721
set_properties_list.append({ set_properties_list.append({"prop": self._prop_on, "value": value_on})
"prop": self._prop_on, await self.set_property_async(prop=self._prop_on, value=value_on)
"value": value_on
})
await self.set_property_async(prop=self._prop_on,
value=value_on)
# brightness # brightness
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(self._brightness_scale, brightness = brightness_to_value(
kwargs[ATTR_BRIGHTNESS]) self._brightness_scale, kwargs[ATTR_BRIGHTNESS]
set_properties_list.append({ )
"prop": self._prop_brightness, set_properties_list.append(
"value": brightness {"prop": self._prop_brightness, "value": brightness}
}) )
# color-temperature # color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs: if ATTR_COLOR_TEMP_KELVIN in kwargs:
set_properties_list.append({ set_properties_list.append(
"prop": self._prop_color_temp, {
"value": kwargs[ATTR_COLOR_TEMP_KELVIN], "prop": self._prop_color_temp,
}) "value": kwargs[ATTR_COLOR_TEMP_KELVIN],
}
)
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
# rgb color # rgb color
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
@ -330,35 +331,33 @@ class Light(MIoTServiceEntity, LightEntity):
g = kwargs[ATTR_RGB_COLOR][1] g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2] b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b rgb = (r << 16) | (g << 8) | b
set_properties_list.append({ set_properties_list.append({"prop": self._prop_color, "value": rgb})
"prop": self._prop_color,
"value": rgb
})
self._attr_color_mode = ColorMode.RGB self._attr_color_mode = ColorMode.RGB
# mode # mode
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
set_properties_list.append({ set_properties_list.append(
"prop": {
self._prop_mode, "prop": self._prop_mode,
"value": "value": self.get_map_key(
self.get_map_key(map_=self._mode_map, map_=self._mode_map, value=kwargs[ATTR_EFFECT]
value=kwargs[ATTR_EFFECT]), ),
}) }
)
await self.set_properties_async(set_properties_list) await self.set_properties_async(set_properties_list)
self.async_write_ha_state() self.async_write_ha_state()
else: else:
if self._prop_on: if self._prop_on:
value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721 value_on = True if self._prop_on.format_ == bool else 1 # noqa: E721
await self.set_property_async(prop=self._prop_on, await self.set_property_async(prop=self._prop_on, value=value_on)
value=value_on)
# brightness # brightness
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = brightness_to_value(self._brightness_scale, brightness = brightness_to_value(
kwargs[ATTR_BRIGHTNESS]) self._brightness_scale, kwargs[ATTR_BRIGHTNESS]
await self.set_property_async(prop=self._prop_brightness, )
value=brightness, await self.set_property_async(
write_ha_state=False) prop=self._prop_brightness, value=brightness, write_ha_state=False
)
# color-temperature # color-temperature
if ATTR_COLOR_TEMP_KELVIN in kwargs: if ATTR_COLOR_TEMP_KELVIN in kwargs:
await self.set_property_async( await self.set_property_async(
@ -373,16 +372,17 @@ class Light(MIoTServiceEntity, LightEntity):
g = kwargs[ATTR_RGB_COLOR][1] g = kwargs[ATTR_RGB_COLOR][1]
b = kwargs[ATTR_RGB_COLOR][2] b = kwargs[ATTR_RGB_COLOR][2]
rgb = (r << 16) | (g << 8) | b rgb = (r << 16) | (g << 8) | b
await self.set_property_async(prop=self._prop_color, await self.set_property_async(
value=rgb, prop=self._prop_color, value=rgb, write_ha_state=False
write_ha_state=False) )
self._attr_color_mode = ColorMode.RGB self._attr_color_mode = ColorMode.RGB
# mode # mode
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
await self.set_property_async( await self.set_property_async(
prop=self._prop_mode, prop=self._prop_mode,
value=self.get_map_key(map_=self._mode_map, value=self.get_map_key(
value=kwargs[ATTR_EFFECT]), map_=self._mode_map, value=kwargs[ATTR_EFFECT]
),
write_ha_state=False, write_ha_state=False,
) )
self.async_write_ha_state() self.async_write_ha_state()

File diff suppressed because it is too large Load Diff

View File

@ -103,8 +103,7 @@ class MIoTOauthClient:
else: else:
self._oauth_host = f"{cloud_server}.{DEFAULT_OAUTH2_API_HOST}" self._oauth_host = f"{cloud_server}.{DEFAULT_OAUTH2_API_HOST}"
self._device_id = f"ha.{uuid}" self._device_id = f"ha.{uuid}"
self._state = hashlib.sha1( self._state = hashlib.sha1(f"d={self._device_id}".encode("utf-8")).hexdigest()
f"d={self._device_id}".encode("utf-8")).hexdigest()
self._session = aiohttp.ClientSession(loop=self._main_loop) self._session = aiohttp.ClientSession(loop=self._main_loop)
@property @property
@ -167,24 +166,31 @@ class MIoTOauthClient:
timeout=MIHOME_HTTP_API_TIMEOUT, timeout=MIHOME_HTTP_API_TIMEOUT,
) )
if http_res.status == 401: if http_res.status == 401:
raise MIoTOauthError("unauthorized(401)", raise MIoTOauthError(
MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED) "unauthorized(401)", MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED
)
if http_res.status != 200: if http_res.status != 200:
raise MIoTOauthError(f"invalid http status code, {http_res.status}") raise MIoTOauthError(f"invalid http status code, {http_res.status}")
res_str = await http_res.text() res_str = await http_res.text()
res_obj = json.loads(res_str) res_obj = json.loads(res_str)
if (not res_obj or res_obj.get("code", None) != 0 or if (
"result" not in res_obj or not res_obj
not all(key in res_obj["result"] for key in or res_obj.get("code", None) != 0
["access_token", "refresh_token", "expires_in"])): or "result" not in res_obj
or not all(
key in res_obj["result"]
for key in ["access_token", "refresh_token", "expires_in"]
)
):
raise MIoTOauthError(f"invalid http response, {res_str}") raise MIoTOauthError(f"invalid http response, {res_str}")
return { return {
**res_obj["result"], **res_obj["result"],
"expires_ts": "expires_ts": int(
int(time.time() + (res_obj["result"].get("expires_in", 0) * time.time()
TOKEN_EXPIRES_TS_RATIO)), + (res_obj["result"].get("expires_in", 0) * TOKEN_EXPIRES_TS_RATIO)
),
} }
async def get_access_token_async(self, code: str) -> dict: async def get_access_token_async(self, code: str) -> dict:
@ -205,7 +211,8 @@ class MIoTOauthClient:
"redirect_uri": self._redirect_url, "redirect_uri": self._redirect_url,
"code": code, "code": code,
"device_id": self._device_id, "device_id": self._device_id,
}) }
)
async def refresh_access_token_async(self, refresh_token: str) -> dict: async def refresh_access_token_async(self, refresh_token: str) -> dict:
"""get access token by refresh token. """get access token by refresh token.
@ -224,7 +231,8 @@ class MIoTOauthClient:
"client_id": self._client_id, "client_id": self._client_id,
"redirect_uri": self._redirect_url, "redirect_uri": self._redirect_url,
"refresh_token": refresh_token, "refresh_token": refresh_token,
}) }
)
class MIoTHttpClient: class MIoTHttpClient:
@ -259,14 +267,16 @@ class MIoTHttpClient:
self._get_prop_timer = None self._get_prop_timer = None
self._get_prop_list = {} self._get_prop_list = {}
if (not isinstance(cloud_server, str) or if (
not isinstance(client_id, str) or not isinstance(cloud_server, str)
not isinstance(access_token, str)): or not isinstance(client_id, str)
or not isinstance(access_token, str)
):
raise MIoTHttpError("invalid params") raise MIoTHttpError("invalid params")
self.update_http_header(cloud_server=cloud_server, self.update_http_header(
client_id=client_id, cloud_server=cloud_server, client_id=client_id, access_token=access_token
access_token=access_token) )
self._session = aiohttp.ClientSession(loop=self._main_loop) self._session = aiohttp.ClientSession(loop=self._main_loop)
@ -309,10 +319,8 @@ class MIoTHttpClient:
# pylint: disable=unused-private-member # pylint: disable=unused-private-member
async def __mihome_api_get_async( async def __mihome_api_get_async(
self, self, url_path: str, params: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT
url_path: str, ) -> dict:
params: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT) -> dict:
http_res = await self._session.get( http_res = await self._session.get(
url=f"{self._base_url}{url_path}", url=f"{self._base_url}{url_path}",
params=params, params=params,
@ -333,16 +341,16 @@ class MIoTHttpClient:
if res_obj.get("code", None) != 0: if res_obj.get("code", None) != 0:
raise MIoTHttpError( raise MIoTHttpError(
f"invalid response code, {res_obj.get('code', None)}, " f"invalid response code, {res_obj.get('code', None)}, "
f"{res_obj.get('message', '')}") f"{res_obj.get('message', '')}"
_LOGGER.debug("mihome api get, %s%s, %s -> %s", self._base_url, )
url_path, params, res_obj) _LOGGER.debug(
"mihome api get, %s%s, %s -> %s", self._base_url, url_path, params, res_obj
)
return res_obj return res_obj
async def __mihome_api_post_async( async def __mihome_api_post_async(
self, self, url_path: str, data: dict, timeout: int = MIHOME_HTTP_API_TIMEOUT
url_path: str, ) -> dict:
data: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT) -> dict:
http_res = await self._session.post( http_res = await self._session.post(
url=f"{self._base_url}{url_path}", url=f"{self._base_url}{url_path}",
json=data, json=data,
@ -363,26 +371,29 @@ class MIoTHttpClient:
if res_obj.get("code", None) != 0: if res_obj.get("code", None) != 0:
raise MIoTHttpError( raise MIoTHttpError(
f"invalid response code, {res_obj.get('code', None)}, " f"invalid response code, {res_obj.get('code', None)}, "
f"{res_obj.get('message', '')}") f"{res_obj.get('message', '')}"
_LOGGER.debug("mihome api post, %s%s, %s -> %s", self._base_url, )
url_path, data, res_obj) _LOGGER.debug(
"mihome api post, %s%s, %s -> %s", self._base_url, url_path, data, res_obj
)
return res_obj return res_obj
async def get_user_info_async(self) -> dict: async def get_user_info_async(self) -> dict:
http_res = await self._session.get( http_res = await self._session.get(
url="https://open.account.xiaomi.com/user/profile", url="https://open.account.xiaomi.com/user/profile",
params={ params={"clientId": self._client_id, "token": self._access_token},
"clientId": self._client_id,
"token": self._access_token
},
headers={"content-type": "application/x-www-form-urlencoded"}, headers={"content-type": "application/x-www-form-urlencoded"},
timeout=MIHOME_HTTP_API_TIMEOUT, timeout=MIHOME_HTTP_API_TIMEOUT,
) )
res_str = await http_res.text() res_str = await http_res.text()
res_obj = json.loads(res_str) res_obj = json.loads(res_str)
if (not res_obj or res_obj.get("code", None) != 0 or if (
"data" not in res_obj or "miliaoNick" not in res_obj["data"]): not res_obj
or res_obj.get("code", None) != 0
or "data" not in res_obj
or "miliaoNick" not in res_obj["data"]
):
raise MIoTOauthError(f"invalid http response, {http_res.text}") raise MIoTOauthError(f"invalid http response, {http_res.text}")
return res_obj["data"] return res_obj["data"]
@ -403,8 +414,7 @@ class MIoTHttpClient:
return cert return cert
async def __get_dev_room_page_async(self, async def __get_dev_room_page_async(self, max_id: Optional[str] = None) -> dict:
max_id: Optional[str] = None) -> dict:
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path="/app/v2/homeroom/get_dev_room_page", url_path="/app/v2/homeroom/get_dev_room_page",
data={ data={
@ -425,34 +435,39 @@ class MIoTHttpClient:
} }
for room in home.get("roomlist", []): for room in home.get("roomlist", []):
if "id" not in room: if "id" not in room:
_LOGGER.error("get dev room page error, invalid room, %s", _LOGGER.error("get dev room page error, invalid room, %s", room)
room)
continue continue
home_list[str(home["id"])]["room_info"][str(room["id"])] = { home_list[str(home["id"])]["room_info"][str(room["id"])] = {
"dids": room.get("dids", None) or [] "dids": room.get("dids", None) or []
} }
if res_obj["result"].get("has_more", False) and isinstance( if res_obj["result"].get("has_more", False) and isinstance(
res_obj["result"].get("max_id", None), str): res_obj["result"].get("max_id", None), str
):
next_list = await self.__get_dev_room_page_async( next_list = await self.__get_dev_room_page_async(
max_id=res_obj["result"]["max_id"]) max_id=res_obj["result"]["max_id"]
)
for home_id, info in next_list.items(): for home_id, info in next_list.items():
home_list.setdefault(home_id, {"dids": [], "room_info": {}}) home_list.setdefault(home_id, {"dids": [], "room_info": {}})
home_list[home_id]["dids"].extend(info["dids"]) home_list[home_id]["dids"].extend(info["dids"])
for room_id, info in info["room_info"].items(): for room_id, info in info["room_info"].items():
home_list[home_id]["room_info"].setdefault( home_list[home_id]["room_info"].setdefault(room_id, {"dids": []})
room_id, {"dids": []})
home_list[home_id]["room_info"][room_id]["dids"].extend( home_list[home_id]["room_info"][room_id]["dids"].extend(
info["dids"]) info["dids"]
)
return home_list return home_list
async def get_separated_shared_devices_async(self) -> dict[str, dict]: async def get_separated_shared_devices_async(self) -> dict[str, dict]:
separated_shared_devices: dict = {} separated_shared_devices: dict = {}
device_list: dict[str, dict] = await self.__get_device_list_page_async( device_list: dict[str, dict] = await self.__get_device_list_page_async(
dids=[], start_did=None) dids=[], start_did=None
)
for did, value in device_list.items(): for did, value in device_list.items():
if (value["owner"] is not None and ("userid" in value["owner"]) and if (
("nickname" in value["owner"])): value["owner"] is not None
and ("userid" in value["owner"])
and ("nickname" in value["owner"])
):
separated_shared_devices.setdefault(did, value["owner"]) separated_shared_devices.setdefault(did, value["owner"])
return separated_shared_devices return separated_shared_devices
@ -480,53 +495,45 @@ class MIoTHttpClient:
if uid is None and device_source == "homelist": if uid is None and device_source == "homelist":
uid = str(home["uid"]) uid = str(home["uid"])
home_infos[device_source][home["id"]] = { home_infos[device_source][home["id"]] = {
"home_id": "home_id": home["id"],
home["id"], "home_name": home["name"],
"home_name": "city_id": home.get("city_id", None),
home["name"], "longitude": home.get("longitude", None),
"city_id": "latitude": home.get("latitude", None),
home.get("city_id", None), "address": home.get("address", None),
"longitude": "dids": home.get("dids", []),
home.get("longitude", None),
"latitude":
home.get("latitude", None),
"address":
home.get("address", None),
"dids":
home.get("dids", []),
"room_info": { "room_info": {
room["id"]: { room["id"]: {
"room_id": room["id"], "room_id": room["id"],
"room_name": room["name"], "room_name": room["name"],
"dids": room.get("dids", []), "dids": room.get("dids", []),
} for room in home.get("roomlist", []) if "id" in room }
for room in home.get("roomlist", [])
if "id" in room
}, },
"group_id": "group_id": calc_group_id(uid=home["uid"], home_id=home["id"]),
calc_group_id(uid=home["uid"], home_id=home["id"]), "uid": str(home["uid"]),
"uid":
str(home["uid"]),
} }
home_infos["uid"] = uid home_infos["uid"] = uid
if res_obj["result"].get("has_more", False) and isinstance( if res_obj["result"].get("has_more", False) and isinstance(
res_obj["result"].get("max_id", None), str): res_obj["result"].get("max_id", None), str
):
more_list = await self.__get_dev_room_page_async( more_list = await self.__get_dev_room_page_async(
max_id=res_obj["result"]["max_id"]) max_id=res_obj["result"]["max_id"]
)
for device_source in ["homelist", "share_home_list"]: for device_source in ["homelist", "share_home_list"]:
for home_id, info in more_list.items(): for home_id, info in more_list.items():
if home_id not in home_infos[device_source]: if home_id not in home_infos[device_source]:
_LOGGER.info("unknown home, %s, %s", home_id, info) _LOGGER.info("unknown home, %s, %s", home_id, info)
continue continue
home_infos[device_source][home_id]["dids"].extend( home_infos[device_source][home_id]["dids"].extend(info["dids"])
info["dids"])
for room_id, info in info["room_info"].items(): for room_id, info in info["room_info"].items():
home_infos[device_source][home_id][ home_infos[device_source][home_id]["room_info"].setdefault(
"room_info"].setdefault(room_id, { room_id, {"room_id": room_id, "room_name": "", "dids": []}
"room_id": room_id, )
"room_name": "", home_infos[device_source][home_id]["room_info"][room_id][
"dids": [] "dids"
}) ].extend(info["dids"])
home_infos[device_source][home_id]["room_info"][
room_id]["dids"].extend(info["dids"])
return { return {
"uid": uid, "uid": uid,
@ -538,15 +545,15 @@ class MIoTHttpClient:
return (await self.get_homeinfos_async()).get("uid", None) return (await self.get_homeinfos_async()).get("uid", None)
async def __get_device_list_page_async( async def __get_device_list_page_async(
self, self, dids: list[str], start_did: Optional[str] = None
dids: list[str], ) -> dict[str, dict]:
start_did: Optional[str] = None) -> dict[str, dict]:
req_data: dict = {"limit": 200, "get_split_device": True, "dids": dids} req_data: dict = {"limit": 200, "get_split_device": True, "dids": dids}
if start_did: if start_did:
req_data["start_did"] = start_did req_data["start_did"] = start_did
device_infos: dict = {} device_infos: dict = {}
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path="/app/v2/home/device_list_page", data=req_data) url_path="/app/v2/home/device_list_page", data=req_data
)
if "result" not in res_obj: if "result" not in res_obj:
raise MIoTHttpError("invalid response result") raise MIoTHttpError("invalid response result")
res_obj = res_obj["result"] res_obj = res_obj["result"]
@ -568,69 +575,56 @@ class MIoTHttpClient:
_LOGGER.info("ignore miwifi.* device, cloud, %s", did) _LOGGER.info("ignore miwifi.* device, cloud, %s", did)
continue continue
device_infos[did] = { device_infos[did] = {
"did": "did": did,
did, "uid": device.get("uid", None),
"uid": "name": name,
device.get("uid", None), "urn": urn,
"name": "model": model,
name, "connect_type": device.get("pid", -1),
"urn": "token": device.get("token", None),
urn, "online": device.get("isOnline", False),
"model": "icon": device.get("icon", None),
model, "parent_id": device.get("parent_id", None),
"connect_type": "manufacturer": model.split(".")[0],
device.get("pid", -1),
"token":
device.get("token", None),
"online":
device.get("isOnline", False),
"icon":
device.get("icon", None),
"parent_id":
device.get("parent_id", None),
"manufacturer":
model.split(".")[0],
# 2: xiao-ai, 1: general speaker # 2: xiao-ai, 1: general speaker
"voice_ctrl": "voice_ctrl": device.get("voice_ctrl", 0),
device.get("voice_ctrl", 0), "rssi": device.get("rssi", None),
"rssi": "owner": device.get("owner", None),
device.get("rssi", None), "pid": device.get("pid", None),
"owner": "local_ip": device.get("local_ip", None),
device.get("owner", None), "ssid": device.get("ssid", None),
"pid": "bssid": device.get("bssid", None),
device.get("pid", None), "order_time": device.get("orderTime", 0),
"local_ip": "fw_version": device.get("extra", {}).get("fw_version", "unknown"),
device.get("local_ip", None),
"ssid":
device.get("ssid", None),
"bssid":
device.get("bssid", None),
"order_time":
device.get("orderTime", 0),
"fw_version":
device.get("extra", {}).get("fw_version", "unknown"),
} }
if isinstance(device.get("extra", None), dict) and device["extra"]: if isinstance(device.get("extra", None), dict) and device["extra"]:
device_infos[did]["fw_version"] = device["extra"].get( device_infos[did]["fw_version"] = device["extra"].get(
"fw_version", None) "fw_version", None
)
device_infos[did]["mcu_version"] = device["extra"].get( device_infos[did]["mcu_version"] = device["extra"].get(
"mcu_version", None) "mcu_version", None
device_infos[did]["platform"] = device["extra"].get( )
"platform", None) device_infos[did]["platform"] = device["extra"].get("platform", None)
next_start_did = res_obj.get("next_start_did", None) next_start_did = res_obj.get("next_start_did", None)
if res_obj.get("has_more", False) and next_start_did: if res_obj.get("has_more", False) and next_start_did:
device_infos.update(await self.__get_device_list_page_async( device_infos.update(
dids=dids, start_did=next_start_did)) await self.__get_device_list_page_async(
dids=dids, start_did=next_start_did
)
)
return device_infos return device_infos
async def get_devices_with_dids_async( async def get_devices_with_dids_async(
self, dids: list[str]) -> Optional[dict[str, dict]]: self, dids: list[str]
results: list[dict[str, dict]] = await asyncio.gather(*[ ) -> Optional[dict[str, dict]]:
self.__get_device_list_page_async(dids=dids[index:index + 150]) results: list[dict[str, dict]] = await asyncio.gather(
for index in range(0, len(dids), 150) *[
]) self.__get_device_list_page_async(dids=dids[index : index + 150])
for index in range(0, len(dids), 150)
]
)
devices = {} devices = {}
for result in results: for result in results:
if result is None: if result is None:
@ -638,16 +632,15 @@ class MIoTHttpClient:
devices.update(result) devices.update(result)
return devices return devices
async def get_devices_async(self, async def get_devices_async(
home_ids: Optional[list[str]] = None self, home_ids: Optional[list[str]] = None
) -> dict[str, dict]: ) -> dict[str, dict]:
homeinfos = await self.get_homeinfos_async() homeinfos = await self.get_homeinfos_async()
homes: dict[str, dict[str, Any]] = {} homes: dict[str, dict[str, Any]] = {}
devices: dict[str, dict] = {} devices: dict[str, dict] = {}
for device_type in ["home_list", "share_home_list"]: for device_type in ["home_list", "share_home_list"]:
homes.setdefault(device_type, {}) homes.setdefault(device_type, {})
for home_id, home_info in (homeinfos.get(device_type, None) or for home_id, home_info in (homeinfos.get(device_type, None) or {}).items():
{}).items():
if isinstance(home_ids, list) and home_id not in home_ids: if isinstance(home_ids, list) and home_id not in home_ids:
continue continue
home_name: str = home_info["home_name"] home_name: str = home_info["home_name"]
@ -661,30 +654,34 @@ class MIoTHttpClient:
"room_info": {}, "room_info": {},
}, },
) )
devices.update({ devices.update(
did: { {
"home_id": home_id,
"home_name": home_name,
"room_id": home_id,
"room_name": home_name,
"group_id": group_id,
} for did in home_info.get("dids", [])
})
for room_id, room_info in home_info.get("room_info").items():
room_name: str = room_info.get("room_name", "")
homes[device_type][home_id]["room_info"][
room_id] = room_name
devices.update({
did: { did: {
"home_id": home_id, "home_id": home_id,
"home_name": home_name, "home_name": home_name,
"room_id": room_id, "room_id": home_id,
"room_name": room_name, "room_name": home_name,
"group_id": group_id, "group_id": group_id,
} for did in room_info.get("dids", []) }
}) for did in home_info.get("dids", [])
separated_shared_devices: dict = await self.get_separated_shared_devices_async( }
) )
for room_id, room_info in home_info.get("room_info").items():
room_name: str = room_info.get("room_name", "")
homes[device_type][home_id]["room_info"][room_id] = room_name
devices.update(
{
did: {
"home_id": home_id,
"home_name": home_name,
"room_id": room_id,
"room_name": room_name,
"group_id": group_id,
}
for did in room_info.get("dids", [])
}
)
separated_shared_devices: dict = await self.get_separated_shared_devices_async()
if separated_shared_devices: if separated_shared_devices:
homes.setdefault("separated_shared_list", {}) homes.setdefault("separated_shared_list", {})
for did, owner in separated_shared_devices.items(): for did, owner in separated_shared_devices.items():
@ -695,20 +692,20 @@ class MIoTHttpClient:
"home_name": owner["nickname"], "home_name": owner["nickname"],
"uid": owner_id, "uid": owner_id,
"group_id": "NotSupport", "group_id": "NotSupport",
"room_info": { "room_info": {"shared_device": "shared_device"},
"shared_device": "shared_device"
},
}, },
) )
devices.update({ devices.update(
did: { {
"home_id": owner_id, did: {
"home_name": owner["nickname"], "home_id": owner_id,
"room_id": "shared_device", "home_name": owner["nickname"],
"room_name": "shared_device", "room_id": "shared_device",
"group_id": "NotSupport", "room_name": "shared_device",
"group_id": "NotSupport",
}
} }
}) )
dids = sorted(list(devices.keys())) dids = sorted(list(devices.keys()))
results = await self.get_devices_with_dids_async(dids=dids) results = await self.get_devices_with_dids_async(dids=dids)
if results is None: if results is None:
@ -727,8 +724,7 @@ class MIoTHttpClient:
parent_did = did.replace(match_str.group(), "") parent_did = did.replace(match_str.group(), "")
if parent_did in devices: if parent_did in devices:
devices[parent_did].setdefault("sub_devices", {}) devices[parent_did].setdefault("sub_devices", {})
devices[parent_did]["sub_devices"][match_str.group() devices[parent_did]["sub_devices"][match_str.group()[1:]] = device
[1:]] = device
else: else:
_LOGGER.error("unknown sub devices, %s, %s", did, parent_did) _LOGGER.error("unknown sub devices, %s, %s", did, parent_did)
return {"uid": homeinfos["uid"], "homes": homes, "devices": devices} return {"uid": homeinfos["uid"], "homes": homes, "devices": devices}
@ -740,21 +736,16 @@ class MIoTHttpClient:
""" """
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path="/app/v2/miotspec/prop/get", url_path="/app/v2/miotspec/prop/get",
data={ data={"datasource": 1, "params": params},
"datasource": 1,
"params": params
},
) )
if "result" not in res_obj: if "result" not in res_obj:
raise MIoTHttpError("invalid response result") raise MIoTHttpError("invalid response result")
return res_obj["result"] return res_obj["result"]
async def __get_prop_async(self, did: str, siid: int, piid: int) -> Any: async def __get_prop_async(self, did: str, siid: int, piid: int) -> Any:
results = await self.get_props_async(params=[{ results = await self.get_props_async(
"did": did, params=[{"did": did, "siid": siid, "piid": piid}]
"siid": siid, )
"piid": piid
}])
if not results: if not results:
return None return None
result = results[0] result = results[0]
@ -782,8 +773,7 @@ class MIoTHttpClient:
results = await self.get_props_async(props_buffer) results = await self.get_props_async(props_buffer)
for result in results: for result in results:
if not all( if not all(key in result for key in ["did", "siid", "piid", "value"]):
key in result for key in ["did", "siid", "piid", "value"]):
continue continue
key = f"{result['did']}.{result['siid']}.{result['piid']}" key = f"{result['did']}.{result['siid']}.{result['piid']}"
prop_obj = self._get_prop_list.pop(key, None) prop_obj = self._get_prop_list.pop(key, None)
@ -810,11 +800,9 @@ class MIoTHttpClient:
self._get_prop_timer = None self._get_prop_timer = None
return True return True
async def get_prop_async(self, async def get_prop_async(
did: str, self, did: str, siid: int, piid: int, immediately: bool = False
siid: int, ) -> Any:
piid: int,
immediately: bool = False) -> Any:
if immediately: if immediately:
return await self.__get_prop_async(did, siid, piid) return await self.__get_prop_async(did, siid, piid)
key: str = f"{did}.{siid}.{piid}" key: str = f"{did}.{siid}.{piid}"
@ -823,11 +811,7 @@ class MIoTHttpClient:
return await prop_obj["fut"] return await prop_obj["fut"]
fut = self._main_loop.create_future() fut = self._main_loop.create_future()
self._get_prop_list[key] = { self._get_prop_list[key] = {
"param": { "param": {"did": did, "siid": siid, "piid": piid},
"did": did,
"siid": siid,
"piid": piid
},
"fut": fut, "fut": fut,
} }
if self._get_prop_timer is None: if self._get_prop_timer is None:
@ -843,9 +827,8 @@ class MIoTHttpClient:
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
""" """
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path="/app/v2/miotspec/prop/set", url_path="/app/v2/miotspec/prop/set", data={"params": params}, timeout=15
data={"params": params}, )
timeout=15)
if "result" not in res_obj: if "result" not in res_obj:
raise MIoTHttpError("invalid response result") raise MIoTHttpError("invalid response result")
@ -856,16 +839,16 @@ class MIoTHttpClient:
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}] params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
""" """
res_obj = await self.__mihome_api_post_async( res_obj = await self.__mihome_api_post_async(
url_path="/app/v2/miotspec/prop/set", url_path="/app/v2/miotspec/prop/set", data={"params": params}, timeout=15
data={"params": params}, )
timeout=15)
if "result" not in res_obj: if "result" not in res_obj:
raise MIoTHttpError("invalid response result") raise MIoTHttpError("invalid response result")
return res_obj["result"] return res_obj["result"]
async def action_async(self, did: str, siid: int, aiid: int, async def action_async(
in_list: list[dict]) -> dict: self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict:
""" """
params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []} params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []}
""" """

View File

@ -116,8 +116,7 @@ class MIoTEntityData:
events: set[MIoTSpecEvent] events: set[MIoTSpecEvent]
actions: set[MIoTSpecAction] actions: set[MIoTSpecAction]
def __init__(self, platform: str, def __init__(self, platform: str, spec: MIoTSpecInstance | MIoTSpecService) -> None:
spec: MIoTSpecInstance | MIoTSpecService) -> None:
self.platform = platform self.platform = platform
self.spec = spec self.spec = spec
self.device_class = None self.device_class = None
@ -151,8 +150,7 @@ class MIoTDevice:
_suggested_area: Optional[str] _suggested_area: Optional[str]
_sub_id: int _sub_id: int
_device_state_sub_list: dict[str, dict[str, Callable[[str, MIoTDeviceState], _device_state_sub_list: dict[str, dict[str, Callable[[str, MIoTDeviceState], None]]]
None]]]
_value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]] _value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]]
_entity_list: dict[str, list[MIoTEntityData]] _entity_list: dict[str, list[MIoTEntityData]]
@ -184,8 +182,7 @@ class MIoTDevice:
self._room_name = device_info.get("room_name", None) self._room_name = device_info.get("room_name", None)
match self.miot_client.area_name_rule: match self.miot_client.area_name_rule:
case "home_room": case "home_room":
self._suggested_area = f"{self._home_name} {self._room_name}".strip( self._suggested_area = f"{self._home_name} {self._room_name}".strip()
)
case "home": case "home":
self._suggested_area = self._home_name.strip() self._suggested_area = self._home_name.strip()
case "room": case "room":
@ -208,14 +205,15 @@ class MIoTDevice:
sub_info = sub_devices.get(f"s{service.iid}", None) sub_info = sub_devices.get(f"s{service.iid}", None)
if sub_info is None: if sub_info is None:
continue continue
_LOGGER.debug("miot device, update service sub info, %s, %s", _LOGGER.debug(
self.did, sub_info) "miot device, update service sub info, %s, %s", self.did, sub_info
)
service.description_trans = sub_info.get( service.description_trans = sub_info.get(
"name", service.description_trans) "name", service.description_trans
)
# Sub device state # Sub device state
self.miot_client.sub_device_state(self._did, self.miot_client.sub_device_state(self._did, self.__on_device_state_changed)
self.__on_device_state_changed)
_LOGGER.debug("miot device init %s", device_info) _LOGGER.debug("miot device init %s", device_info)
@ -240,14 +238,13 @@ class MIoTDevice:
return self._action_list return self._action_list
async def action_async(self, siid: int, aiid: int, in_list: list) -> list: async def action_async(self, siid: int, aiid: int, in_list: list) -> list:
return await self.miot_client.action_async(did=self._did, return await self.miot_client.action_async(
siid=siid, did=self._did, siid=siid, aiid=aiid, in_list=in_list
aiid=aiid, )
in_list=in_list)
def sub_device_state( def sub_device_state(
self, key: str, handler: Callable[[str, MIoTDeviceState], self, key: str, handler: Callable[[str, MIoTDeviceState], None]
None]) -> int: ) -> int:
sub_id = self.__gen_sub_id() sub_id = self.__gen_sub_id()
if key in self._device_state_sub_list: if key in self._device_state_sub_list:
self._device_state_sub_list[key][str(sub_id)] = handler self._device_state_sub_list[key][str(sub_id)] = handler
@ -262,8 +259,9 @@ class MIoTDevice:
if not sub_list: if not sub_list:
self._device_state_sub_list.pop(key, None) self._device_state_sub_list.pop(key, None)
def sub_property(self, handler: Callable[[dict, Any], None], siid: int, def sub_property(
piid: int) -> int: self, handler: Callable[[dict, Any], None], siid: int, piid: int
) -> int:
key: str = f"p.{siid}.{piid}" key: str = f"p.{siid}.{piid}"
def _on_prop_changed(params: dict, ctx: Any) -> None: def _on_prop_changed(params: dict, ctx: Any) -> None:
@ -275,10 +273,9 @@ class MIoTDevice:
self._value_sub_list[key][str(sub_id)] = handler self._value_sub_list[key][str(sub_id)] = handler
else: else:
self._value_sub_list[key] = {str(sub_id): handler} self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_prop(did=self._did, self.miot_client.sub_prop(
handler=_on_prop_changed, did=self._did, handler=_on_prop_changed, siid=siid, piid=piid
siid=siid, )
piid=piid)
return sub_id return sub_id
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None: def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
@ -291,8 +288,9 @@ class MIoTDevice:
self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid) self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
self._value_sub_list.pop(key, None) self._value_sub_list.pop(key, None)
def sub_event(self, handler: Callable[[dict, Any], None], siid: int, def sub_event(
eiid: int) -> int: self, handler: Callable[[dict, Any], None], siid: int, eiid: int
) -> int:
key: str = f"e.{siid}.{eiid}" key: str = f"e.{siid}.{eiid}"
def _on_event_occurred(params: dict, ctx: Any) -> None: def _on_event_occurred(params: dict, ctx: Any) -> None:
@ -304,10 +302,9 @@ class MIoTDevice:
self._value_sub_list[key][str(sub_id)] = handler self._value_sub_list[key][str(sub_id)] = handler
else: else:
self._value_sub_list[key] = {str(sub_id): handler} self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_event(did=self._did, self.miot_client.sub_event(
handler=_on_event_occurred, did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid
siid=siid, )
eiid=eiid)
return sub_id return sub_id
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None: def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
@ -332,7 +329,8 @@ class MIoTDevice:
suggested_area=self._suggested_area, suggested_area=self._suggested_area,
configuration_url=( configuration_url=(
f"https://home.mi.com/webapp/content/baike/product/index.html?" f"https://home.mi.com/webapp/content/baike/product/index.html?"
f"model={self._model}"), f"model={self._model}"
),
) )
@property @property
@ -342,35 +340,46 @@ class MIoTDevice:
@property @property
def did_tag(self) -> str: def did_tag(self) -> str:
return slugify_did(cloud_server=self.miot_client.cloud_server, return slugify_did(cloud_server=self.miot_client.cloud_server, did=self._did)
did=self._did)
def gen_device_entity_id(self, ha_domain: str) -> str: def gen_device_entity_id(self, ha_domain: str) -> str:
return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" return (
f"{self._model_strs[-1][:20]}") f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_"
f"{self._model_strs[-1][:20]}"
)
def gen_service_entity_id(self, ha_domain: str, siid: int, def gen_service_entity_id(self, ha_domain: str, siid: int, description: str) -> str:
description: str) -> str: return (
return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_"
f"{self._model_strs[-1][:20]}_s_{siid}_{description}") f"{self._model_strs[-1][:20]}_s_{siid}_{description}"
)
def gen_prop_entity_id(self, ha_domain: str, spec_name: str, siid: int, def gen_prop_entity_id(
piid: int) -> str: self, ha_domain: str, spec_name: str, siid: int, piid: int
return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" ) -> str:
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" return (
f"_p_{siid}_{piid}") f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_"
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}"
f"_p_{siid}_{piid}"
)
def gen_event_entity_id(self, ha_domain: str, spec_name: str, siid: int, def gen_event_entity_id(
eiid: int) -> str: self, ha_domain: str, spec_name: str, siid: int, eiid: int
return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" ) -> str:
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" return (
f"_e_{siid}_{eiid}") f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_"
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}"
f"_e_{siid}_{eiid}"
)
def gen_action_entity_id(self, ha_domain: str, spec_name: str, siid: int, def gen_action_entity_id(
aiid: int) -> str: self, ha_domain: str, spec_name: str, siid: int, aiid: int
return (f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_" ) -> str:
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}" return (
f"_a_{siid}_{aiid}") f"{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_"
f"{self._model_strs[-1][:20]}_{slugify_name(spec_name)}"
f"_a_{siid}_{aiid}"
)
@property @property
def name(self) -> str: def name(self) -> str:
@ -407,7 +416,8 @@ class MIoTDevice:
self._action_list[action.platform].append(action) self._action_list[action.platform].append(action)
def parse_miot_device_entity( def parse_miot_device_entity(
self, spec_instance: MIoTSpecInstance) -> Optional[MIoTEntityData]: self, spec_instance: MIoTSpecInstance
) -> Optional[MIoTEntityData]:
if spec_instance.name not in SPEC_DEVICE_TRANS_MAP: if spec_instance.name not in SPEC_DEVICE_TRANS_MAP:
return None return None
spec_name: str = spec_instance.name spec_name: str = spec_instance.name
@ -417,8 +427,9 @@ class MIoTDevice:
return None return None
# 1. The device shall have all required services. # 1. The device shall have all required services.
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]["required"].keys() required_services = SPEC_DEVICE_TRANS_MAP[spec_name]["required"].keys()
if not {service.name for service in spec_instance.services if not {service.name for service in spec_instance.services}.issuperset(
}.issuperset(required_services): required_services
):
return None return None
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].keys() optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].keys()
@ -434,56 +445,74 @@ class MIoTDevice:
# 2. The service shall have all required properties, actions. # 2. The service shall have all required properties, actions.
if service.name in required_services: if service.name in required_services:
required_properties = ( required_properties = (
SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["required"]
service.name, {}).get("required", .get(service.name, {})
{}).get("properties", {})) .get("required", {})
.get("properties", {})
)
optional_properties = ( optional_properties = (
SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["required"]
service.name, {}).get("optional", .get(service.name, {})
{}).get("properties", set({}))) .get("optional", {})
.get("properties", set({}))
)
required_actions = ( required_actions = (
SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["required"]
service.name, {}).get("required", .get(service.name, {})
{}).get("actions", set({}))) .get("required", {})
.get("actions", set({}))
)
optional_actions = ( optional_actions = (
SPEC_DEVICE_TRANS_MAP[spec_name]["required"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["required"]
service.name, {}).get("optional", .get(service.name, {})
{}).get("actions", set({}))) .get("optional", {})
.get("actions", set({}))
)
elif service.name in optional_services: elif service.name in optional_services:
required_properties = ( required_properties = (
SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["optional"]
service.name, {}).get("required", .get(service.name, {})
{}).get("properties", {})) .get("required", {})
.get("properties", {})
)
optional_properties = ( optional_properties = (
SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["optional"]
service.name, {}).get("optional", .get(service.name, {})
{}).get("properties", set({}))) .get("optional", {})
.get("properties", set({}))
)
required_actions = ( required_actions = (
SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["optional"]
service.name, {}).get("required", .get(service.name, {})
{}).get("actions", set({}))) .get("required", {})
.get("actions", set({}))
)
optional_actions = ( optional_actions = (
SPEC_DEVICE_TRANS_MAP[spec_name]["optional"].get( SPEC_DEVICE_TRANS_MAP[spec_name]["optional"]
service.name, {}).get("optional", .get(service.name, {})
{}).get("actions", set({}))) .get("optional", {})
.get("actions", set({}))
)
else: else:
continue continue
if not {prop.name for prop in service.properties if prop.access if not {prop.name for prop in service.properties if prop.access}.issuperset(
}.issuperset(set(required_properties.keys())): set(required_properties.keys())
):
return None return None
if not {action.name for action in service.actions if not {action.name for action in service.actions}.issuperset(
}.issuperset(required_actions): required_actions
):
return None return None
# 3. The required property shall have all required access mode. # 3. The required property shall have all required access mode.
for prop in service.properties: for prop in service.properties:
if prop.name in required_properties: if prop.name in required_properties:
if not set(prop.access).issuperset( if not set(prop.access).issuperset(required_properties[prop.name]):
required_properties[prop.name]):
return None return None
# property # property
for prop in service.properties: for prop in service.properties:
if prop.name in set.union(set(required_properties.keys()), if prop.name in set.union(
optional_properties): set(required_properties.keys()), optional_properties
):
if prop.unit: if prop.unit:
prop.external_unit = self.unit_convert(prop.unit) prop.external_unit = self.unit_convert(prop.unit)
# prop.icon = self.icon_convert(prop.unit) # prop.icon = self.icon_convert(prop.unit)
@ -500,7 +529,8 @@ class MIoTDevice:
return entity_data return entity_data
def parse_miot_service_entity( def parse_miot_service_entity(
self, miot_service: MIoTSpecService) -> Optional[MIoTEntityData]: self, miot_service: MIoTSpecService
) -> Optional[MIoTEntityData]:
if miot_service.platform or miot_service.name not in SPEC_SERVICE_TRANS_MAP: if miot_service.platform or miot_service.name not in SPEC_SERVICE_TRANS_MAP:
return None return None
service_name = miot_service.name service_name = miot_service.name
@ -510,25 +540,28 @@ class MIoTDevice:
return None return None
# Required properties, required access mode # Required properties, required access mode
required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][ required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
"required"].get("properties", {}) "required"
if not {prop.name for prop in miot_service.properties if prop.access ].get("properties", {})
}.issuperset(set(required_properties.keys())): if not {
prop.name for prop in miot_service.properties if prop.access
}.issuperset(set(required_properties.keys())):
return None return None
for prop in miot_service.properties: for prop in miot_service.properties:
if prop.name in required_properties: if prop.name in required_properties:
if not set(prop.access).issuperset( if not set(prop.access).issuperset(required_properties[prop.name]):
required_properties[prop.name]):
return None return None
# Required actions # Required actions
# Required events # Required events
platform = SPEC_SERVICE_TRANS_MAP[service_name]["entity"] platform = SPEC_SERVICE_TRANS_MAP[service_name]["entity"]
entity_data = MIoTEntityData(platform=platform, spec=miot_service) entity_data = MIoTEntityData(platform=platform, spec=miot_service)
# Optional properties # Optional properties
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][ optional_properties = SPEC_SERVICE_TRANS_MAP[service_name]["optional"].get(
"optional"].get("properties", set({})) "properties", set({})
)
for prop in miot_service.properties: for prop in miot_service.properties:
if prop.name in set.union(set(required_properties.keys()), if prop.name in set.union(
optional_properties): set(required_properties.keys()), optional_properties
):
if prop.unit: if prop.unit:
prop.external_unit = self.unit_convert(prop.unit) prop.external_unit = self.unit_convert(prop.unit)
# prop.icon = self.icon_convert(prop.unit) # prop.icon = self.icon_convert(prop.unit)
@ -539,13 +572,16 @@ class MIoTDevice:
miot_service.platform = platform miot_service.platform = platform
# entity_category # entity_category
if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get( if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(
"entity_category", None): "entity_category", None
):
miot_service.entity_category = entity_category miot_service.entity_category = entity_category
return entity_data return entity_data
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
if (miot_prop.platform or if (
miot_prop.name not in SPEC_PROP_TRANS_MAP["properties"]): miot_prop.platform
or miot_prop.name not in SPEC_PROP_TRANS_MAP["properties"]
):
return False return False
prop_name = miot_prop.name prop_name = miot_prop.name
if isinstance(SPEC_PROP_TRANS_MAP["properties"][prop_name], str): if isinstance(SPEC_PROP_TRANS_MAP["properties"][prop_name], str):
@ -559,20 +595,27 @@ class MIoTDevice:
prop_access.add("write") prop_access.add("write")
if prop_access != (SPEC_PROP_TRANS_MAP["entities"][platform]["access"]): if prop_access != (SPEC_PROP_TRANS_MAP["entities"][platform]["access"]):
return False return False
if (miot_prop.format_.__name__ if (
not in SPEC_PROP_TRANS_MAP["entities"][platform]["format"]): miot_prop.format_.__name__
not in SPEC_PROP_TRANS_MAP["entities"][platform]["format"]
):
return False return False
miot_prop.device_class = SPEC_PROP_TRANS_MAP["properties"][prop_name][ miot_prop.device_class = SPEC_PROP_TRANS_MAP["properties"][prop_name][
"device_class"] "device_class"
]
# Optional params # Optional params
if "state_class" in SPEC_PROP_TRANS_MAP["properties"][prop_name]: if "state_class" in SPEC_PROP_TRANS_MAP["properties"][prop_name]:
miot_prop.state_class = SPEC_PROP_TRANS_MAP["properties"][ miot_prop.state_class = SPEC_PROP_TRANS_MAP["properties"][prop_name][
prop_name]["state_class"] "state_class"
if (not miot_prop.external_unit and "unit_of_measurement" ]
in SPEC_PROP_TRANS_MAP["properties"][prop_name]): if (
not miot_prop.external_unit
and "unit_of_measurement" in SPEC_PROP_TRANS_MAP["properties"][prop_name]
):
# Priority: spec_modify.unit > unit_convert > specv2entity.unit # Priority: spec_modify.unit > unit_convert > specv2entity.unit
miot_prop.external_unit = SPEC_PROP_TRANS_MAP["properties"][ miot_prop.external_unit = SPEC_PROP_TRANS_MAP["properties"][prop_name][
prop_name]["unit_of_measurement"] "unit_of_measurement"
]
# Priority: default.icon when device_class is set > spec_modify.icon # Priority: default.icon when device_class is set > spec_modify.icon
# > icon_convert # > icon_convert
miot_prop.platform = platform miot_prop.platform = platform
@ -581,14 +624,12 @@ class MIoTDevice:
def spec_transform(self) -> None: def spec_transform(self) -> None:
"""Parse service, property, event, action from device spec.""" """Parse service, property, event, action from device spec."""
# STEP 1: device conversion # STEP 1: device conversion
device_entity = self.parse_miot_device_entity( device_entity = self.parse_miot_device_entity(spec_instance=self.spec_instance)
spec_instance=self.spec_instance)
if device_entity: if device_entity:
self.append_entity(entity_data=device_entity) self.append_entity(entity_data=device_entity)
# STEP 2: service conversion # STEP 2: service conversion
for service in self.spec_instance.services: for service in self.spec_instance.services:
service_entity = self.parse_miot_service_entity( service_entity = self.parse_miot_service_entity(miot_service=service)
miot_service=service)
if service_entity: if service_entity:
self.append_entity(entity_data=service_entity) self.append_entity(entity_data=service_entity)
# STEP 3.1: property conversion # STEP 3.1: property conversion
@ -772,14 +813,14 @@ class MIoTDevice:
if spec_unit in {"percentage"}: if spec_unit in {"percentage"}:
return "mdi:percent" return "mdi:percent"
if spec_unit in { if spec_unit in {
"weeks", "weeks",
"days", "days",
"hour", "hour",
"hours", "hours",
"minutes", "minutes",
"seconds", "seconds",
"ms", "ms",
"μs", "μs",
}: }:
return "mdi:clock" return "mdi:clock"
if spec_unit in {"celsius"}: if spec_unit in {"celsius"}:
@ -836,13 +877,13 @@ class MIoTDevice:
self._sub_id += 1 self._sub_id += 1
return self._sub_id return self._sub_id
def __on_device_state_changed(self, did: str, state: MIoTDeviceState, def __on_device_state_changed(
ctx: Any) -> None: self, did: str, state: MIoTDeviceState, ctx: Any
) -> None:
self._online = state == MIoTDeviceState.ONLINE self._online = state == MIoTDeviceState.ONLINE
for key, sub_list in self._device_state_sub_list.items(): for key, sub_list in self._device_state_sub_list.items():
for handler in sub_list.values(): for handler in sub_list.values():
self.miot_client.main_loop.call_soon_threadsafe( self.miot_client.main_loop.call_soon_threadsafe(handler, key, state)
handler, key, state)
class MIoTServiceEntity(Entity): class MIoTServiceEntity(Entity):
@ -859,13 +900,11 @@ class MIoTServiceEntity(Entity):
_value_sub_ids: dict[str, int] _value_sub_ids: dict[str, int]
_event_occurred_handler: Optional[Callable[[MIoTSpecEvent, dict], None]] _event_occurred_handler: Optional[Callable[[MIoTSpecEvent, dict], None]]
_prop_changed_subs: dict[MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], _prop_changed_subs: dict[MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]
None]]
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle] _pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
def __init__(self, miot_device: MIoTDevice, def __init__(self, miot_device: MIoTDevice, entity_data: MIoTEntityData) -> None:
entity_data: MIoTEntityData) -> None:
if miot_device is None or entity_data is None or entity_data.spec is None: if miot_device is None or entity_data is None or entity_data.spec is None:
raise MIoTDeviceError("init error, invalid params") raise MIoTDeviceError("init error, invalid params")
self.miot_device = miot_device self.miot_device = miot_device
@ -886,7 +925,8 @@ class MIoTServiceEntity(Entity):
) )
self._attr_name = ( self._attr_name = (
f"{'* ' if self.entity_data.spec.proprietary else ' '}" f"{'* ' if self.entity_data.spec.proprietary else ' '}"
f"{self.entity_data.spec.description_trans}") f"{self.entity_data.spec.description_trans}"
)
self._attr_entity_category = entity_data.spec.entity_category self._attr_entity_category = entity_data.spec.entity_category
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
@ -906,8 +946,7 @@ class MIoTServiceEntity(Entity):
) )
@property @property
def event_occurred_handler( def event_occurred_handler(self) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
self) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
return self._event_occurred_handler return self._event_occurred_handler
@event_occurred_handler.setter @event_occurred_handler.setter
@ -915,8 +954,8 @@ class MIoTServiceEntity(Entity):
self._event_occurred_handler = func self._event_occurred_handler = func
def sub_prop_changed( def sub_prop_changed(
self, prop: MIoTSpecProperty, self, prop: MIoTSpecProperty, handler: Callable[[MIoTSpecProperty, Any], None]
handler: Callable[[MIoTSpecProperty, Any], None]) -> None: ) -> None:
if not prop or not handler: if not prop or not handler:
_LOGGER.error("sub_prop_changed error, invalid prop/handler") _LOGGER.error("sub_prop_changed error, invalid prop/handler")
return return
@ -934,7 +973,8 @@ class MIoTServiceEntity(Entity):
if isinstance(self.entity_data.spec, MIoTSpecService): if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f"s.{self.entity_data.spec.iid}" state_id = f"s.{self.entity_data.spec.iid}"
self._state_sub_id = self.miot_device.sub_device_state( self._state_sub_id = self.miot_device.sub_device_state(
key=state_id, handler=self.__on_device_state_changed) key=state_id, handler=self.__on_device_state_changed
)
# Sub prop # Sub prop
for prop in self.entity_data.props: for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable: if not prop.notifiable and not prop.readable:
@ -949,9 +989,8 @@ class MIoTServiceEntity(Entity):
for event in self.entity_data.events: for event in self.entity_data.events:
key = f"e.{event.service.iid}.{event.iid}" key = f"e.{event.service.iid}.{event.iid}"
self._value_sub_ids[key] = self.miot_device.sub_event( self._value_sub_ids[key] = self.miot_device.sub_event(
handler=self.__on_event_occurred, handler=self.__on_event_occurred, siid=event.service.iid, eiid=event.iid
siid=event.service.iid, )
eiid=event.iid)
# Refresh value # Refresh value
if self._attr_available: if self._attr_available:
@ -964,34 +1003,30 @@ class MIoTServiceEntity(Entity):
state_id = "s.0" state_id = "s.0"
if isinstance(self.entity_data.spec, MIoTSpecService): if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f"s.{self.entity_data.spec.iid}" state_id = f"s.{self.entity_data.spec.iid}"
self.miot_device.unsub_device_state(key=state_id, self.miot_device.unsub_device_state(key=state_id, sub_id=self._state_sub_id)
sub_id=self._state_sub_id)
# Unsub prop # Unsub prop
for prop in self.entity_data.props: for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable: if not prop.notifiable and not prop.readable:
continue continue
sub_id = self._value_sub_ids.pop(f"p.{prop.service.iid}.{prop.iid}", sub_id = self._value_sub_ids.pop(f"p.{prop.service.iid}.{prop.iid}", None)
None)
if sub_id: if sub_id:
self.miot_device.unsub_property(siid=prop.service.iid, self.miot_device.unsub_property(
piid=prop.iid, siid=prop.service.iid, piid=prop.iid, sub_id=sub_id
sub_id=sub_id) )
# Unsub event # Unsub event
for event in self.entity_data.events: for event in self.entity_data.events:
sub_id = self._value_sub_ids.pop( sub_id = self._value_sub_ids.pop(f"e.{event.service.iid}.{event.iid}", None)
f"e.{event.service.iid}.{event.iid}", None)
if sub_id: if sub_id:
self.miot_device.unsub_event(siid=event.service.iid, self.miot_device.unsub_event(
eiid=event.iid, siid=event.service.iid, eiid=event.iid, sub_id=sub_id
sub_id=sub_id) )
def get_map_value(self, map_: Optional[dict[int, Any]], key: int) -> Any: def get_map_value(self, map_: Optional[dict[int, Any]], key: int) -> Any:
if map_ is None: if map_ is None:
return None return None
return map_.get(key, None) return map_.get(key, None)
def get_map_key(self, map_: Optional[dict[int, Any]], def get_map_key(self, map_: Optional[dict[int, Any]], value: Any) -> Optional[int]:
value: Any) -> Optional[int]:
if map_ is None: if map_ is None:
return None return None
for key, value_ in map_.items(): for key, value_ in map_.items():
@ -1009,8 +1044,7 @@ class MIoTServiceEntity(Entity):
return None return None
return self._prop_value_map.get(prop, None) return self._prop_value_map.get(prop, None)
def set_prop_value(self, prop: Optional[MIoTSpecProperty], def set_prop_value(self, prop: Optional[MIoTSpecProperty], value: Any) -> None:
value: Any) -> None:
if not prop: if not prop:
_LOGGER.error( _LOGGER.error(
"set_prop_value error, property is None, %s, %s", "set_prop_value error, property is None, %s, %s",
@ -1033,11 +1067,15 @@ class MIoTServiceEntity(Entity):
) )
value = prop.value_format(value) value = prop.value_format(value)
if prop not in self.entity_data.props: if prop not in self.entity_data.props:
raise RuntimeError(f"set property failed, unknown property, " raise RuntimeError(
f"{self.entity_id}, {self.name}, {prop.name}") f"set property failed, unknown property, "
f"{self.entity_id}, {self.name}, {prop.name}"
)
if not prop.writable: if not prop.writable:
raise RuntimeError(f"set property failed, not writable, " raise RuntimeError(
f"{self.entity_id}, {self.name}, {prop.name}") f"set property failed, not writable, "
f"{self.entity_id}, {self.name}, {prop.name}"
)
try: try:
await self.miot_device.miot_client.set_prop_async( await self.miot_device.miot_client.set_prop_async(
did=self.miot_device.did, did=self.miot_device.did,
@ -1047,7 +1085,8 @@ class MIoTServiceEntity(Entity):
) )
except MIoTClientError as e: except MIoTClientError as e:
raise RuntimeError( raise RuntimeError(
f"{e}, {self.entity_id}, {self.name}, {prop.name}") from e f"{e}, {self.entity_id}, {self.name}, {prop.name}"
) from e
if update_value: if update_value:
self._prop_value_map[prop] = value self._prop_value_map[prop] = value
if write_ha_state: if write_ha_state:
@ -1064,32 +1103,40 @@ class MIoTServiceEntity(Entity):
prop = set_property.get("prop") prop = set_property.get("prop")
value = set_property.get("value") value = set_property.get("value")
if not prop: if not prop:
raise RuntimeError(f"set property failed, property is None, " raise RuntimeError(
f"{self.entity_id}, {self.name}") f"set property failed, property is None, "
f"{self.entity_id}, {self.name}"
)
set_property["value"] = prop.value_format(value) set_property["value"] = prop.value_format(value)
if prop not in self.entity_data.props: if prop not in self.entity_data.props:
raise RuntimeError( raise RuntimeError(
f"set property failed, unknown property, " f"set property failed, unknown property, "
f"{self.entity_id}, {self.name}, {prop.name}") f"{self.entity_id}, {self.name}, {prop.name}"
)
if not prop.writable: if not prop.writable:
raise RuntimeError( raise RuntimeError(
f"set property failed, not writable, " f"set property failed, not writable, "
f"{self.entity_id}, {self.name}, {prop.name}") f"{self.entity_id}, {self.name}, {prop.name}"
)
try: try:
await self.miot_device.miot_client.set_props_async([{ 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, "did": self.miot_device.did,
"value": set_property["value"], "siid": set_property["prop"].service.iid,
} for set_property in set_properties_list]) "piid": set_property["prop"].iid,
"value": set_property["value"],
}
for set_property in set_properties_list
]
)
except MIoTClientError as e: except MIoTClientError as e:
raise RuntimeError( raise RuntimeError(
f"{e}, {self.entity_id}, {self.name}, {'/'.join([set_property['prop'].name for set_property in set_properties_list])}" f"{e}, {self.entity_id}, {self.name}, {'/'.join([set_property['prop'].name for set_property in set_properties_list])}"
) from e ) from e
if update_value: if update_value:
for set_property in set_properties_list: for set_property in set_properties_list:
self._prop_value_map[ self._prop_value_map[set_property["prop"]] = set_property["value"]
set_property["prop"]] = set_property["value"]
if write_ha_state: if write_ha_state:
self.async_write_ha_state() self.async_write_ha_state()
return True return True
@ -1118,22 +1165,23 @@ class MIoTServiceEntity(Entity):
prop.name, prop.name,
) )
return None return None
result = prop.value_format(await result = prop.value_format(
self.miot_device.miot_client.get_prop_async( await self.miot_device.miot_client.get_prop_async(
did=self.miot_device.did, did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid
siid=prop.service.iid, )
piid=prop.iid)) )
if result != self._prop_value_map[prop]: if result != self._prop_value_map[prop]:
self._prop_value_map[prop] = result self._prop_value_map[prop] = result
self.async_write_ha_state() self.async_write_ha_state()
return result return result
async def action_async(self, async def action_async(
action: MIoTSpecAction, self, action: MIoTSpecAction, in_list: Optional[list] = None
in_list: Optional[list] = None) -> bool: ) -> bool:
if not action: if not action:
raise RuntimeError( raise RuntimeError(
f"action failed, action is None, {self.entity_id}, {self.name}") f"action failed, action is None, {self.entity_id}, {self.name}"
)
try: try:
await self.miot_device.miot_client.action_async( await self.miot_device.miot_client.action_async(
did=self.miot_device.did, did=self.miot_device.did,
@ -1143,7 +1191,8 @@ class MIoTServiceEntity(Entity):
) )
except MIoTClientError as e: except MIoTClientError as e:
raise RuntimeError( raise RuntimeError(
f"{e}, {self.entity_id}, {self.name}, {action.name}") from e f"{e}, {self.entity_id}, {self.name}, {action.name}"
) from e
return True return True
def __on_properties_changed(self, params: dict, ctx: Any) -> None: def __on_properties_changed(self, params: dict, ctx: Any) -> None:
@ -1164,8 +1213,7 @@ class MIoTServiceEntity(Entity):
if self._event_occurred_handler is None: if self._event_occurred_handler is None:
return return
for event in self.entity_data.events: for event in self.entity_data.events:
if event.iid != params["eiid"] or event.service.iid != params[ if event.iid != params["eiid"] or event.service.iid != params["siid"]:
"siid"]:
continue continue
trans_arg = {} trans_arg = {}
for item in params["arguments"]: for item in params["arguments"]:
@ -1176,8 +1224,7 @@ class MIoTServiceEntity(Entity):
self._event_occurred_handler(event, trans_arg) self._event_occurred_handler(event, trans_arg)
break break
def __on_device_state_changed(self, key: str, def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None:
state: MIoTDeviceState) -> None:
state_new = state == MIoTDeviceState.ONLINE state_new = state == MIoTDeviceState.ONLINE
if state_new == self._attr_available: if state_new == self._attr_available:
return return
@ -1192,11 +1239,13 @@ class MIoTServiceEntity(Entity):
if not prop.readable: if not prop.readable:
continue continue
self.miot_device.miot_client.request_refresh_prop( self.miot_device.miot_client.request_refresh_prop(
did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid) did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid
)
if self._pending_write_ha_state_timer: if self._pending_write_ha_state_timer:
self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer.cancel()
self._pending_write_ha_state_timer = self._main_loop.call_later( self._pending_write_ha_state_timer = self._main_loop.call_later(
1, self.__write_ha_state_handler) 1, self.__write_ha_state_handler
)
def __write_ha_state_handler(self) -> None: def __write_ha_state_handler(self) -> None:
self._pending_write_ha_state_timer = None self._pending_write_ha_state_timer = None
@ -1237,17 +1286,16 @@ class MIoTPropertyEntity(Entity):
self._pending_write_ha_state_timer = None self._pending_write_ha_state_timer = None
# Gen entity_id # Gen entity_id
self.entity_id = self.miot_device.gen_prop_entity_id( self.entity_id = self.miot_device.gen_prop_entity_id(
ha_domain=DOMAIN, ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, piid=spec.iid
spec_name=spec.name, )
siid=spec.service.iid,
piid=spec.iid)
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
self._attr_should_poll = False self._attr_should_poll = False
self._attr_has_entity_name = True self._attr_has_entity_name = True
self._attr_name = ( self._attr_name = (
f"{'* ' if self.spec.proprietary else ' '}" f"{'* ' if self.spec.proprietary else ' '}"
f"{self.service.description_trans} {spec.description_trans}") f"{self.service.description_trans} {spec.description_trans}"
)
self._attr_available = miot_device.online self._attr_available = miot_device.online
_LOGGER.info( _LOGGER.info(
@ -1273,9 +1321,8 @@ class MIoTPropertyEntity(Entity):
) )
# Sub value changed # Sub value changed
self._value_sub_id = self.miot_device.sub_property( self._value_sub_id = self.miot_device.sub_property(
handler=self.__on_value_changed, handler=self.__on_value_changed, siid=self.service.iid, piid=self.spec.iid
siid=self.service.iid, )
piid=self.spec.iid)
# Refresh value # Refresh value
if self._attr_available: if self._attr_available:
self.__request_refresh_prop() self.__request_refresh_prop()
@ -1285,11 +1332,11 @@ class MIoTPropertyEntity(Entity):
self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer.cancel()
self._pending_write_ha_state_timer = None self._pending_write_ha_state_timer = None
self.miot_device.unsub_device_state( self.miot_device.unsub_device_state(
key=f"{self.service.iid}.{self.spec.iid}", key=f"{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id
sub_id=self._state_sub_id) )
self.miot_device.unsub_property(siid=self.service.iid, self.miot_device.unsub_property(
piid=self.spec.iid, siid=self.service.iid, piid=self.spec.iid, sub_id=self._value_sub_id
sub_id=self._value_sub_id) )
def get_vlist_description(self, value: Any) -> Optional[str]: def get_vlist_description(self, value: Any) -> Optional[str]:
if not self._value_list: if not self._value_list:
@ -1322,14 +1369,15 @@ class MIoTPropertyEntity(Entity):
async def get_property_async(self) -> Any: async def get_property_async(self) -> Any:
if not self.spec.readable: if not self.spec.readable:
_LOGGER.error("get property failed, not readable, %s, %s", _LOGGER.error(
self.entity_id, self.name) "get property failed, not readable, %s, %s", self.entity_id, self.name
)
return None return None
return self.spec.value_format( return self.spec.value_format(
await self.miot_device.miot_client.get_prop_async( await self.miot_device.miot_client.get_prop_async(
did=self.miot_device.did, did=self.miot_device.did, siid=self.spec.service.iid, piid=self.spec.iid
siid=self.spec.service.iid, )
piid=self.spec.iid)) )
def __on_value_changed(self, params: dict, ctx: Any) -> None: def __on_value_changed(self, params: dict, ctx: Any) -> None:
_LOGGER.debug("property changed, %s", params) _LOGGER.debug("property changed, %s", params)
@ -1338,8 +1386,7 @@ class MIoTPropertyEntity(Entity):
if not self._pending_write_ha_state_timer: if not self._pending_write_ha_state_timer:
self.async_write_ha_state() self.async_write_ha_state()
def __on_device_state_changed(self, key: str, def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None:
state: MIoTDeviceState) -> None:
self._attr_available = state == MIoTDeviceState.ONLINE self._attr_available = state == MIoTDeviceState.ONLINE
if not self._attr_available: if not self._attr_available:
self.async_write_ha_state() self.async_write_ha_state()
@ -1350,13 +1397,13 @@ class MIoTPropertyEntity(Entity):
def __request_refresh_prop(self) -> None: def __request_refresh_prop(self) -> None:
if self.spec.readable: if self.spec.readable:
self.miot_device.miot_client.request_refresh_prop( self.miot_device.miot_client.request_refresh_prop(
did=self.miot_device.did, did=self.miot_device.did, siid=self.service.iid, piid=self.spec.iid
siid=self.service.iid, )
piid=self.spec.iid)
if self._pending_write_ha_state_timer: if self._pending_write_ha_state_timer:
self._pending_write_ha_state_timer.cancel() self._pending_write_ha_state_timer.cancel()
self._pending_write_ha_state_timer = self._main_loop.call_later( self._pending_write_ha_state_timer = self._main_loop.call_later(
1, self.__write_ha_state_handler) 1, self.__write_ha_state_handler
)
def __write_ha_state_handler(self) -> None: def __write_ha_state_handler(self) -> None:
self._pending_write_ha_state_timer = None self._pending_write_ha_state_timer = None
@ -1387,17 +1434,16 @@ class MIoTEventEntity(Entity):
self._main_loop = miot_device.miot_client.main_loop self._main_loop = miot_device.miot_client.main_loop
# Gen entity_id # Gen entity_id
self.entity_id = self.miot_device.gen_event_entity_id( self.entity_id = self.miot_device.gen_event_entity_id(
ha_domain=DOMAIN, ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, eiid=spec.iid
spec_name=spec.name, )
siid=spec.service.iid,
eiid=spec.iid)
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
self._attr_should_poll = False self._attr_should_poll = False
self._attr_has_entity_name = True self._attr_has_entity_name = True
self._attr_name = ( self._attr_name = (
f"{'* ' if self.spec.proprietary else ' '}" f"{'* ' if self.spec.proprietary else ' '}"
f"{self.service.description_trans} {spec.description_trans}") f"{self.service.description_trans} {spec.description_trans}"
)
self._attr_available = miot_device.online self._attr_available = miot_device.online
self._attr_event_types = [spec.description_trans] self._attr_event_types = [spec.description_trans]
@ -1428,23 +1474,21 @@ class MIoTEventEntity(Entity):
) )
# Sub value changed # Sub value changed
self._value_sub_id = self.miot_device.sub_event( self._value_sub_id = self.miot_device.sub_event(
handler=self.__on_event_occurred, handler=self.__on_event_occurred, siid=self.service.iid, eiid=self.spec.iid
siid=self.service.iid, )
eiid=self.spec.iid)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state( self.miot_device.unsub_device_state(
key=f"event.{self.service.iid}.{self.spec.iid}", key=f"event.{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id
sub_id=self._state_sub_id) )
self.miot_device.unsub_event(siid=self.service.iid, self.miot_device.unsub_event(
eiid=self.spec.iid, siid=self.service.iid, eiid=self.spec.iid, sub_id=self._value_sub_id
sub_id=self._value_sub_id) )
@abstractmethod @abstractmethod
def on_event_occurred(self, def on_event_occurred(
name: str, self, name: str, arguments: dict[str, Any] | None = None
arguments: dict[str, Any] | None = None) -> None: ) -> None: ...
...
def __on_event_occurred(self, params: dict, ctx: Any) -> None: def __on_event_occurred(self, params: dict, ctx: Any) -> None:
_LOGGER.debug("event occurred, %s", params) _LOGGER.debug("event occurred, %s", params)
@ -1455,8 +1499,9 @@ class MIoTEventEntity(Entity):
continue continue
if "piid" in item: if "piid" in item:
trans_arg[self._arguments_map[item["piid"]]] = item["value"] trans_arg[self._arguments_map[item["piid"]]] = item["value"]
elif isinstance(item["value"], list) and len( elif isinstance(item["value"], list) and len(item["value"]) == len(
item["value"]) == len(self.spec.argument): self.spec.argument
):
# Dirty fix for cloud multi-arguments # Dirty fix for cloud multi-arguments
trans_arg = { trans_arg = {
prop.description_trans: item["value"][index] prop.description_trans: item["value"][index]
@ -1470,12 +1515,10 @@ class MIoTEventEntity(Entity):
params, params,
error, error,
) )
self.on_event_occurred(name=self.spec.description_trans, self.on_event_occurred(name=self.spec.description_trans, arguments=trans_arg)
arguments=trans_arg)
self.async_write_ha_state() self.async_write_ha_state()
def __on_device_state_changed(self, key: str, def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None:
state: MIoTDeviceState) -> None:
state_new = state == MIoTDeviceState.ONLINE state_new = state == MIoTDeviceState.ONLINE
if state_new == self._attr_available: if state_new == self._attr_available:
return return
@ -1507,17 +1550,16 @@ class MIoTActionEntity(Entity):
self._state_sub_id = 0 self._state_sub_id = 0
# Gen entity_id # Gen entity_id
self.entity_id = self.miot_device.gen_action_entity_id( self.entity_id = self.miot_device.gen_action_entity_id(
ha_domain=DOMAIN, ha_domain=DOMAIN, spec_name=spec.name, siid=spec.service.iid, aiid=spec.iid
spec_name=spec.name, )
siid=spec.service.iid,
aiid=spec.iid)
# Set entity attr # Set entity attr
self._attr_unique_id = self.entity_id self._attr_unique_id = self.entity_id
self._attr_should_poll = False self._attr_should_poll = False
self._attr_has_entity_name = True self._attr_has_entity_name = True
self._attr_name = ( self._attr_name = (
f"{'* ' if self.spec.proprietary else ' '}" f"{'* ' if self.spec.proprietary else ' '}"
f"{self.service.description_trans} {spec.description_trans}") f"{self.service.description_trans} {spec.description_trans}"
)
self._attr_available = miot_device.online self._attr_available = miot_device.online
_LOGGER.debug( _LOGGER.debug(
@ -1541,11 +1583,10 @@ class MIoTActionEntity(Entity):
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state( self.miot_device.unsub_device_state(
key=f"a.{self.service.iid}.{self.spec.iid}", key=f"a.{self.service.iid}.{self.spec.iid}", sub_id=self._state_sub_id
sub_id=self._state_sub_id) )
async def action_async(self, async def action_async(self, in_list: Optional[list] = None) -> Optional[list]:
in_list: Optional[list] = None) -> Optional[list]:
try: try:
return await self.miot_device.miot_client.action_async( return await self.miot_device.miot_client.action_async(
did=self.miot_device.did, did=self.miot_device.did,
@ -1556,8 +1597,7 @@ class MIoTActionEntity(Entity):
except MIoTClientError as e: except MIoTClientError as e:
raise RuntimeError(f"{e}, {self.entity_id}, {self.name}") from e raise RuntimeError(f"{e}, {self.entity_id}, {self.name}") from e
def __on_device_state_changed(self, key: str, def __on_device_state_changed(self, key: str, state: MIoTDeviceState) -> None:
state: MIoTDeviceState) -> None:
state_new = state == MIoTDeviceState.ONLINE state_new = state == MIoTDeviceState.ONLINE
if state_new == self._attr_available: if state_new == self._attr_available:
return return

View File

@ -162,18 +162,17 @@ class _MIoTLanDevice:
# All functions SHOULD be called from the internal loop # All functions SHOULD be called from the internal loop
def __init__(self, def __init__(
manager: "MIoTLan", self, manager: "MIoTLan", did: str, token: str, ip: Optional[str] = None
did: str, ) -> None:
token: str,
ip: Optional[str] = None) -> None:
self._manager: MIoTLan = manager self._manager: MIoTLan = manager
self.did = did self.did = did
self.token = bytes.fromhex(token) self.token = bytes.fromhex(token)
aes_key: bytes = self.__md5(self.token) aes_key: bytes = self.__md5(self.token)
aex_iv: bytes = self.__md5(aes_key + self.token) aex_iv: bytes = self.__md5(aes_key + self.token)
self.cipher = Cipher(algorithms.AES128(aes_key), modes.CBC(aex_iv), self.cipher = Cipher(
default_backend()) algorithms.AES128(aes_key), modes.CBC(aex_iv), default_backend()
)
self.ip = ip self.ip = ip
self.offset = 0 self.offset = 0
self.subscribed = False self.subscribed = False
@ -200,8 +199,7 @@ class _MIoTLanDevice:
self.ip = ip self.ip = ip
if self._if_name != if_name: if self._if_name != if_name:
self._if_name = if_name self._if_name = if_name
_LOGGER.info("device if_name change, %s, %s", self._if_name, _LOGGER.info("device if_name change, %s, %s", self._if_name, self.did)
self.did)
self.__update_keep_alive(state=_MIoTLanDeviceState.FRESH) self.__update_keep_alive(state=_MIoTLanDeviceState.FRESH)
@property @property
@ -215,18 +213,16 @@ class _MIoTLanDevice:
self._online = online self._online = online
self._manager.broadcast_device_state( self._manager.broadcast_device_state(
did=self.did, did=self.did,
state={ state={"online": self._online, "push_available": self.subscribed},
"online": self._online,
"push_available": self.subscribed
},
) )
@property @property
def if_name(self) -> Optional[str]: def if_name(self) -> Optional[str]:
return self._if_name return self._if_name
def gen_packet(self, out_buffer: bytearray, clear_data: dict, did: str, def gen_packet(
offset: int) -> int: self, out_buffer: bytearray, clear_data: dict, did: str, offset: int
) -> int:
clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode("utf-8") clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode("utf-8")
padder = padding.PKCS7(algorithms.AES128.block_size).padder() padder = padding.PKCS7(algorithms.AES128.block_size).padder()
padded_data = padder.update(clear_bytes) + padder.finalize() padded_data = padder.update(clear_bytes) + padder.finalize()
@ -235,8 +231,9 @@ class _MIoTLanDevice:
encryptor = self.cipher.encryptor() encryptor = self.cipher.encryptor()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize() encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
data_len: int = len(encrypted_data) + self.OT_HEADER_LEN data_len: int = len(encrypted_data) + self.OT_HEADER_LEN
out_buffer[:32] = struct.pack(">HHQI16s", self.OT_HEADER, data_len, out_buffer[:32] = struct.pack(
int(did), offset, self.token) ">HHQI16s", self.OT_HEADER, data_len, int(did), offset, self.token
)
out_buffer[32:data_len] = encrypted_data out_buffer[32:data_len] = encrypted_data
msg_md5: bytes = self.__md5(out_buffer[0:data_len]) msg_md5: bytes = self.__md5(out_buffer[0:data_len])
out_buffer[16:32] = msg_md5 out_buffer[16:32] = msg_md5
@ -250,11 +247,11 @@ class _MIoTLanDevice:
if md5_orig != md5_calc: if md5_orig != md5_calc:
raise ValueError(f"invalid md5, {md5_orig}, {md5_calc}") raise ValueError(f"invalid md5, {md5_orig}, {md5_calc}")
decryptor = self.cipher.decryptor() decryptor = self.cipher.decryptor()
decrypted_padded_data = (decryptor.update(encrypted_data[32:data_len]) + decrypted_padded_data = (
decryptor.finalize()) decryptor.update(encrypted_data[32:data_len]) + decryptor.finalize()
)
unpadder = padding.PKCS7(algorithms.AES128.block_size).unpadder() unpadder = padding.PKCS7(algorithms.AES128.block_size).unpadder()
decrypted_data = unpadder.update( decrypted_data = unpadder.update(decrypted_padded_data) + unpadder.finalize()
decrypted_padded_data) + unpadder.finalize()
# Some device will add a redundant \0 at the end of JSON string # Some device will add a redundant \0 at the end of JSON string
decrypted_data = decrypted_data.rstrip(b"\x00") decrypted_data = decrypted_data.rstrip(b"\x00")
return json.loads(decrypted_data) return json.loads(decrypted_data)
@ -305,10 +302,7 @@ class _MIoTLanDevice:
self.subscribed = False self.subscribed = False
self._manager.broadcast_device_state( self._manager.broadcast_device_state(
did=self.did, did=self.did,
state={ state={"online": self._online, "push_available": self.subscribed},
"online": self._online,
"push_available": self.subscribed
},
) )
def on_delete(self) -> None: def on_delete(self) -> None:
@ -321,35 +315,42 @@ class _MIoTLanDevice:
_LOGGER.debug("miot lan device delete, %s", self.did) _LOGGER.debug("miot lan device delete, %s", self.did)
def update_info(self, info: dict) -> None: def update_info(self, info: dict) -> None:
if ("token" in info and len(info["token"]) == 32 and if (
info["token"].upper() != self.token.hex().upper()): "token" in info
and len(info["token"]) == 32
and info["token"].upper() != self.token.hex().upper()
):
# Update token # Update token
self.token = bytes.fromhex(info["token"]) self.token = bytes.fromhex(info["token"])
aes_key: bytes = self.__md5(self.token) aes_key: bytes = self.__md5(self.token)
aex_iv: bytes = self.__md5(aes_key + self.token) aex_iv: bytes = self.__md5(aes_key + self.token)
self.cipher = Cipher(algorithms.AES128(aes_key), modes.CBC(aex_iv), self.cipher = Cipher(
default_backend()) algorithms.AES128(aes_key), modes.CBC(aex_iv), default_backend()
)
_LOGGER.debug("update token, %s", self.did) _LOGGER.debug("update token, %s", self.did)
def __subscribe_handler(self, msg: dict, sub_ts: int) -> None: def __subscribe_handler(self, msg: dict, sub_ts: int) -> None:
if ("result" not in msg or "code" not in msg["result"] or if (
msg["result"]["code"] != 0): "result" not in msg
or "code" not in msg["result"]
or msg["result"]["code"] != 0
):
_LOGGER.error("subscribe device error, %s, %s", self.did, msg) _LOGGER.error("subscribe device error, %s, %s", self.did, msg)
return return
self.subscribed = True self.subscribed = True
self.sub_ts = sub_ts self.sub_ts = sub_ts
self._manager.broadcast_device_state( self._manager.broadcast_device_state(
did=self.did, did=self.did,
state={ state={"online": self._online, "push_available": self.subscribed},
"online": self._online,
"push_available": self.subscribed
},
) )
_LOGGER.info("subscribe success, %s, %s", self._if_name, self.did) _LOGGER.info("subscribe success, %s, %s", self._if_name, self.did)
def __unsubscribe_handler(self, msg: dict, ctx: Any) -> None: def __unsubscribe_handler(self, msg: dict, ctx: Any) -> None:
if ("result" not in msg or "code" not in msg["result"] or if (
msg["result"]["code"] != 0): "result" not in msg
or "code" not in msg["result"]
or msg["result"]["code"] != 0
):
_LOGGER.error("unsubscribe device error, %s, %s", self.did, msg) _LOGGER.error("unsubscribe device error, %s, %s", self.did, msg)
return return
_LOGGER.info("unsubscribe success, %s, %s", self._if_name, self.did) _LOGGER.info("unsubscribe success, %s, %s", self._if_name, self.did)
@ -372,8 +373,11 @@ class _MIoTLanDevice:
self.__update_keep_alive, self.__update_keep_alive,
_MIoTLanDeviceState.PING1, _MIoTLanDeviceState.PING1,
) )
case (_MIoTLanDeviceState.PING1 | _MIoTLanDeviceState.PING2 | case (
_MIoTLanDeviceState.PING3): _MIoTLanDeviceState.PING1
| _MIoTLanDeviceState.PING2
| _MIoTLanDeviceState.PING3
):
# Set the timer first to avoid Any early returns # Set the timer first to avoid Any early returns
self._ka_timer = self._manager.internal_loop.call_later( self._ka_timer = self._manager.internal_loop.call_later(
self.FAST_PING_INTERVAL, self.FAST_PING_INTERVAL,
@ -411,16 +415,16 @@ class _MIoTLanDevice:
if not online: if not online:
self.online = False self.online = False
else: else:
if len(self._online_offline_history if len(self._online_offline_history) < self.NETWORK_UNSTABLE_CNT_TH or (
) < self.NETWORK_UNSTABLE_CNT_TH or ( ts_now - self._online_offline_history[0]["ts"]
ts_now - self._online_offline_history[0]["ts"] > self.NETWORK_UNSTABLE_TIME_TH
> self.NETWORK_UNSTABLE_TIME_TH): ):
self.online = True self.online = True
else: else:
_LOGGER.info("unstable device detected, %s", self.did) _LOGGER.info("unstable device detected, %s", self.did)
self._online_offline_timer = self._manager.internal_loop.call_later( self._online_offline_timer = self._manager.internal_loop.call_later(
self.NETWORK_UNSTABLE_RESUME_TH, self.NETWORK_UNSTABLE_RESUME_TH, self.__online_resume_handler
self.__online_resume_handler) )
def __online_resume_handler(self) -> None: def __online_resume_handler(self) -> None:
_LOGGER.info("unstable resume threshold past, %s", self.did) _LOGGER.info("unstable resume threshold past, %s", self.did)
@ -500,19 +504,21 @@ class MIoTLan:
self._net_ifs = set(net_ifs) self._net_ifs = set(net_ifs)
self._network = network self._network = network
self._network.sub_network_info( self._network.sub_network_info(
key="miot_lan", key="miot_lan", handler=self.__on_network_info_change_external_async
handler=self.__on_network_info_change_external_async) )
self._mips_service = mips_service self._mips_service = mips_service
self._mips_service.sub_service_change( self._mips_service.sub_service_change(
key="miot_lan", group_id="*", handler=self.__on_mips_service_change) key="miot_lan", group_id="*", handler=self.__on_mips_service_change
)
self._enable_subscribe = enable_subscribe self._enable_subscribe = enable_subscribe
self._virtual_did = (str(virtual_did) if self._virtual_did = (
(virtual_did is not None) else str( str(virtual_did) if (virtual_did is not None) else str(secrets.randbits(64))
secrets.randbits(64))) )
# Init socket probe message # Init socket probe message
probe_bytes = bytearray(self.OT_PROBE_LEN) probe_bytes = bytearray(self.OT_PROBE_LEN)
probe_bytes[:20] = ( probe_bytes[:20] = (
b"!1\x00\x20\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffMDID") b"!1\x00\x20\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffMDID"
)
probe_bytes[20:28] = struct.pack(">Q", int(self._virtual_did)) probe_bytes[20:28] = struct.pack(">Q", int(self._virtual_did))
probe_bytes[28:32] = b"\x00\x00\x00\x00" probe_bytes[28:32] = b"\x00\x00\x00\x00"
self._probe_msg = bytes(probe_bytes) self._probe_msg = bytes(probe_bytes)
@ -537,16 +543,17 @@ class MIoTLan:
self._init_lock = asyncio.Lock() self._init_lock = asyncio.Lock()
self._init_done = False self._init_done = False
if len(self._mips_service.get_services()) == 0 and len( if len(self._mips_service.get_services()) == 0 and len(self._net_ifs) > 0:
self._net_ifs) > 0:
_LOGGER.info("no central hub gateway service, init miot lan") _LOGGER.info("no central hub gateway service, init miot lan")
self._main_loop.call_later( self._main_loop.call_later(
0, lambda: self._main_loop.create_task(self.init_async())) 0, lambda: self._main_loop.create_task(self.init_async())
)
def __assert_service_ready(self) -> None: def __assert_service_ready(self) -> None:
if not self._init_done: if not self._init_done:
raise MIoTLanError("MIoT lan is not ready", raise MIoTLanError(
MIoTErrorCode.CODE_LAN_UNAVAILABLE) "MIoT lan is not ready", MIoTErrorCode.CODE_LAN_UNAVAILABLE
)
@property @property
def virtual_did(self) -> str: def virtual_did(self) -> str:
@ -585,8 +592,8 @@ class MIoTLan:
return return
try: try:
self._profile_models = await self._main_loop.run_in_executor( self._profile_models = await self._main_loop.run_in_executor(
None, load_yaml_file, None, load_yaml_file, gen_absolute_path(self.PROFILE_MODELS_FILE)
gen_absolute_path(self.PROFILE_MODELS_FILE)) )
except Exception as err: # pylint: disable=broad-exception-caught except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error("load profile models error, %s", err) _LOGGER.error("load profile models error, %s", err)
self._profile_models = {} self._profile_models = {}
@ -599,14 +606,16 @@ class MIoTLan:
self._init_done = True self._init_done = True
for handler in list(self._lan_state_sub_map.values()): for handler in list(self._lan_state_sub_map.values()):
self._main_loop.create_task(handler(True)) self._main_loop.create_task(handler(True))
_LOGGER.info("miot lan init, %s ,%s", self._net_ifs, _LOGGER.info(
self._available_net_ifs) "miot lan init, %s ,%s", self._net_ifs, self._available_net_ifs
)
def __internal_loop_thread(self) -> None: def __internal_loop_thread(self) -> None:
_LOGGER.info("miot lan thread start") _LOGGER.info("miot lan thread start")
self.__init_socket() self.__init_socket()
self._scan_timer = self._internal_loop.call_later( self._scan_timer = self._internal_loop.call_later(
int(3 * random.random()), self.__scan_devices) int(3 * random.random()), self.__scan_devices
)
self._internal_loop.run_forever() self._internal_loop.run_forever()
_LOGGER.info("miot lan thread exit") _LOGGER.info("miot lan thread exit")
@ -673,8 +682,8 @@ class MIoTLan:
self._enable_subscribe = enable_subscribe self._enable_subscribe = enable_subscribe
return return
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__update_subscribe_option, self.__update_subscribe_option, {"enable_subscribe": enable_subscribe}
{"enable_subscribe": enable_subscribe}) )
def update_devices(self, devices: dict[str, dict]) -> bool: def update_devices(self, devices: dict[str, dict]) -> bool:
_LOGGER.info("update devices, %s", devices) _LOGGER.info("update devices, %s", devices)
@ -690,8 +699,7 @@ class MIoTLan:
self._internal_loop.call_soon_threadsafe(self.__delete_devices, devices) self._internal_loop.call_soon_threadsafe(self.__delete_devices, devices)
return True return True
def sub_lan_state(self, key: str, handler: Callable[[bool], def sub_lan_state(self, key: str, handler: Callable[[bool], Coroutine]) -> None:
Coroutine]) -> None:
self._lan_state_sub_map[key] = handler self._lan_state_sub_map[key] = handler
def unsub_lan_state(self, key: str) -> None: def unsub_lan_state(self, key: str) -> None:
@ -708,9 +716,7 @@ class MIoTLan:
return False return False
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__sub_device_state, self.__sub_device_state,
_MIoTLanSubDeviceData(key=key, _MIoTLanSubDeviceData(key=key, handler=handler, handler_ctx=handler_ctx),
handler=handler,
handler_ctx=handler_ctx),
) )
return True return True
@ -719,7 +725,8 @@ class MIoTLan:
if not self._init_done: if not self._init_done:
return False return False
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__unsub_device_state, _MIoTLanUnsubDeviceData(key=key)) self.__unsub_device_state, _MIoTLanUnsubDeviceData(key=key)
)
return True return True
@final @final
@ -738,24 +745,24 @@ class MIoTLan:
key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}" key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}"
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__sub_broadcast, self.__sub_broadcast,
_MIoTLanRegisterBroadcastData(key=key, _MIoTLanRegisterBroadcastData(
handler=handler, key=key, handler=handler, handler_ctx=handler_ctx
handler_ctx=handler_ctx), ),
) )
return True return True
@final @final
def unsub_prop(self, def unsub_prop(
did: str, self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
siid: Optional[int] = None, ) -> bool:
piid: Optional[int] = None) -> bool:
if not self._init_done: if not self._init_done:
return False return False
if not self._enable_subscribe: if not self._enable_subscribe:
return False return False
key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}" key = f"{did}/p/{'#' if siid is None or piid is None else f'{siid}/{piid}'}"
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)) self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)
)
return True return True
@final @final
@ -774,90 +781,80 @@ class MIoTLan:
key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}"
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__sub_broadcast, self.__sub_broadcast,
_MIoTLanRegisterBroadcastData(key=key, _MIoTLanRegisterBroadcastData(
handler=handler, key=key, handler=handler, handler_ctx=handler_ctx
handler_ctx=handler_ctx), ),
) )
return True return True
@final @final
def unsub_event(self, def unsub_event(
did: str, self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
siid: Optional[int] = None, ) -> bool:
eiid: Optional[int] = None) -> bool:
if not self._init_done: if not self._init_done:
return False return False
if not self._enable_subscribe: if not self._enable_subscribe:
return False return False
key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}" key = f"{did}/e/{'#' if siid is None or eiid is None else f'{siid}/{eiid}'}"
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)) self.__unsub_broadcast, _MIoTLanUnregisterBroadcastData(key=key)
)
return True return True
@final @final
async def get_prop_async(self, async def get_prop_async(
did: str, self, did: str, siid: int, piid: int, timeout_ms: int = 10000
siid: int, ) -> Any:
piid: int,
timeout_ms: int = 10000) -> Any:
self.__assert_service_ready() self.__assert_service_ready()
result_obj = await self.__call_api_async( result_obj = await self.__call_api_async(
did=did, did=did,
msg={ msg={
"method": "get_properties", "method": "get_properties",
"params": [{ "params": [{"did": did, "siid": siid, "piid": piid}],
"did": did,
"siid": siid,
"piid": piid
}],
}, },
timeout_ms=timeout_ms, timeout_ms=timeout_ms,
) )
if (result_obj and "result" in result_obj and if (
len(result_obj["result"]) == 1 and result_obj
"did" in result_obj["result"][0] and and "result" in result_obj
result_obj["result"][0]["did"] == did): and len(result_obj["result"]) == 1
and "did" in result_obj["result"][0]
and result_obj["result"][0]["did"] == did
):
return result_obj["result"][0].get("value", None) return result_obj["result"][0].get("value", None)
return None return None
@final @final
async def set_prop_async(self, async def set_prop_async(
did: str, self, did: str, siid: int, piid: int, value: Any, timeout_ms: int = 10000
siid: int, ) -> dict:
piid: int,
value: Any,
timeout_ms: int = 10000) -> dict:
self.__assert_service_ready() self.__assert_service_ready()
result_obj = await self.__call_api_async( result_obj = await self.__call_api_async(
did=did, did=did,
msg={ msg={
"method": "method": "set_properties",
"set_properties", "params": [{"did": did, "siid": siid, "piid": piid, "value": value}],
"params": [{
"did": did,
"siid": siid,
"piid": piid,
"value": value
}],
}, },
timeout_ms=timeout_ms, timeout_ms=timeout_ms,
) )
if result_obj: if result_obj:
if ("result" in result_obj and len(result_obj["result"]) == 1 and if (
"did" in result_obj["result"][0] and "result" in result_obj
result_obj["result"][0]["did"] == did and and len(result_obj["result"]) == 1
"code" in result_obj["result"][0]): and "did" in result_obj["result"][0]
and result_obj["result"][0]["did"] == did
and "code" in result_obj["result"][0]
):
return result_obj["result"][0] return result_obj["result"][0]
if "code" in result_obj: if "code" in result_obj:
return result_obj return result_obj
raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR) raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR)
@final @final
async def set_props_async(self, async def set_props_async(
did: str, self, did: str, props_list: List[Dict[str, Any]], timeout_ms: int = 10000
props_list: List[Dict[str, Any]], ) -> dict:
timeout_ms: int = 10000) -> dict:
self.__assert_service_ready() self.__assert_service_ready()
result_obj = await self.__call_api_async( result_obj = await self.__call_api_async(
did=did, did=did,
@ -868,10 +865,12 @@ class MIoTLan:
timeout_ms=timeout_ms, timeout_ms=timeout_ms,
) )
if result_obj: if result_obj:
if ("result" in result_obj and if (
len(result_obj["result"]) == len(props_list) and "result" in result_obj
result_obj["result"][0].get("did") == did and and len(result_obj["result"]) == len(props_list)
all("code" in item for item in result_obj["result"])): and result_obj["result"][0].get("did") == did
and all("code" in item for item in result_obj["result"])
):
return result_obj["result"] return result_obj["result"]
if "error" in result_obj: if "error" in result_obj:
return result_obj["error"] return result_obj["error"]
@ -881,23 +880,15 @@ class MIoTLan:
} }
@final @final
async def action_async(self, async def action_async(
did: str, self, did: str, siid: int, aiid: int, in_list: list, timeout_ms: int = 10000
siid: int, ) -> dict:
aiid: int,
in_list: list,
timeout_ms: int = 10000) -> dict:
self.__assert_service_ready() self.__assert_service_ready()
result_obj = await self.__call_api_async( result_obj = await self.__call_api_async(
did=did, did=did,
msg={ msg={
"method": "action", "method": "action",
"params": { "params": {"did": did, "siid": siid, "aiid": aiid, "in": in_list},
"did": did,
"siid": siid,
"aiid": aiid,
"in": in_list
},
}, },
timeout_ms=timeout_ms, timeout_ms=timeout_ms,
) )
@ -909,8 +900,7 @@ class MIoTLan:
raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR) raise MIoTError("Invalid result", MIoTErrorCode.CODE_INTERNAL_ERROR)
@final @final
async def get_dev_list_async(self, async def get_dev_list_async(self, timeout_ms: int = 10000) -> dict[str, dict]:
timeout_ms: int = 10000) -> dict[str, dict]:
if not self._init_done: if not self._init_done:
return {} return {}
@ -920,30 +910,28 @@ class MIoTLan:
fut: asyncio.Future = self._main_loop.create_future() fut: asyncio.Future = self._main_loop.create_future()
self._internal_loop.call_soon_threadsafe( self._internal_loop.call_soon_threadsafe(
self.__get_dev_list, self.__get_dev_list,
_MIoTLanGetDevListData(handler=get_device_list_handler, _MIoTLanGetDevListData(
handler_ctx=fut, handler=get_device_list_handler, handler_ctx=fut, timeout_ms=timeout_ms
timeout_ms=timeout_ms), ),
) )
return await fut return await fut
async def __call_api_async(self, async def __call_api_async(
did: str, self, did: str, msg: dict, timeout_ms: int = 10000
msg: dict, ) -> dict:
timeout_ms: int = 10000) -> dict:
def call_api_handler(msg: dict, fut: asyncio.Future): def call_api_handler(msg: dict, fut: asyncio.Future):
self._main_loop.call_soon_threadsafe(fut.set_result, msg) self._main_loop.call_soon_threadsafe(fut.set_result, msg)
fut: asyncio.Future = self._main_loop.create_future() fut: asyncio.Future = self._main_loop.create_future()
self._internal_loop.call_soon_threadsafe(self.__call_api, did, msg, self._internal_loop.call_soon_threadsafe(
call_api_handler, fut, self.__call_api, did, msg, call_api_handler, fut, timeout_ms
timeout_ms) )
return await fut return await fut
async def __on_network_info_change_external_async( async def __on_network_info_change_external_async(
self, status: InterfaceStatus, info: NetworkInfo) -> None: self, status: InterfaceStatus, info: NetworkInfo
_LOGGER.info("on network info change, status: %s, info: %s", status, ) -> None:
info) _LOGGER.info("on network info change, status: %s, info: %s", status, info)
available_net_ifs = set() available_net_ifs = set()
for if_name in list(self._network.network_info.keys()): for if_name in list(self._network.network_info.keys()):
available_net_ifs.add(if_name) available_net_ifs.add(if_name)
@ -965,11 +953,10 @@ class MIoTLan:
_MIoTLanNetworkUpdateData(status=status, if_name=info.name), _MIoTLanNetworkUpdateData(status=status, if_name=info.name),
) )
async def __on_mips_service_change(self, group_id: str, async def __on_mips_service_change(
state: MipsServiceState, self, group_id: str, state: MipsServiceState, data: dict
data: dict) -> None: ) -> None:
_LOGGER.info("on mips service change, %s, %s, %s", group_id, state, _LOGGER.info("on mips service change, %s, %s, %s", group_id, state, data)
data)
if len(self._mips_service.get_services()) > 0: if len(self._mips_service.get_services()) > 0:
_LOGGER.info("find central service, deinit miot lan") _LOGGER.info("find central service, deinit miot lan")
await self.deinit_async() await self.deinit_async()
@ -982,10 +969,9 @@ class MIoTLan:
def ping(self, if_name: Optional[str], target_ip: str) -> None: def ping(self, if_name: Optional[str], target_ip: str) -> None:
if not target_ip: if not target_ip:
return return
self.__sendto(if_name=if_name, self.__sendto(
data=self._probe_msg, if_name=if_name, data=self._probe_msg, address=target_ip, port=self.OT_PORT
address=target_ip, )
port=self.OT_PORT)
def send2device( def send2device(
self, self,
@ -1034,27 +1020,22 @@ class MIoTLan:
handler_ctx: Any = None, handler_ctx: Any = None,
timeout_ms: Optional[int] = None, timeout_ms: Optional[int] = None,
) -> None: ) -> None:
def request_timeout_handler(req_data: _MIoTLanRequestData): def request_timeout_handler(req_data: _MIoTLanRequestData):
self._pending_requests.pop(req_data.msg_id, None) self._pending_requests.pop(req_data.msg_id, None)
if req_data and req_data.handler: if req_data and req_data.handler:
req_data.handler( req_data.handler(
{ {"code": MIoTErrorCode.CODE_TIMEOUT.value, "error": "timeout"},
"code": MIoTErrorCode.CODE_TIMEOUT.value,
"error": "timeout"
},
req_data.handler_ctx, req_data.handler_ctx,
) )
timer: Optional[asyncio.TimerHandle] = None timer: Optional[asyncio.TimerHandle] = None
request_data = _MIoTLanRequestData(msg_id=msg_id, request_data = _MIoTLanRequestData(
handler=handler, msg_id=msg_id, handler=handler, handler_ctx=handler_ctx, timeout=timer
handler_ctx=handler_ctx, )
timeout=timer)
if timeout_ms: if timeout_ms:
timer = self._internal_loop.call_later(timeout_ms / 1000, timer = self._internal_loop.call_later(
request_timeout_handler, timeout_ms / 1000, request_timeout_handler, request_data
request_data) )
request_data.timeout = timer request_data.timeout = timer
self._pending_requests[msg_id] = request_data self._pending_requests[msg_id] = request_data
self.__sendto(if_name=if_name, data=msg, address=ip, port=self.OT_PORT) self.__sendto(if_name=if_name, data=msg, address=ip, port=self.OT_PORT)
@ -1085,10 +1066,7 @@ class MIoTLan:
try: try:
self.send2device( self.send2device(
did=did, did=did,
msg={ msg={"from": "ha.xiaomi_home", **msg},
"from": "ha.xiaomi_home",
**msg
},
handler=handler, handler=handler,
handler_ctx=handler_ctx, handler_ctx=handler_ctx,
timeout_ms=timeout_ms, timeout_ms=timeout_ms,
@ -1096,10 +1074,7 @@ class MIoTLan:
except Exception as err: # pylint: disable=broad-exception-caught except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error("send2device error, %s", err) _LOGGER.error("send2device error, %s", err)
handler( handler(
{ {"code": MIoTErrorCode.CODE_INTERNAL_ERROR.value, "error": str(err)},
"code": MIoTErrorCode.CODE_INTERNAL_ERROR.value,
"error": str(err)
},
handler_ctx, handler_ctx,
) )
@ -1120,10 +1095,9 @@ class MIoTLan:
def __get_dev_list(self, data: _MIoTLanGetDevListData) -> None: def __get_dev_list(self, data: _MIoTLanGetDevListData) -> None:
dev_list = { dev_list = {
device.did: { device.did: {"online": device.online, "push_available": device.subscribed}
"online": device.online, for device in self._lan_devices.values()
"push_available": device.subscribed if device.online
} for device in self._lan_devices.values() if device.online
} }
data.handler(dev_list, data.handler_ctx) data.handler(dev_list, data.handler_ctx)
@ -1136,8 +1110,9 @@ class MIoTLan:
if "model" not in info or info["model"] in self._profile_models: if "model" not in info or info["model"] in self._profile_models:
# Do not support the local control of # Do not support the local control of
# Profile device for the time being # Profile device for the time being
_LOGGER.info("model not support local ctrl, %s, %s", did, _LOGGER.info(
info.get("model")) "model not support local ctrl, %s, %s", did, info.get("model")
)
continue continue
if did not in self._lan_devices: if did not in self._lan_devices:
if "token" not in info: if "token" not in info:
@ -1146,10 +1121,9 @@ class MIoTLan:
if len(info["token"]) != 32: if len(info["token"]) != 32:
_LOGGER.error("invalid device token, %s, %s", did, info) _LOGGER.error("invalid device token, %s, %s", did, info)
continue continue
self._lan_devices[did] = _MIoTLanDevice(manager=self, self._lan_devices[did] = _MIoTLanDevice(
did=did, manager=self, did=did, token=info["token"], ip=info.get("ip", None)
token=info["token"], )
ip=info.get("ip", None))
else: else:
self._lan_devices[did].update_info(info) self._lan_devices[did].update_info(info)
@ -1220,17 +1194,15 @@ class MIoTLan:
return return
# Create socket # Create socket
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Set SO_BINDTODEVICE # Set SO_BINDTODEVICE
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, if_name.encode())
if_name.encode())
sock.bind(("", self._local_port or 0)) sock.bind(("", self._local_port or 0))
self._internal_loop.add_reader(sock.fileno(), self._internal_loop.add_reader(
self.__socket_read_handler, sock.fileno(), self.__socket_read_handler, (if_name, sock)
(if_name, sock)) )
self._broadcast_socks[if_name] = sock self._broadcast_socks[if_name] = sock
self._local_port = self._local_port or sock.getsockname()[1] self._local_port = self._local_port or sock.getsockname()[1]
_LOGGER.info("created socket, %s, %s", if_name, self._local_port) _LOGGER.info("created socket, %s, %s", if_name, self._local_port)
@ -1252,9 +1224,9 @@ class MIoTLan:
def __socket_read_handler(self, ctx: tuple[str, socket.socket]) -> None: def __socket_read_handler(self, ctx: tuple[str, socket.socket]) -> None:
try: try:
data_len, addr = ctx[1].recvfrom_into(self._read_buffer, data_len, addr = ctx[1].recvfrom_into(
self.OT_MSG_LEN, self._read_buffer, self.OT_MSG_LEN, socket.MSG_DONTWAIT
socket.MSG_DONTWAIT) )
if data_len < 0: if data_len < 0:
# Socket error # Socket error
_LOGGER.error("socket read error, %s, %s", ctx[0], data_len) _LOGGER.error("socket read error, %s, %s", ctx[0], data_len)
@ -1262,13 +1234,15 @@ class MIoTLan:
if addr[1] != self.OT_PORT: if addr[1] != self.OT_PORT:
# Not ot msg # Not ot msg
return return
self.__raw_message_handler(self._read_buffer[:data_len], data_len, self.__raw_message_handler(
addr[0], ctx[0]) self._read_buffer[:data_len], data_len, addr[0], ctx[0]
)
except Exception as err: # pylint: disable=broad-exception-caught except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error("socket read handler error, %s", err) _LOGGER.error("socket read handler error, %s", err)
def __raw_message_handler(self, data: bytearray, data_len: int, ip: str, def __raw_message_handler(
if_name: str) -> None: self, data: bytearray, data_len: int, ip: str, if_name: str
) -> None:
if data[:2] != self.OT_HEADER: if data[:2] != self.OT_HEADER:
return return
# Keep alive message # Keep alive message
@ -1282,14 +1256,22 @@ class MIoTLan:
if data_len == self.OT_PROBE_LEN or device.subscribed: if data_len == self.OT_PROBE_LEN or device.subscribed:
device.keep_alive(ip=ip, if_name=if_name) device.keep_alive(ip=ip, if_name=if_name)
# Manage device subscribe status # Manage device subscribe status
if (self._enable_subscribe and data_len == self.OT_PROBE_LEN and if (
data[16:20] == b"MSUB" and data[24:27] == b"PUB"): self._enable_subscribe
device.supported_wildcard_sub = (int( and data_len == self.OT_PROBE_LEN
data[28]) == self.OT_SUPPORT_WILDCARD_SUB) and data[16:20] == b"MSUB"
and data[24:27] == b"PUB"
):
device.supported_wildcard_sub = (
int(data[28]) == self.OT_SUPPORT_WILDCARD_SUB
)
sub_ts = struct.unpack(">I", data[20:24])[0] sub_ts = struct.unpack(">I", data[20:24])[0]
sub_type = int(data[27]) sub_type = int(data[27])
if (device.supported_wildcard_sub and sub_type in [0, 1, 4] and if (
sub_ts != device.sub_ts): device.supported_wildcard_sub
and sub_type in [0, 1, 4]
and sub_ts != device.sub_ts
):
device.subscribed = False device.subscribed = False
device.subscribe() device.subscribe()
if data_len > self.OT_PROBE_LEN: if data_len > self.OT_PROBE_LEN:
@ -1306,52 +1288,49 @@ class MIoTLan:
_LOGGER.warning("invalid message, no id, %s, %s", did, msg) _LOGGER.warning("invalid message, no id, %s, %s", did, msg)
return return
# Reply # Reply
req: Optional[_MIoTLanRequestData] = self._pending_requests.pop( req: Optional[_MIoTLanRequestData] = self._pending_requests.pop(msg["id"], None)
msg["id"], None)
if req: if req:
if req.timeout: if req.timeout:
req.timeout.cancel() req.timeout.cancel()
req.timeout = None req.timeout = None
if req.handler is not None: if req.handler is not None:
self._main_loop.call_soon_threadsafe(req.handler, msg, self._main_loop.call_soon_threadsafe(req.handler, msg, req.handler_ctx)
req.handler_ctx)
return return
# Handle up link message # Handle up link message
if "method" not in msg or "params" not in msg: if "method" not in msg or "params" not in msg:
_LOGGER.debug("invalid message, no method or params, %s, %s", did, _LOGGER.debug("invalid message, no method or params, %s, %s", did, msg)
msg)
return return
# Filter dup message # Filter dup message
if self.__filter_dup_message(did, msg["id"]): if self.__filter_dup_message(did, msg["id"]):
self.send2device(did=did, self.send2device(did=did, msg={"id": msg["id"], "result": {"code": 0}})
msg={
"id": msg["id"],
"result": {
"code": 0
}
})
return return
_LOGGER.debug("lan message, %s, %s", did, msg) _LOGGER.debug("lan message, %s, %s", did, msg)
if msg["method"] == "properties_changed": if msg["method"] == "properties_changed":
for param in msg["params"]: for param in msg["params"]:
if "siid" not in param and "piid" not in param: if "siid" not in param and "piid" not in param:
_LOGGER.debug("invalid message, no siid or piid, %s, %s", _LOGGER.debug("invalid message, no siid or piid, %s, %s", did, msg)
did, msg)
continue continue
key = f"{did}/p/{param['siid']}/{param['piid']}" key = f"{did}/p/{param['siid']}/{param['piid']}"
subs: list[_MIoTLanRegisterBroadcastData] = list( subs: list[_MIoTLanRegisterBroadcastData] = list(
self._device_msg_matcher.iter_match(key)) self._device_msg_matcher.iter_match(key)
)
for sub in subs: for sub in subs:
self._main_loop.call_soon_threadsafe( self._main_loop.call_soon_threadsafe(
sub.handler, param, sub.handler_ctx) sub.handler, param, sub.handler_ctx
elif (msg["method"] == "event_occured" and "siid" in msg["params"] and )
"eiid" in msg["params"]): elif (
msg["method"] == "event_occured"
and "siid" in msg["params"]
and "eiid" in msg["params"]
):
key = f"{did}/e/{msg['params']['siid']}/{msg['params']['eiid']}" key = f"{did}/e/{msg['params']['siid']}/{msg['params']['eiid']}"
subs: list[_MIoTLanRegisterBroadcastData] = list( subs: list[_MIoTLanRegisterBroadcastData] = list(
self._device_msg_matcher.iter_match(key)) self._device_msg_matcher.iter_match(key)
)
for sub in subs: for sub in subs:
self._main_loop.call_soon_threadsafe(sub.handler, msg["params"], self._main_loop.call_soon_threadsafe(
sub.handler_ctx) sub.handler, msg["params"], sub.handler_ctx
)
else: else:
_LOGGER.debug("invalid message, unknown method, %s, %s", did, msg) _LOGGER.debug("invalid message, unknown method, %s, %s", did, msg)
# Reply # Reply
@ -1362,12 +1341,13 @@ class MIoTLan:
if filter_id in self._reply_msg_buffer: if filter_id in self._reply_msg_buffer:
return True return True
self._reply_msg_buffer[filter_id] = self._internal_loop.call_later( self._reply_msg_buffer[filter_id] = self._internal_loop.call_later(
5, lambda filter_id: self._reply_msg_buffer.pop(filter_id, None), 5, lambda filter_id: self._reply_msg_buffer.pop(filter_id, None), filter_id
filter_id) )
return False return False
def __sendto(self, if_name: Optional[str], data: bytes, address: str, def __sendto(
port: int) -> None: self, if_name: Optional[str], data: bytes, address: str, port: int
) -> None:
if if_name is None: if if_name is None:
# Broadcast # Broadcast
for if_n, sock in self._broadcast_socks.items(): for if_n, sock in self._broadcast_socks.items():
@ -1394,12 +1374,14 @@ class MIoTLan:
pass pass
scan_time = self.__get_next_scan_time() scan_time = self.__get_next_scan_time()
self._scan_timer = self._internal_loop.call_later( self._scan_timer = self._internal_loop.call_later(
scan_time, self.__scan_devices) scan_time, self.__scan_devices
)
_LOGGER.debug("next scan time: %ss", scan_time) _LOGGER.debug("next scan time: %ss", scan_time)
def __get_next_scan_time(self) -> float: def __get_next_scan_time(self) -> float:
if not self._last_scan_interval: if not self._last_scan_interval:
self._last_scan_interval = self.OT_PROBE_INTERVAL_MIN self._last_scan_interval = self.OT_PROBE_INTERVAL_MIN
self._last_scan_interval = min(self._last_scan_interval * 2, self._last_scan_interval = min(
self.OT_PROBE_INTERVAL_MAX) self._last_scan_interval * 2, self.OT_PROBE_INTERVAL_MAX
)
return self._last_scan_interval return self._last_scan_interval

File diff suppressed because it is too large Load Diff

View File

@ -70,8 +70,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up a config entry.""" """Set up a config entry."""
device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][ device_list: list[MIoTDevice] = hass.data[DOMAIN]["devices"][config_entry.entry_id]
config_entry.entry_id]
new_entities = [] new_entities = []
for miot_device in device_list: for miot_device in device_list:
@ -86,13 +85,13 @@ async def async_setup_entry(
for miot_device in device_list: for miot_device in device_list:
if "device:light" in miot_device.spec_instance.urn: if "device:light" in miot_device.spec_instance.urn:
if miot_device.entity_list.get("light", []): if miot_device.entity_list.get("light", []):
device_id = list( device_id = list(miot_device.device_info.get("identifiers"))[0][1]
miot_device.device_info.get("identifiers"))[0][1]
light_entity_id = miot_device.gen_device_entity_id(DOMAIN) light_entity_id = miot_device.gen_device_entity_id(DOMAIN)
new_light_select_entities.append( new_light_select_entities.append(
LightCommandSendMode(hass=hass, LightCommandSendMode(
light_entity_id=light_entity_id, hass=hass, light_entity_id=light_entity_id, device_id=device_id
device_id=device_id)) )
)
if new_light_select_entities: if new_light_select_entities:
async_add_entities(new_light_select_entities) async_add_entities(new_light_select_entities)
@ -109,8 +108,7 @@ class Select(MIoTPropertyEntity, SelectEntity):
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
await self.set_property_async(value=self.get_vlist_value( await self.set_property_async(value=self.get_vlist_value(description=option))
description=option))
@property @property
def current_option(self) -> Optional[str]: def current_option(self) -> Optional[str]:
@ -123,20 +121,18 @@ class LightCommandSendMode(SelectEntity, RestoreEntity):
then send other color temperatures and brightness or send them all at the same time. then send other color temperatures and brightness or send them all at the same time.
The default is to send one by one.""" The default is to send one by one."""
def __init__(self, hass: HomeAssistant, light_entity_id: str, def __init__(self, hass: HomeAssistant, light_entity_id: str, device_id: str):
device_id: str):
super().__init__() super().__init__()
self.hass = hass self.hass = hass
self._device_id = device_id self._device_id = device_id
self._attr_name = "Command Send Mode" self._attr_name = "Command Send Mode"
self._attr_unique_id = f"{light_entity_id}_command_send_mode" self._attr_unique_id = f"{light_entity_id}_command_send_mode"
self._attr_options = [ self._attr_options = ["Send One by One", "Send Turn On First", "Send Together"]
"Send One by One", "Send Turn On First", "Send Together"
]
self._attr_device_info = {"identifiers": {(DOMAIN, device_id)}} self._attr_device_info = {"identifiers": {(DOMAIN, device_id)}}
self._attr_current_option = self._attr_options[0] # 默认选项 self._attr_current_option = self._attr_options[0] # 默认选项
self._attr_entity_category = (EntityCategory.CONFIG self._attr_entity_category = (
) # **重点:告诉 HA 这是配置类实体** EntityCategory.CONFIG
) # **重点:告诉 HA 这是配置类实体**
async def async_select_option(self, option: str): async def async_select_option(self, option: str):
"""处理用户选择的选项。""" """处理用户选择的选项。"""
@ -147,8 +143,9 @@ class LightCommandSendMode(SelectEntity, RestoreEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""在实体添加到 Home Assistant 时恢复上次的状态。""" """在实体添加到 Home Assistant 时恢复上次的状态。"""
await super().async_added_to_hass() await super().async_added_to_hass()
if (last_state := await self.async_get_last_state() if (
) and last_state.state in self._attr_options: last_state := await self.async_get_last_state()
) and last_state.state in self._attr_options:
self._attr_current_option = last_state.state self._attr_current_option = last_state.state
@property @property