Merge branch 'main' of https://github.com/zghnwsq/fork_ha_xiaomi_home into fix_vacuum_warn

This commit is contained in:
tedwang 2025-01-23 11:01:23 +08:00
commit 04c44f36b1
19 changed files with 702 additions and 389 deletions

View File

@ -98,6 +98,9 @@ footer: Optional. The footer is the place to reference GitHub issues and PRs tha
When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md). When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
When you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.
## How to Get Help ## How to Get Help
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub. If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.

View File

@ -258,13 +258,14 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
f'{self.entity_id}') f'{self.entity_id}')
return return
# set air-conditioner on # set air-conditioner on
elif self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async(prop=self._prop_on, value=True) await self.set_property_async(
prop=self._prop_on, value=True, write_ha_state=False)
# set mode # set mode
mode_value = self.get_map_key( mode_value = self.get_map_key(
map_=self._hvac_mode_map, value=hvac_mode) map_=self._hvac_mode_map, value=hvac_mode)
if ( if (
mode_value is None or not mode_value or
not await self.set_property_async( not await self.set_property_async(
prop=self._prop_mode, value=mode_value) prop=self._prop_mode, value=mode_value)
): ):
@ -295,39 +296,37 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
if swing_mode == SWING_BOTH: if swing_mode == SWING_BOTH:
if await self.set_property_async( await self.set_property_async(
prop=self._prop_horizontal_swing, value=True, update=False): prop=self._prop_horizontal_swing, value=True,
self.set_prop_value(self._prop_horizontal_swing, value=True) write_ha_state=False)
if await self.set_property_async( await self.set_property_async(
prop=self._prop_vertical_swing, value=True, update=False): prop=self._prop_vertical_swing, value=True)
self.set_prop_value(self._prop_vertical_swing, value=True)
elif swing_mode == SWING_HORIZONTAL: elif swing_mode == SWING_HORIZONTAL:
if await self.set_property_async( await self.set_property_async(
prop=self._prop_horizontal_swing, value=True, update=False): prop=self._prop_horizontal_swing, value=True)
self.set_prop_value(self._prop_horizontal_swing, value=True)
elif swing_mode == SWING_VERTICAL: elif swing_mode == SWING_VERTICAL:
if await self.set_property_async( await self.set_property_async(
prop=self._prop_vertical_swing, value=True, update=False): prop=self._prop_vertical_swing, value=True)
self.set_prop_value(self._prop_vertical_swing, value=True)
elif swing_mode == SWING_ON: elif swing_mode == SWING_ON:
if await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_on, value=True, update=False): prop=self._prop_fan_on, value=True)
self.set_prop_value(self._prop_fan_on, value=True)
elif swing_mode == SWING_OFF: elif swing_mode == SWING_OFF:
if self._prop_fan_on and await self.set_property_async( if self._prop_fan_on:
prop=self._prop_fan_on, value=False, update=False): await self.set_property_async(
self.set_prop_value(self._prop_fan_on, value=False) prop=self._prop_fan_on, value=False,
if self._prop_horizontal_swing and await self.set_property_async( write_ha_state=False)
if self._prop_horizontal_swing:
await self.set_property_async(
prop=self._prop_horizontal_swing, value=False, prop=self._prop_horizontal_swing, value=False,
update=False): write_ha_state=False)
self.set_prop_value(self._prop_horizontal_swing, value=False) if self._prop_vertical_swing:
if self._prop_vertical_swing and await self.set_property_async( await self.set_property_async(
prop=self._prop_vertical_swing, value=False, update=False): prop=self._prop_vertical_swing, value=False,
self.set_prop_value(self._prop_vertical_swing, value=False) write_ha_state=False)
self.async_write_ha_state()
else: else:
raise RuntimeError( raise RuntimeError(
f'unknown swing_mode, {swing_mode}, {self.entity_id}') f'unknown swing_mode, {swing_mode}, {self.entity_id}')
self.async_write_ha_state()
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode.""" """Set new target fan mode."""
@ -368,9 +367,9 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
"""Return the hvac mode. e.g., heat, cool mode.""" """Return the hvac mode. e.g., heat, cool mode."""
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
return HVACMode.OFF return HVACMode.OFF
return self.get_map_key( return self.get_map_value(
map_=self._hvac_mode_map, map_=self._hvac_mode_map,
value=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))
@property @property
def fan_mode(self) -> Optional[str]: def fan_mode(self) -> Optional[str]:
@ -388,12 +387,10 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
Requires ClimateEntityFeature.SWING_MODE. Requires ClimateEntityFeature.SWING_MODE.
""" """
horizontal: bool = ( horizontal = (
self.get_prop_value(prop=self._prop_horizontal_swing) self.get_prop_value(prop=self._prop_horizontal_swing))
if self._prop_horizontal_swing else None) vertical = (
vertical: bool = ( self.get_prop_value(prop=self._prop_vertical_swing))
self.get_prop_value(prop=self._prop_vertical_swing)
if self._prop_vertical_swing else None)
if horizontal and vertical: if horizontal and vertical:
return SWING_BOTH return SWING_BOTH
if horizontal: if horizontal:
@ -449,7 +446,11 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
self.set_prop_value(prop=self._prop_fan_level, self.set_prop_value(prop=self._prop_fan_level,
value=v_ac_state['S']) value=v_ac_state['S'])
# D: swing mode. 0: on, 1: off # D: swing mode. 0: on, 1: off
if 'D' in v_ac_state and len(self._attr_swing_modes) == 2: if (
'D' in v_ac_state
and self._attr_swing_modes
and len(self._attr_swing_modes) == 2
):
if ( if (
SWING_HORIZONTAL in self._attr_swing_modes SWING_HORIZONTAL in self._attr_swing_modes
and self._prop_horizontal_swing and self._prop_horizontal_swing
@ -464,10 +465,10 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
self.set_prop_value( self.set_prop_value(
prop=self._prop_vertical_swing, prop=self._prop_vertical_swing,
value=v_ac_state['D'] == 0) value=v_ac_state['D'] == 0)
if self._value_ac_state:
self._value_ac_state.update(v_ac_state) self._value_ac_state.update(v_ac_state)
_LOGGER.debug( _LOGGER.debug(
'ac_state update, %s', self._value_ac_state) 'ac_state update, %s', self._value_ac_state)
class Heater(MIoTServiceEntity, ClimateEntity): class Heater(MIoTServiceEntity, ClimateEntity):

View File

@ -200,7 +200,7 @@ class Cover(MIoTServiceEntity, CoverEntity):
if pos is None: if pos is None:
return None return None
pos = round(pos*self._prop_position_value_range/100) pos = round(pos*self._prop_position_value_range/100)
return await self.set_property_async( await self.set_property_async(
prop=self._prop_target_position, value=pos) prop=self._prop_target_position, value=pos)
@property @property

View File

@ -303,7 +303,7 @@ class Fan(MIoTServiceEntity, FanEntity):
fan_level = self.get_prop_value(prop=self._prop_fan_level) fan_level = self.get_prop_value(prop=self._prop_fan_level)
if fan_level is None: if fan_level is None:
return None return None
if self._speed_names: if self._speed_names and self._speed_name_map:
return ordered_list_item_to_percentage( return ordered_list_item_to_percentage(
self._speed_names, self._speed_name_map[fan_level]) self._speed_names, self._speed_name_map[fan_level])
else: else:

View File

@ -96,7 +96,7 @@ class Light(MIoTServiceEntity, LightEntity):
"""Light entities for Xiaomi Home.""" """Light entities for Xiaomi Home."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
_VALUE_RANGE_MODE_COUNT_MAX = 30 _VALUE_RANGE_MODE_COUNT_MAX = 30
_prop_on: MIoTSpecProperty _prop_on: Optional[MIoTSpecProperty]
_prop_brightness: Optional[MIoTSpecProperty] _prop_brightness: Optional[MIoTSpecProperty]
_prop_color_temp: Optional[MIoTSpecProperty] _prop_color_temp: Optional[MIoTSpecProperty]
_prop_color: Optional[MIoTSpecProperty] _prop_color: Optional[MIoTSpecProperty]
@ -250,23 +250,25 @@ class Light(MIoTServiceEntity, LightEntity):
Shall set attributes in kwargs if applicable. Shall set attributes in kwargs if applicable.
""" """
result: bool = False
# on # on
# Dirty logic for lumi.gateway.mgl03 indicator light # Dirty logic for lumi.gateway.mgl03 indicator light
value_on = True if self._prop_on.format_ == bool else 1 if self._prop_on:
result = await self.set_property_async( value_on = True if self._prop_on.format_ == bool else 1
prop=self._prop_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( brightness = brightness_to_value(
self._brightness_scale, kwargs[ATTR_BRIGHTNESS]) self._brightness_scale, kwargs[ATTR_BRIGHTNESS])
result = await self.set_property_async( await self.set_property_async(
prop=self._prop_brightness, value=brightness) 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:
result = await self.set_property_async( await self.set_property_async(
prop=self._prop_color_temp, prop=self._prop_color_temp,
value=kwargs[ATTR_COLOR_TEMP_KELVIN]) value=kwargs[ATTR_COLOR_TEMP_KELVIN],
write_ha_state=False)
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:
@ -274,19 +276,23 @@ 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
result = await self.set_property_async( await self.set_property_async(
prop=self._prop_color, value=rgb) prop=self._prop_color, value=rgb,
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:
result = await self.set_property_async( await self.set_property_async(
prop=self._prop_mode, prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT])) map_=self._mode_map, value=kwargs[ATTR_EFFECT]),
return result write_ha_state=False)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off.""" """Turn the light off."""
if not self._prop_on:
return
# Dirty logic for lumi.gateway.mgl03 indicator light # Dirty logic for lumi.gateway.mgl03 indicator light
value_on = False if self._prop_on.format_ == bool else 0 value_on = False if self._prop_on.format_ == bool else 0
return await self.set_property_async(prop=self._prop_on, value=value_on) await self.set_property_async(prop=self._prop_on, value=value_on)

View File

@ -150,7 +150,7 @@ class MIoTClient:
# Device list update timestamp # Device list update timestamp
_device_list_update_ts: int _device_list_update_ts: int
_sub_source_list: dict[str, str] _sub_source_list: dict[str, Optional[str]]
_sub_tree: MIoTMatcher _sub_tree: MIoTMatcher
_sub_device_state: dict[str, MipsDeviceState] _sub_device_state: dict[str, MipsDeviceState]
@ -620,7 +620,7 @@ class MIoTClient:
# Priority local control # Priority local control
if self._ctrl_mode == CtrlMode.AUTO: if self._ctrl_mode == CtrlMode.AUTO:
# Gateway control # Gateway control
device_gw: dict = self._device_list_gateway.get(did, None) device_gw = self._device_list_gateway.get(did, None)
if ( if (
device_gw and device_gw.get('online', False) device_gw and device_gw.get('online', False)
and device_gw.get('specv2_access', False) and device_gw.get('specv2_access', False)
@ -641,7 +641,7 @@ class MIoTClient:
raise MIoTClientError( raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Lan control # Lan control
device_lan: dict = self._device_list_lan.get(did, None) device_lan = self._device_list_lan.get(did, None)
if device_lan and device_lan.get('online', False): if device_lan and device_lan.get('online', False):
result = await self._miot_lan.set_prop_async( result = await self._miot_lan.set_prop_async(
did=did, siid=siid, piid=piid, value=value) did=did, siid=siid, piid=piid, value=value)
@ -657,7 +657,7 @@ class MIoTClient:
# Cloud control # Cloud control
device_cloud = self._device_list_cloud.get(did, None) device_cloud = self._device_list_cloud.get(did, None)
if device_cloud and device_cloud.get('online', False): if device_cloud and device_cloud.get('online', False):
result: list = await self._http.set_prop_async( result = await self._http.set_prop_async(
params=[ params=[
{'did': did, 'siid': siid, 'piid': piid, 'value': value} {'did': did, 'siid': siid, 'piid': piid, 'value': value}
]) ])
@ -746,7 +746,7 @@ class MIoTClient:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
device_gw: dict = self._device_list_gateway.get(did, None) device_gw = self._device_list_gateway.get(did, None)
# Priority local control # Priority local control
if self._ctrl_mode == CtrlMode.AUTO: if self._ctrl_mode == CtrlMode.AUTO:
if ( if (
@ -782,7 +782,7 @@ class MIoTClient:
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Cloud control # Cloud control
device_cloud = self._device_list_cloud.get(did, None) device_cloud = self._device_list_cloud.get(did, None)
if device_cloud.get('online', False): if device_cloud and device_cloud.get('online', False):
result: dict = await self._http.action_async( result: dict = await self._http.action_async(
did=did, siid=siid, aiid=aiid, in_list=in_list) did=did, siid=siid, aiid=aiid, in_list=in_list)
if result: if result:
@ -798,14 +798,15 @@ class MIoTClient:
dids=[did])) dids=[did]))
raise MIoTClientError( raise MIoTClientError(
self.__get_exec_error_with_rc(rc=rc)) self.__get_exec_error_with_rc(rc=rc))
# Show error message # TODO: Show error message
_LOGGER.error( _LOGGER.error(
'client action failed, %s.%d.%d', did, siid, aiid) 'client action failed, %s.%d.%d', did, siid, aiid)
return None return []
def sub_prop( def sub_prop(
self, did: str, handler: Callable[[dict, Any], None], self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, piid: int = None, handler_ctx: Any = None siid: Optional[int] = None, piid: Optional[int] = None,
handler_ctx: Any = None
) -> bool: ) -> bool:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
@ -818,7 +819,9 @@ class MIoTClient:
_LOGGER.debug('client sub prop, %s', topic) _LOGGER.debug('client sub prop, %s', topic)
return True return True
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool: def unsub_prop(
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
) -> bool:
topic = ( topic = (
f'{did}/p/' f'{did}/p/'
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
@ -829,7 +832,8 @@ class MIoTClient:
def sub_event( def sub_event(
self, did: str, handler: Callable[[dict, Any], None], self, did: str, handler: Callable[[dict, Any], None],
siid: int = None, eiid: int = None, handler_ctx: Any = None siid: Optional[int] = None, eiid: Optional[int] = None,
handler_ctx: Any = None
) -> bool: ) -> bool:
if did not in self._device_list_cache: if did not in self._device_list_cache:
raise MIoTClientError(f'did not exist, {did}') raise MIoTClientError(f'did not exist, {did}')
@ -841,7 +845,9 @@ class MIoTClient:
_LOGGER.debug('client sub event, %s', topic) _LOGGER.debug('client sub event, %s', topic)
return True return True
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool: def unsub_event(
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
) -> bool:
topic = ( topic = (
f'{did}/e/' f'{did}/e/'
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
@ -1081,7 +1087,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify() self.__request_show_devices_changed_notify()
@ -1091,8 +1097,8 @@ class MIoTClient:
self, group_id: str, state: bool self, group_id: str, state: bool
) -> None: ) -> None:
_LOGGER.info('local mips state changed, %s, %s', group_id, state) _LOGGER.info('local mips state changed, %s, %s', group_id, state)
mips: MipsLocalClient = self._mips_local.get(group_id, None) mips = self._mips_local.get(group_id, None)
if mips is None: if not mips:
_LOGGER.error( _LOGGER.error(
'local mips state changed, mips not exist, %s', group_id) 'local mips state changed, mips not exist, %s', group_id)
return return
@ -1124,7 +1130,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self.__request_show_devices_changed_notify() self.__request_show_devices_changed_notify()
@ -1171,7 +1177,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
self._device_list_lan = {} self._device_list_lan = {}
@ -1201,7 +1207,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
return return
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1257,7 +1263,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
return return
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1301,9 +1307,8 @@ class MIoTClient:
async def __load_cache_device_async(self) -> None: async def __load_cache_device_async(self) -> None:
"""Load device list from cache.""" """Load device list from cache."""
cache_list: Optional[dict[str, dict]] = await self._storage.load_async( cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
domain='miot_devices', domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
name=f'{self._uid}_{self._cloud_server}', type_=dict) # type: ignore
type_=dict)
if not cache_list: if not cache_list:
self.__show_client_error_notify( self.__show_client_error_notify(
message=self._i18n.translate( message=self._i18n.translate(
@ -1346,7 +1351,7 @@ class MIoTClient:
cloud_state_old: Optional[bool] = self._device_list_cloud.get( cloud_state_old: Optional[bool] = self._device_list_cloud.get(
did, {}).get('online', None) did, {}).get('online', None)
cloud_state_new: Optional[bool] = None cloud_state_new: Optional[bool] = None
device_new: dict = cloud_list.pop(did, None) device_new = cloud_list.pop(did, None)
if device_new: if device_new:
cloud_state_new = device_new.get('online', None) cloud_state_new = device_new.get('online', None)
# Update cache device info # Update cache device info
@ -1371,7 +1376,7 @@ class MIoTClient:
continue continue
info['online'] = state_new info['online'] = state_new
# Call device state changed callback # Call device state changed callback
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1426,8 +1431,7 @@ class MIoTClient:
self, dids: list[str] self, dids: list[str]
) -> None: ) -> None:
_LOGGER.debug('refresh cloud device with dids, %s', dids) _LOGGER.debug('refresh cloud device with dids, %s', dids)
cloud_list: dict[str, dict] = ( cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
await self._http.get_devices_with_dids_async(dids=dids))
if cloud_list is None: if cloud_list is None:
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids) _LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
return return
@ -1466,11 +1470,11 @@ class MIoTClient:
for did, info in self._device_list_cache.items(): for did, info in self._device_list_cache.items():
if did not in filter_dids: if did not in filter_dids:
continue continue
device_old: dict = self._device_list_gateway.get(did, None) device_old = self._device_list_gateway.get(did, None)
gw_state_old = device_old.get( gw_state_old = device_old.get(
'online', False) if device_old else False 'online', False) if device_old else False
gw_state_new: bool = False gw_state_new: bool = False
device_new: dict = gw_list.pop(did, None) device_new = gw_list.pop(did, None)
if device_new: if device_new:
# Update gateway device info # Update gateway device info
self._device_list_gateway[did] = { self._device_list_gateway[did] = {
@ -1493,7 +1497,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
info['online'] = state_new info['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1518,7 +1522,7 @@ class MIoTClient:
if state_old == state_new: if state_old == state_new:
continue continue
self._device_list_cache[did]['online'] = state_new self._device_list_cache[did]['online'] = state_new
sub: MipsDeviceState = self._sub_device_state.get(did, None) sub = self._sub_device_state.get(did, None)
if sub and sub.handler: if sub and sub.handler:
sub.handler( sub.handler(
did, MIoTDeviceState.ONLINE if state_new did, MIoTDeviceState.ONLINE if state_new
@ -1533,7 +1537,7 @@ class MIoTClient:
'refresh gw devices with group_id, %s', group_id) 'refresh gw devices with group_id, %s', group_id)
# Remove timer # Remove timer
self._mips_local_state_changed_timers.pop(group_id, None) self._mips_local_state_changed_timers.pop(group_id, None)
mips: MipsLocalClient = self._mips_local.get(group_id, None) mips = self._mips_local.get(group_id, None)
if not mips: if not mips:
_LOGGER.error('mips not exist, %s', group_id) _LOGGER.error('mips not exist, %s', group_id)
return return
@ -1900,77 +1904,73 @@ async def get_miot_instance_async(
) -> MIoTClient: ) -> MIoTClient:
if entry_id is None: if entry_id is None:
raise MIoTClientError('invalid entry_id') raise MIoTClientError('invalid entry_id')
miot_client: MIoTClient = None miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None): if miot_client:
_LOGGER.info('instance exist, %s', entry_id) _LOGGER.info('instance exist, %s', entry_id)
miot_client = a return miot_client
else: # Create new instance
if entry_data is None: if not entry_data:
raise MIoTClientError('entry data is None') raise MIoTClientError('entry data is None')
# Get running loop # Get running loop
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
if loop is None: if not loop:
raise MIoTClientError('loop is None') raise MIoTClientError('loop is None')
# MIoT storage # MIoT storage
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get( storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
'miot_storage', None) 'miot_storage', None)
if not storage: if not storage:
storage = MIoTStorage( storage = MIoTStorage(
root_path=entry_data['storage_path'], loop=loop) root_path=entry_data['storage_path'], loop=loop)
hass.data[DOMAIN]['miot_storage'] = storage hass.data[DOMAIN]['miot_storage'] = storage
_LOGGER.info('create miot_storage instance') _LOGGER.info('create miot_storage instance')
global_config: dict = await storage.load_user_config_async( global_config: dict = await storage.load_user_config_async(
uid='global_config', cloud_server='all', uid='global_config', cloud_server='all',
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe']) keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
# MIoT network # MIoT network
network_detect_addr: dict = global_config.get( network_detect_addr: dict = global_config.get('network_detect_addr', {})
'network_detect_addr', {}) network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get( 'miot_network', None)
'miot_network', None) if not network:
if not network: network = MIoTNetwork(
network = MIoTNetwork( ip_addr_list=network_detect_addr.get('ip', []),
ip_addr_list=network_detect_addr.get('ip', []), url_addr_list=network_detect_addr.get('url', []),
url_addr_list=network_detect_addr.get('url', []), refresh_interval=NETWORK_REFRESH_INTERVAL,
refresh_interval=NETWORK_REFRESH_INTERVAL, loop=loop)
loop=loop) hass.data[DOMAIN]['miot_network'] = network
hass.data[DOMAIN]['miot_network'] = network await network.init_async()
await network.init_async() _LOGGER.info('create miot_network instance')
_LOGGER.info('create miot_network instance') # MIoT service
# MIoT service mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
mips_service: Optional[MipsService] = hass.data[DOMAIN].get( 'mips_service', None)
'mips_service', None) if not mips_service:
if not mips_service: aiozc = await zeroconf.async_get_async_instance(hass)
aiozc = await zeroconf.async_get_async_instance(hass) mips_service = MipsService(aiozc=aiozc, loop=loop)
mips_service = MipsService(aiozc=aiozc, loop=loop) hass.data[DOMAIN]['mips_service'] = mips_service
hass.data[DOMAIN]['mips_service'] = mips_service await mips_service.init_async()
await mips_service.init_async() _LOGGER.info('create mips_service instance')
_LOGGER.info('create mips_service instance') # MIoT lan
# MIoT lan miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get( if not miot_lan:
'miot_lan', None) miot_lan = MIoTLan(
if not miot_lan: net_ifs=global_config.get('net_interfaces', []),
miot_lan = MIoTLan(
net_ifs=global_config.get('net_interfaces', []),
network=network,
mips_service=mips_service,
enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop)
hass.data[DOMAIN]['miot_lan'] = miot_lan
_LOGGER.info('create miot_lan instance')
# MIoT client
miot_client = MIoTClient(
entry_id=entry_id,
entry_data=entry_data,
network=network, network=network,
storage=storage,
mips_service=mips_service, mips_service=mips_service,
miot_lan=miot_lan, enable_subscribe=global_config.get('enable_subscribe', False),
loop=loop loop=loop)
) hass.data[DOMAIN]['miot_lan'] = miot_lan
miot_client.persistent_notify = persistent_notify _LOGGER.info('create miot_lan instance')
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client) # MIoT client
_LOGGER.info( miot_client = MIoTClient(
'new miot_client instance, %s, %s', entry_id, entry_data) entry_id=entry_id,
await miot_client.init_async() entry_data=entry_data,
network=network,
storage=storage,
mips_service=mips_service,
miot_lan=miot_lan,
loop=loop
)
miot_client.persistent_notify = persistent_notify
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
_LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)
await miot_client.init_async()
return miot_client return miot_client

View File

@ -382,7 +382,7 @@ class MIoTHttpClient:
return res_obj['data'] return res_obj['data']
async def get_central_cert_async(self, csr: str) -> Optional[str]: async def get_central_cert_async(self, csr: str) -> str:
if not isinstance(csr, str): if not isinstance(csr, str):
raise MIoTHttpError('invalid params') raise MIoTHttpError('invalid params')

View File

@ -56,6 +56,7 @@ from homeassistant.const import (
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
@ -72,6 +73,7 @@ from homeassistant.const import (
UnitOfPower, UnitOfPower,
UnitOfVolume, UnitOfVolume,
UnitOfVolumeFlowRate, UnitOfVolumeFlowRate,
UnitOfDataRate
) )
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.components.switch import SwitchDeviceClass from homeassistant.components.switch import SwitchDeviceClass
@ -243,12 +245,12 @@ class MIoTDevice:
def sub_device_state( def sub_device_state(
self, key: str, handler: Callable[[str, MIoTDeviceState], None] self, key: str, handler: Callable[[str, MIoTDeviceState], None]
) -> int: ) -> int:
self._sub_id += 1 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(self._sub_id)] = handler self._device_state_sub_list[key][str(sub_id)] = handler
else: else:
self._device_state_sub_list[key] = {str(self._sub_id): handler} self._device_state_sub_list[key] = {str(sub_id): handler}
return self._sub_id return sub_id
def unsub_device_state(self, key: str, sub_id: int) -> None: def unsub_device_state(self, key: str, sub_id: int) -> None:
sub_list = self._device_state_sub_list.get(key, None) sub_list = self._device_state_sub_list.get(key, None)
@ -266,14 +268,14 @@ class MIoTDevice:
for handler in self._value_sub_list[key].values(): for handler in self._value_sub_list[key].values():
handler(params, ctx) handler(params, ctx)
self._sub_id += 1 sub_id = self.__gen_sub_id()
if key in self._value_sub_list: if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler self._value_sub_list[key][str(sub_id)] = handler
else: else:
self._value_sub_list[key] = {str(self._sub_id): handler} self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_prop( self.miot_client.sub_prop(
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid) did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
return self._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:
key: str = f'p.{siid}.{piid}' key: str = f'p.{siid}.{piid}'
@ -294,14 +296,14 @@ class MIoTDevice:
for handler in self._value_sub_list[key].values(): for handler in self._value_sub_list[key].values():
handler(params, ctx) handler(params, ctx)
self._sub_id += 1 sub_id = self.__gen_sub_id()
if key in self._value_sub_list: if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler self._value_sub_list[key][str(sub_id)] = handler
else: else:
self._value_sub_list[key] = {str(self._sub_id): handler} self._value_sub_list[key] = {str(sub_id): handler}
self.miot_client.sub_event( self.miot_client.sub_event(
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid) did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
return self._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:
key: str = f'e.{siid}.{eiid}' key: str = f'e.{siid}.{eiid}'
@ -414,10 +416,12 @@ class MIoTDevice:
spec_name: str = spec_instance.name spec_name: str = spec_instance.name
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str): if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name] spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:
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 { if not {
service.name for service in spec_instance.services service.name for service in spec_instance.services
}.issuperset(required_services): }.issuperset(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()
@ -427,9 +431,13 @@ class MIoTDevice:
for service in spec_instance.services: for service in spec_instance.services:
if service.platform: if service.platform:
continue continue
required_properties: dict
optional_properties: dict
required_actions: set
optional_actions: set
# 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: dict = SPEC_DEVICE_TRANS_MAP[spec_name][ required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
'required'].get( 'required'].get(
service.name, {} service.name, {}
).get('required', {}).get('properties', {}) ).get('required', {}).get('properties', {})
@ -446,7 +454,7 @@ class MIoTDevice:
service.name, {} service.name, {}
).get('optional', {}).get('actions', set({})) ).get('optional', {}).get('actions', set({}))
elif service.name in optional_services: elif service.name in optional_services:
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][ required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
'optional'].get( 'optional'].get(
service.name, {} service.name, {}
).get('required', {}).get('properties', {}) ).get('required', {}).get('properties', {})
@ -484,7 +492,7 @@ class MIoTDevice:
set(required_properties.keys()), 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)
prop.platform = platform prop.platform = platform
entity_data.props.add(prop) entity_data.props.add(prop)
# action # action
@ -499,85 +507,95 @@ class MIoTDevice:
return entity_data return entity_data
def parse_miot_service_entity( def parse_miot_service_entity(
self, service_instance: MIoTSpecService self, miot_service: MIoTSpecService
) -> Optional[MIoTEntityData]: ) -> Optional[MIoTEntityData]:
service = service_instance if (
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP): miot_service.platform
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
):
return None return None
service_name = miot_service.name
service_name = service.name
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str): if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
service_name = SPEC_SERVICE_TRANS_MAP[service_name] service_name = SPEC_SERVICE_TRANS_MAP[service_name]
# 1. The service shall have all required properties. if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:
return None
# 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'].get('properties', {})
if not { if not {
prop.name for prop in service.properties if prop.access prop.name for prop in miot_service.properties if prop.access
}.issuperset(set(required_properties.keys())): }.issuperset(set(required_properties.keys())):
return None return None
# 2. The required property shall have all required access mode. for prop in miot_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
# Required actions
# Required events
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity'] platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
entity_data = MIoTEntityData(platform=platform, spec=service_instance) entity_data = MIoTEntityData(platform=platform, spec=miot_service)
# Optional properties
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][ optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][
'optional'].get('properties', set({})) 'optional'].get('properties', set({}))
for prop in service.properties: for prop in miot_service.properties:
if prop.name in set.union( if prop.name in set.union(
set(required_properties.keys()), 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)
prop.platform = platform prop.platform = platform
entity_data.props.add(prop) entity_data.props.add(prop)
# action # Optional actions
# event # Optional events
# No actions or events is in SPEC_SERVICE_TRANS_MAP now. miot_service.platform = platform
service.platform = platform
return entity_data return entity_data
def parse_miot_property_entity( def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
self, property_instance: MIoTSpecProperty
) -> Optional[dict[str, str]]:
prop = property_instance
if ( if (
prop.platform miot_prop.platform
or (prop.name not in SPEC_PROP_TRANS_MAP['properties']) or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
): ):
return None return False
prop_name = miot_prop.name
prop_name = prop.name
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str): if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name] prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity'] platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
# Check
prop_access: set = set({}) prop_access: set = set({})
if prop.readable: if miot_prop.readable:
prop_access.add('read') prop_access.add('read')
if prop.writable: if miot_prop.writable:
prop_access.add('write') prop_access.add('write')
if prop_access != (SPEC_PROP_TRANS_MAP[ if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']): 'entities'][platform]['access']):
return None return False
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[ if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']: 'entities'][platform]['format']:
return None return False
if prop.unit: miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
prop.external_unit = self.unit_convert(prop.unit)
prop.icon = self.icon_convert(prop.unit)
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
'device_class'] 'device_class']
result = {'platform': platform, 'device_class': device_class} # Optional params
# optional: if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]: miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional'] prop_name]['state_class']
if 'state_class' in optional: if (
result['state_class'] = optional['state_class'] not miot_prop.external_unit
if not prop.unit and 'unit_of_measurement' in optional: and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
result['unit_of_measurement'] = optional['unit_of_measurement'] prop_name]
return result ):
# Priority: spec_modify.unit > unit_convert > specv2entity.unit
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
prop_name]['unit_of_measurement']
if (
not miot_prop.icon
and 'icon' in SPEC_PROP_TRANS_MAP['properties'][prop_name]
):
# Priority: spec_modify.icon > icon_convert > specv2entity.icon
miot_prop.icon = SPEC_PROP_TRANS_MAP['properties'][prop_name][
'icon']
miot_prop.platform = platform
return True
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."""
@ -589,7 +607,7 @@ class MIoTDevice:
# 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(
service_instance=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
@ -598,20 +616,11 @@ class MIoTDevice:
continue continue
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) if not prop.icon:
prop_entity = self.parse_miot_property_entity( prop.icon = self.icon_convert(prop.unit)
property_instance=prop) # Special conversion
if prop_entity: self.parse_miot_property_entity(miot_prop=prop)
prop.platform = prop_entity['platform'] # General conversion
prop.device_class = prop_entity['device_class']
if 'state_class' in prop_entity:
prop.state_class = prop_entity['state_class']
if 'unit_of_measurement' in prop_entity:
prop.external_unit = self.unit_convert(
prop_entity['unit_of_measurement'])
prop.icon = self.icon_convert(
prop_entity['unit_of_measurement'])
# general conversion
if not prop.platform: if not prop.platform:
if prop.writable: if prop.writable:
if prop.format_ == str: if prop.format_ == str:
@ -625,7 +634,7 @@ class MIoTDevice:
prop.platform = 'number' prop.platform = 'number'
else: else:
# Irregular property will not be transformed. # Irregular property will not be transformed.
pass continue
elif prop.readable or prop.notifiable: elif prop.readable or prop.notifiable:
if prop.format_ == bool: if prop.format_ == bool:
prop.platform = 'binary_sensor' prop.platform = 'binary_sensor'
@ -653,11 +662,66 @@ class MIoTDevice:
self.append_action(action=action) self.append_action(action=action)
def unit_convert(self, spec_unit: str) -> Optional[str]: def unit_convert(self, spec_unit: str) -> Optional[str]:
"""Convert MIoT unit to Home Assistant unit.""" """Convert MIoT unit to Home Assistant unit.
25/01/20: All online prop unit statistical tables: unit, quantity.
{
"no_unit": 148499,
"percentage": 10042,
"kelvin": 1895,
"rgb": 772, // color
"celsius": 5762,
"none": 16106,
"hours": 1540,
"minutes": 5061,
"ms": 27,
"watt": 216,
"arcdegrees": 159,
"ppm": 177,
"μg/m3": 106,
"days": 571,
"seconds": 2749,
"B/s": 21,
"pascal": 110,
"mg/m3": 339,
"lux": 125,
"kWh": 124,
"mv": 2,
"V": 38,
"A": 29,
"mV": 4,
"L": 352,
"m": 37,
"毫摩尔每升": 2, // blood-sugar, cholesterol
"mmol/L": 1, // urea
"weeks": 26,
"meter": 3,
"dB": 26,
"hour": 14,
"calorie": 19, // 1 cal = 4.184 J
"ppb": 3,
"arcdegress": 30,
"bpm": 4, // realtime-heartrate
"gram": 7,
"km/h": 9,
"W": 1,
"m3/h": 2,
"kilopascal": 1,
"mL": 4,
"mmHg": 4,
"w": 1,
"liter": 1,
"cm": 3,
"mA": 2,
"kilogram": 2,
"kcal/d": 2, // basal-metabolism
"times": 1 // exercise-count
}
"""
unit_map = { unit_map = {
'percentage': PERCENTAGE, 'percentage': PERCENTAGE,
'weeks': UnitOfTime.WEEKS, 'weeks': UnitOfTime.WEEKS,
'days': UnitOfTime.DAYS, 'days': UnitOfTime.DAYS,
'hour': UnitOfTime.HOURS,
'hours': UnitOfTime.HOURS, 'hours': UnitOfTime.HOURS,
'minutes': UnitOfTime.MINUTES, 'minutes': UnitOfTime.MINUTES,
'seconds': UnitOfTime.SECONDS, 'seconds': UnitOfTime.SECONDS,
@ -672,30 +736,48 @@ class MIoTDevice:
'ppb': CONCENTRATION_PARTS_PER_BILLION, 'ppb': CONCENTRATION_PARTS_PER_BILLION,
'lux': LIGHT_LUX, 'lux': LIGHT_LUX,
'pascal': UnitOfPressure.PA, 'pascal': UnitOfPressure.PA,
'kilopascal': UnitOfPressure.KPA,
'mmHg': UnitOfPressure.MMHG,
'bar': UnitOfPressure.BAR, 'bar': UnitOfPressure.BAR,
'watt': UnitOfPower.WATT,
'L': UnitOfVolume.LITERS, 'L': UnitOfVolume.LITERS,
'liter': UnitOfVolume.LITERS,
'mL': UnitOfVolume.MILLILITERS, 'mL': UnitOfVolume.MILLILITERS,
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR, 'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
'm/s': UnitOfSpeed.METERS_PER_SECOND, 'm/s': UnitOfSpeed.METERS_PER_SECOND,
'watt': UnitOfPower.WATT,
'w': UnitOfPower.WATT,
'W': UnitOfPower.WATT,
'kWh': UnitOfEnergy.KILO_WATT_HOUR, 'kWh': UnitOfEnergy.KILO_WATT_HOUR,
'A': UnitOfElectricCurrent.AMPERE, 'A': UnitOfElectricCurrent.AMPERE,
'mA': UnitOfElectricCurrent.MILLIAMPERE, 'mA': UnitOfElectricCurrent.MILLIAMPERE,
'V': UnitOfElectricPotential.VOLT, 'V': UnitOfElectricPotential.VOLT,
'mv': UnitOfElectricPotential.MILLIVOLT,
'mV': UnitOfElectricPotential.MILLIVOLT, 'mV': UnitOfElectricPotential.MILLIVOLT,
'cm': UnitOfLength.CENTIMETERS,
'm': UnitOfLength.METERS, 'm': UnitOfLength.METERS,
'meter': UnitOfLength.METERS,
'km': UnitOfLength.KILOMETERS, 'km': UnitOfLength.KILOMETERS,
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, 'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
'gram': UnitOfMass.GRAMS, 'gram': UnitOfMass.GRAMS,
'kilogram': UnitOfMass.KILOGRAMS,
'dB': SIGNAL_STRENGTH_DECIBELS, 'dB': SIGNAL_STRENGTH_DECIBELS,
'arcdegrees': DEGREE,
'arcdegress': DEGREE,
'kB': UnitOfInformation.KILOBYTES, 'kB': UnitOfInformation.KILOBYTES,
'MB': UnitOfInformation.MEGABYTES,
'GB': UnitOfInformation.GIGABYTES,
'TB': UnitOfInformation.TERABYTES,
'B/s': UnitOfDataRate.BYTES_PER_SECOND,
'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,
'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,
'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND
} }
# Handle UnitOfConductivity separately since # Handle UnitOfConductivity separately since
# it might not be available in all HA versions # it might not be available in all HA versions
try: try:
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
from homeassistant.const import UnitOfConductivity from homeassistant.const import UnitOfConductivity # type: ignore
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
unit_map['μS/cm'] = 'μS/cm' unit_map['μS/cm'] = 'μS/cm'
@ -703,59 +785,66 @@ class MIoTDevice:
return unit_map.get(spec_unit, None) return unit_map.get(spec_unit, None)
def icon_convert(self, spec_unit: str) -> Optional[str]: def icon_convert(self, spec_unit: str) -> Optional[str]:
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', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']: 'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
}:
return 'mdi:clock' return 'mdi:clock'
if spec_unit in ['celsius']: if spec_unit in {'celsius'}:
return 'mdi:temperature-celsius' return 'mdi:temperature-celsius'
if spec_unit in ['fahrenheit']: if spec_unit in {'fahrenheit'}:
return 'mdi:temperature-fahrenheit' return 'mdi:temperature-fahrenheit'
if spec_unit in ['kelvin']: if spec_unit in {'kelvin'}:
return 'mdi:temperature-kelvin' return 'mdi:temperature-kelvin'
if spec_unit in ['μg/m3', 'mg/m3', 'ppm', 'ppb']: if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:
return 'mdi:blur' return 'mdi:blur'
if spec_unit in ['lux']: if spec_unit in {'lux'}:
return 'mdi:brightness-6' return 'mdi:brightness-6'
if spec_unit in ['pascal', 'megapascal', 'bar']: if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
return 'mdi:gauge' return 'mdi:gauge'
if spec_unit in ['watt']: if spec_unit in {'watt', 'w', 'W'}:
return 'mdi:flash-triangle' return 'mdi:flash-triangle'
if spec_unit in ['L', 'mL']: if spec_unit in {'L', 'mL'}:
return 'mdi:gas-cylinder' return 'mdi:gas-cylinder'
if spec_unit in ['km/h', 'm/s']: if spec_unit in {'km/h', 'm/s'}:
return 'mdi:speedometer' return 'mdi:speedometer'
if spec_unit in ['kWh']: if spec_unit in {'kWh'}:
return 'mdi:transmission-tower' return 'mdi:transmission-tower'
if spec_unit in ['A', 'mA']: if spec_unit in {'A', 'mA'}:
return 'mdi:current-ac' return 'mdi:current-ac'
if spec_unit in ['V', 'mV']: if spec_unit in {'V', 'mv', 'mV'}:
return 'mdi:current-dc' return 'mdi:current-dc'
if spec_unit in ['m', 'km']: if spec_unit in {'cm', 'm', 'meter', 'km'}:
return 'mdi:ruler' return 'mdi:ruler'
if spec_unit in ['rgb']: if spec_unit in {'rgb'}:
return 'mdi:palette' return 'mdi:palette'
if spec_unit in ['m3/h', 'L/s']: if spec_unit in {'m3/h', 'L/s'}:
return 'mdi:pipe-leak' return 'mdi:pipe-leak'
if spec_unit in ['μS/cm']: if spec_unit in {'μS/cm'}:
return 'mdi:resistor-nodes' return 'mdi:resistor-nodes'
if spec_unit in ['gram']: if spec_unit in {'gram', 'kilogram'}:
return 'mdi:weight' return 'mdi:weight'
if spec_unit in ['dB']: if spec_unit in {'dB'}:
return 'mdi:signal-distance-variant' return 'mdi:signal-distance-variant'
if spec_unit in ['times']: if spec_unit in {'times'}:
return 'mdi:counter' return 'mdi:counter'
if spec_unit in ['mmol/L']: if spec_unit in {'mmol/L'}:
return 'mdi:dots-hexagon' return 'mdi:dots-hexagon'
if spec_unit in ['arcdegress']: if spec_unit in {'kB', 'MB', 'GB'}:
return 'mdi:angle-obtuse'
if spec_unit in ['kB']:
return 'mdi:network-pos' return 'mdi:network-pos'
if spec_unit in ['calorie', 'kCal']: if spec_unit in {'arcdegress', 'arcdegrees'}:
return 'mdi:angle-obtuse'
if spec_unit in {'B/s', 'KB/s', 'MB/s', 'GB/s'}:
return 'mdi:network'
if spec_unit in {'calorie', 'kCal'}:
return 'mdi:food' return 'mdi:food'
return None return None
def __gen_sub_id(self) -> int:
self._sub_id += 1
return self._sub_id
def __on_device_state_changed( def __on_device_state_changed(
self, did: str, state: MIoTDeviceState, ctx: Any self, did: str, state: MIoTDeviceState, ctx: Any
) -> None: ) -> None:
@ -903,14 +992,14 @@ class MIoTServiceEntity(Entity):
siid=event.service.iid, eiid=event.iid, sub_id=sub_id) siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
def get_map_value( def get_map_value(
self, map_: dict[int, Any], key: int self, map_: Optional[dict[int, Any]], key: int
) -> Any: ) -> 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( def get_map_key(
self, map_: dict[int, Any], value: Any self, map_: Optional[dict[int, Any]], value: Any
) -> Optional[int]: ) -> Optional[int]:
if map_ is None: if map_ is None:
return None return None
@ -919,7 +1008,7 @@ class MIoTServiceEntity(Entity):
return key return key
return None return None
def get_prop_value(self, prop: MIoTSpecProperty) -> Any: def get_prop_value(self, prop: Optional[MIoTSpecProperty]) -> Any:
if not prop: if not prop:
_LOGGER.error( _LOGGER.error(
'get_prop_value error, property is None, %s, %s', 'get_prop_value error, property is None, %s, %s',
@ -927,7 +1016,9 @@ 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: MIoTSpecProperty, value: Any) -> None: def set_prop_value(
self, prop: Optional[MIoTSpecProperty], 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',
@ -936,13 +1027,14 @@ class MIoTServiceEntity(Entity):
self._prop_value_map[prop] = value self._prop_value_map[prop] = value
async def set_property_async( async def set_property_async(
self, prop: MIoTSpecProperty, value: Any, update: bool = True self, prop: Optional[MIoTSpecProperty], value: Any,
update_value: bool = True, write_ha_state: bool = True
) -> bool: ) -> bool:
value = prop.value_format(value)
if not prop: if not prop:
raise RuntimeError( raise RuntimeError(
f'set property failed, property is None, ' f'set property failed, property is None, '
f'{self.entity_id}, {self.name}') f'{self.entity_id}, {self.name}')
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, '
@ -958,8 +1050,9 @@ 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: if update_value:
self._prop_value_map[prop] = value self._prop_value_map[prop] = value
if write_ha_state:
self.async_write_ha_state() self.async_write_ha_state()
return True return True
@ -1184,6 +1277,7 @@ class MIoTPropertyEntity(Entity):
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)
self._value = self.spec.value_format(params['value']) self._value = self.spec.value_format(params['value'])
self._value = self.spec.eval_expr(self._value)
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()

View File

@ -1414,7 +1414,7 @@ class MipsLocalClient(_MipsClient):
@final @final
@on_dev_list_changed.setter @on_dev_list_changed.setter
def on_dev_list_changed( def on_dev_list_changed(
self, func: Callable[[Any, list[str]], Coroutine] self, func: Optional[Callable[[Any, list[str]], Coroutine]]
) -> None: ) -> None:
"""run in main loop.""" """run in main loop."""
self._on_dev_list_changed = func self._on_dev_list_changed = func

View File

@ -94,7 +94,7 @@ class MIoTNetwork:
_main_loop: asyncio.AbstractEventLoop _main_loop: asyncio.AbstractEventLoop
_ip_addr_map: dict[str, float] _ip_addr_map: dict[str, float]
_url_addr_list: dict[str, float] _http_addr_map: dict[str, float]
_http_session: aiohttp.ClientSession _http_session: aiohttp.ClientSession
_refresh_interval: int _refresh_interval: int
@ -283,8 +283,8 @@ class MIoTNetwork:
[ [
'ping', '-c', '1', '-w', 'ping', '-c', '1', '-w',
str(self._DETECT_TIMEOUT), address]), str(self._DETECT_TIMEOUT), address]),
stdout=subprocess.PIPE, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE stderr=subprocess.DEVNULL
) )
await process.communicate() await process.communicate()
if process.returncode == 0: if process.returncode == 0:

View File

@ -475,14 +475,13 @@ class _MIoTSpecBase:
proprietary: bool proprietary: bool
need_filter: bool need_filter: bool
name: str name: str
icon: Optional[str]
# External params # External params
platform: Optional[str] platform: Optional[str]
device_class: Any device_class: Any
state_class: Any state_class: Any
icon: Optional[str]
external_unit: Any external_unit: Any
expression: Optional[str]
spec_id: int spec_id: int
@ -495,13 +494,12 @@ class _MIoTSpecBase:
self.proprietary = spec.get('proprietary', False) self.proprietary = spec.get('proprietary', False)
self.need_filter = spec.get('need_filter', False) self.need_filter = spec.get('need_filter', False)
self.name = spec.get('name', 'xiaomi') self.name = spec.get('name', 'xiaomi')
self.icon = spec.get('icon', None)
self.platform = None self.platform = None
self.device_class = None self.device_class = None
self.state_class = None self.state_class = None
self.icon = None
self.external_unit = None self.external_unit = None
self.expression = None
self.spec_id = hash(f'{self.type_}.{self.iid}') self.spec_id = hash(f'{self.type_}.{self.iid}')
@ -516,6 +514,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
"""MIoT SPEC property class.""" """MIoT SPEC property class."""
unit: Optional[str] unit: Optional[str]
precision: int precision: int
expr: Optional[str]
_format_: Type _format_: Type
_value_range: Optional[MIoTSpecValueRange] _value_range: Optional[MIoTSpecValueRange]
@ -537,7 +536,8 @@ class MIoTSpecProperty(_MIoTSpecBase):
unit: Optional[str] = None, unit: Optional[str] = None,
value_range: Optional[dict] = None, value_range: Optional[dict] = None,
value_list: Optional[list[dict]] = None, value_list: Optional[list[dict]] = None,
precision: Optional[int] = None precision: Optional[int] = None,
expr: Optional[str] = None
) -> None: ) -> None:
super().__init__(spec=spec) super().__init__(spec=spec)
self.service = service self.service = service
@ -547,6 +547,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
self.value_range = value_range self.value_range = value_range
self.value_list = value_list self.value_list = value_list
self.precision = precision or 1 self.precision = precision or 1
self.expr = expr
self.spec_id = hash( self.spec_id = hash(
f'p.{self.name}.{self.service.iid}.{self.iid}') f'p.{self.name}.{self.service.iid}.{self.iid}')
@ -619,6 +620,18 @@ class MIoTSpecProperty(_MIoTSpecBase):
elif isinstance(value, MIoTSpecValueList): elif isinstance(value, MIoTSpecValueList):
self._value_list = value self._value_list = value
def eval_expr(self, src_value: Any) -> Any:
if not self.expr:
return src_value
try:
# pylint: disable=eval-used
return eval(self.expr, {'src_value': src_value})
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error(
'eval expression error, %s, %s, %s, %s',
self.iid, src_value, self.expr, err)
return src_value
def value_format(self, value: Any) -> Any: def value_format(self, value: Any) -> Any:
if value is None: if value is None:
return None return None
@ -645,7 +658,9 @@ class MIoTSpecProperty(_MIoTSpecBase):
'value_range': ( 'value_range': (
self._value_range.dump() if self._value_range else None), self._value_range.dump() if self._value_range else None),
'value_list': self._value_list.dump() if self._value_list else None, 'value_list': self._value_list.dump() if self._value_list else None,
'precision': self.precision 'precision': self.precision,
'expr': self.expr,
'icon': self.icon
} }
@ -738,7 +753,6 @@ class MIoTSpecService(_MIoTSpecBase):
} }
# @dataclass
class MIoTSpecInstance: class MIoTSpecInstance:
"""MIoT SPEC instance class.""" """MIoT SPEC instance class."""
urn: str urn: str
@ -780,7 +794,8 @@ class MIoTSpecInstance:
unit=prop['unit'], unit=prop['unit'],
value_range=prop['value_range'], value_range=prop['value_range'],
value_list=prop['value_list'], value_list=prop['value_list'],
precision=prop.get('precision', None)) precision=prop.get('precision', None),
expr=prop.get('expr', None))
spec_service.properties.append(spec_prop) spec_service.properties.append(spec_prop)
for event in service['events']: for event in service['events']:
spec_event = MIoTSpecEvent( spec_event = MIoTSpecEvent(
@ -1125,6 +1140,79 @@ class _SpecFilter:
return False return False
class _SpecModify:
"""MIoT-Spec-V2 modify for entity conversion."""
_SPEC_MODIFY_FILE = 'specs/spec_modify.yaml'
_main_loop: asyncio.AbstractEventLoop
_data: Optional[dict]
_selected: Optional[dict]
def __init__(
self, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
self._data = None
async def init_async(self) -> None:
if isinstance(self._data, dict):
return
modify_data = None
self._data = {}
self._selected = None
try:
modify_data = await self._main_loop.run_in_executor(
None, load_yaml_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self._SPEC_MODIFY_FILE))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('spec modify, load file error, %s', err)
return
if not isinstance(modify_data, dict):
_LOGGER.error('spec modify, invalid spec modify content')
return
for key, value in modify_data.items():
if not isinstance(key, str) or not isinstance(value, (dict, str)):
_LOGGER.error('spec modify, invalid spec modify data')
return
self._data = modify_data
async def deinit_async(self) -> None:
self._data = None
self._selected = None
async def set_spec_async(self, urn: str) -> None:
if not self._data:
return
self._selected = self._data.get(urn, None)
if isinstance(self._selected, str):
return await self.set_spec_async(urn=self._selected)
def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='unit')
def get_prop_expr(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='expr')
def get_prop_icon(self, siid: int, piid: int) -> Optional[str]:
return self.__get_prop_item(siid=siid, piid=piid, key='icon')
def get_prop_access(self, siid: int, piid: int) -> Optional[list]:
access = self.__get_prop_item(siid=siid, piid=piid, key='access')
if not isinstance(access, list):
return None
return access
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
if not self._selected:
return None
prop = self._selected.get(f'prop.{siid}.{piid}', None)
if not prop:
return None
return prop.get(key, None)
class MIoTSpecParser: class MIoTSpecParser:
"""MIoT SPEC parser.""" """MIoT SPEC parser."""
# pylint: disable=inconsistent-quotes # pylint: disable=inconsistent-quotes
@ -1138,6 +1226,7 @@ class MIoTSpecParser:
_multi_lang: _MIoTSpecMultiLang _multi_lang: _MIoTSpecMultiLang
_bool_trans: _SpecBoolTranslation _bool_trans: _SpecBoolTranslation
_spec_filter: _SpecFilter _spec_filter: _SpecFilter
_spec_modify: _SpecModify
_init_done: bool _init_done: bool
@ -1155,6 +1244,7 @@ class MIoTSpecParser:
self._bool_trans = _SpecBoolTranslation( self._bool_trans = _SpecBoolTranslation(
lang=self._lang, loop=self._main_loop) lang=self._lang, loop=self._main_loop)
self._spec_filter = _SpecFilter(loop=self._main_loop) self._spec_filter = _SpecFilter(loop=self._main_loop)
self._spec_modify = _SpecModify(loop=self._main_loop)
self._init_done = False self._init_done = False
@ -1163,6 +1253,7 @@ class MIoTSpecParser:
return return
await self._bool_trans.init_async() await self._bool_trans.init_async()
await self._spec_filter.init_async() await self._spec_filter.init_async()
await self._spec_modify.init_async()
std_lib_cache = await self._storage.load_async( std_lib_cache = await self._storage.load_async(
domain=self._DOMAIN, name='spec_std_lib', type_=dict) domain=self._DOMAIN, name='spec_std_lib', type_=dict)
if ( if (
@ -1202,6 +1293,7 @@ class MIoTSpecParser:
# self._std_lib.deinit() # self._std_lib.deinit()
await self._bool_trans.deinit_async() await self._bool_trans.deinit_async()
await self._spec_filter.deinit_async() await self._spec_filter.deinit_async()
await self._spec_modify.deinit_async()
async def parse( async def parse(
self, urn: str, skip_cache: bool = False, self, urn: str, skip_cache: bool = False,
@ -1281,6 +1373,8 @@ class MIoTSpecParser:
await self._multi_lang.set_spec_async(urn=urn) await self._multi_lang.set_spec_async(urn=urn)
# Set spec filter # Set spec filter
await self._spec_filter.set_spec_spec(urn_key=urn_key) await self._spec_filter.set_spec_spec(urn_key=urn_key)
# Set spec modify
await self._spec_modify.set_spec_async(urn=urn)
# Parse device type # Parse device type
spec_instance: MIoTSpecInstance = MIoTSpecInstance( spec_instance: MIoTSpecInstance = MIoTSpecInstance(
urn=urn, name=urn_strs[3], urn=urn, name=urn_strs[3],
@ -1326,12 +1420,14 @@ class MIoTSpecParser:
): ):
continue continue
p_type_strs: list[str] = property_['type'].split(':') p_type_strs: list[str] = property_['type'].split(':')
# Handle special property.unit
unit = property_.get('unit', None)
spec_prop: MIoTSpecProperty = MIoTSpecProperty( spec_prop: MIoTSpecProperty = MIoTSpecProperty(
spec=property_, spec=property_,
service=spec_service, service=spec_service,
format_=property_['format'], format_=property_['format'],
access=property_['access'], access=property_['access'],
unit=property_.get('unit', None)) unit=unit if unit != 'none' else None)
spec_prop.name = p_type_strs[3] spec_prop.name = p_type_strs[3]
# Filter spec property # Filter spec property
spec_prop.need_filter = ( spec_prop.need_filter = (
@ -1371,7 +1467,19 @@ class MIoTSpecParser:
if v_descriptions: if v_descriptions:
# bool without value-list.name # bool without value-list.name
spec_prop.value_list = v_descriptions spec_prop.value_list = v_descriptions
# Prop modify
spec_prop.unit = self._spec_modify.get_prop_unit(
siid=service['iid'], piid=property_['iid']
) or spec_prop.unit
spec_prop.expr = self._spec_modify.get_prop_expr(
siid=service['iid'], piid=property_['iid'])
spec_prop.icon = self._spec_modify.get_prop_icon(
siid=service['iid'], piid=property_['iid'])
spec_service.properties.append(spec_prop) spec_service.properties.append(spec_prop)
custom_access = self._spec_modify.get_prop_access(
siid=service['iid'], piid=property_['iid'])
if custom_access:
spec_prop.access = custom_access
# Parse service event # Parse service event
for event in service.get('events', []): for event in service.get('events', []):
if ( if (

View File

@ -59,43 +59,6 @@ data:
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
translate: translate:
contact_state:
de:
'false': Kein Kontakt
'true': Kontakt
en:
'false': No Contact
'true': Contact
es:
'false': Sin contacto
'true': Contacto
fr:
'false': Pas de contact
'true': Contact
it:
'false': Nessun contatto
'true': Contatto
ja:
'false': 非接触
'true': 接触
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Нет контакта
'true': Контакт
zh-Hans:
'false': 分离
'true': 接触
zh-Hant:
'false': 分離
'true': 接觸
default: default:
de: de:
'false': Falsch 'false': Falsch
@ -133,6 +96,43 @@ translate:
zh-Hant: zh-Hant:
'false': 'false':
'true': 'true':
contact_state:
de:
'false': Kein Kontakt
'true': Kontakt
en:
'false': No Contact
'true': Contact
es:
'false': Sin contacto
'true': Contacto
fr:
'false': Pas de contact
'true': Contact
it:
'false': Nessun contatto
'true': Contatto
ja:
'false': 非接触
'true': 接触
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Нет контакта
'true': Контакт
zh-Hans:
'false': 分离
'true': 接触
zh-Hant:
'false': 分離
'true': 接觸
motion_state: motion_state:
de: de:
'false': Keine Bewegung erkannt 'false': Keine Bewegung erkannt

View File

@ -0,0 +1,44 @@
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
prop.2.1:
name: access-mode
access:
- read
- notify
prop.2.2:
name: ip-address
icon: mdi:ip
prop.2.3:
name: wifi-ssid
access:
- read
- notify
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1:
prop.5.1:
name: power-consumption
expr: round(src_value/1000, 3)
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
prop.2.2:
name: power-consumption
expr: round(src_value/1000, 3)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:
prop.11.1:
name: power-consumption
expr: round(src_value/100, 2)
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
prop.3.1:
name: electric-power
expr: round(src_value/100, 2)
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
prop.2.1:
name: download-speed
icon: mdi:download
unit: B/s
prop.2.2:
name: upload-speed
icon: mdi:upload
unit: B/s

View File

@ -50,10 +50,15 @@ from homeassistant.components.sensor import SensorStateClass
from homeassistant.components.event import EventDeviceClass from homeassistant.components.event import EventDeviceClass
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
LIGHT_LUX,
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
UnitOfElectricCurrent, UnitOfElectricCurrent,
UnitOfElectricPotential, UnitOfElectricPotential,
UnitOfTemperature,
UnitOfPressure,
PERCENTAGE
) )
# pylint: disable=pointless-string-statement # pylint: disable=pointless-string-statement
@ -96,7 +101,7 @@ from homeassistant.const import (
} }
} }
""" """
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = { SPEC_DEVICE_TRANS_MAP: dict = {
'humidifier': { 'humidifier': {
'required': { 'required': {
'humidifier': { 'humidifier': {
@ -263,7 +268,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
} }
} }
""" """
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { SPEC_SERVICE_TRANS_MAP: dict = {
'light': { 'light': {
'required': { 'required': {
'properties': { 'properties': {
@ -334,15 +339,13 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
'<property instance name>':{ '<property instance name>':{
'device_class': str, 'device_class': str,
'entity': str, 'entity': str,
'optional':{ 'state_class'?: str,
'state_class': str, 'unit_of_measurement'?: str
'unit_of_measurement': str
}
} }
} }
} }
""" """
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { SPEC_PROP_TRANS_MAP: dict = {
'entities': { 'entities': {
'sensor': { 'sensor': {
'format': {'int', 'float'}, 'format': {'int', 'float'},
@ -356,99 +359,111 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
'properties': { 'properties': {
'temperature': { 'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE, 'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfTemperature.CELSIUS
}, },
'relative-humidity': { 'relative-humidity': {
'device_class': SensorDeviceClass.HUMIDITY, 'device_class': SensorDeviceClass.HUMIDITY,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
}, },
'air-quality-index': { 'air-quality-index': {
'device_class': SensorDeviceClass.AQI, 'device_class': SensorDeviceClass.AQI,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
}, },
'pm2.5-density': { 'pm2.5-density': {
'device_class': SensorDeviceClass.PM25, 'device_class': SensorDeviceClass.PM25,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'pm10-density': { 'pm10-density': {
'device_class': SensorDeviceClass.PM10, 'device_class': SensorDeviceClass.PM10,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'pm1': { 'pm1': {
'device_class': SensorDeviceClass.PM1, 'device_class': SensorDeviceClass.PM1,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
}, },
'atmospheric-pressure': { 'atmospheric-pressure': {
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE, 'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfPressure.PA
}, },
'tvoc-density': { 'tvoc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, 'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT
}, },
'voc-density': 'tvoc-density', 'voc-density': 'tvoc-density',
'battery-level': { 'battery-level': {
'device_class': SensorDeviceClass.BATTERY, 'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': PERCENTAGE
}, },
'voltage': { 'voltage': {
'device_class': SensorDeviceClass.VOLTAGE, 'device_class': SensorDeviceClass.VOLTAGE,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfElectricPotential.VOLT
'unit_of_measurement': UnitOfElectricPotential.VOLT },
} 'electric-current': {
'device_class': SensorDeviceClass.CURRENT,
'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
}, },
'illumination': { 'illumination': {
'device_class': SensorDeviceClass.ILLUMINANCE, 'device_class': SensorDeviceClass.ILLUMINANCE,
'entity': 'sensor' 'entity': 'sensor',
'state_class': SensorStateClass.MEASUREMENT,
'unit_of_measurement': LIGHT_LUX
}, },
'no-one-determine-time': { 'no-one-determine-time': {
'device_class': SensorDeviceClass.DURATION, 'device_class': SensorDeviceClass.DURATION,
'entity': 'sensor' 'entity': 'sensor'
}, },
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time',
'electric-power': { 'electric-power': {
'device_class': SensorDeviceClass.POWER, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfPower.WATT
}
}, },
'electric-current': { 'surge-power': {
'device_class': SensorDeviceClass.CURRENT, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
}
}, },
'power-consumption': { 'power-consumption': {
'device_class': SensorDeviceClass.ENERGY, 'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.TOTAL_INCREASING,
'state_class': SensorStateClass.TOTAL_INCREASING, 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
}
}, },
'power': { 'power': {
'device_class': SensorDeviceClass.POWER, 'device_class': SensorDeviceClass.POWER,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.MEASUREMENT,
'state_class': SensorStateClass.MEASUREMENT, 'unit_of_measurement': UnitOfPower.WATT
'unit_of_measurement': UnitOfPower.WATT
}
}, },
'total-battery': { 'total-battery': {
'device_class': SensorDeviceClass.ENERGY, 'device_class': SensorDeviceClass.ENERGY,
'entity': 'sensor', 'entity': 'sensor',
'optional': { 'state_class': SensorStateClass.TOTAL_INCREASING,
'state_class': SensorStateClass.TOTAL_INCREASING, 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR }
}
},
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time'
} }
} }

View File

@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
# Set device_class # Set device_class
if self._value_list: if self._value_list:
self._attr_device_class = SensorDeviceClass.ENUM self._attr_device_class = SensorDeviceClass.ENUM
self._attr_icon = 'mdi:message-text' self._attr_icon = 'mdi:format-text'
self._attr_native_unit_of_measurement = None self._attr_native_unit_of_measurement = None
self._attr_options = self._value_list.descriptions self._attr_options = self._value_list.descriptions
else: else:
@ -109,6 +109,9 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_device_class, None) # type: ignore self._attr_device_class, None) # type: ignore
self._attr_native_unit_of_measurement = list( self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None unit_sets)[0] if unit_sets else None
# Set suggested precision
if spec.format_ in {int, float}:
self._attr_suggested_display_precision = spec.precision
# Set state_class # Set state_class
if spec.state_class: if spec.state_class:
self._attr_state_class = spec.state_class self._attr_state_class = spec.state_class

View File

@ -100,7 +100,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
) -> None: ) -> None:
"""Initialize the Water heater.""" """Initialize the Water heater."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_temperature_unit = None self._attr_temperature_unit = None # type: ignore
self._attr_supported_features = WaterHeaterEntityFeature(0) self._attr_supported_features = WaterHeaterEntityFeature(0)
self._prop_on = None self._prop_on = None
self._prop_temp = None self._prop_temp = None
@ -112,20 +112,20 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
for prop in entity_data.props: for prop in entity_data.props:
# on # on
if prop.name == 'on': if prop.name == 'on':
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
self._prop_on = prop self._prop_on = prop
# temperature # temperature
if prop.name == 'temperature': if prop.name == 'temperature':
if prop.value_range: if not prop.value_range:
if (
self._attr_temperature_unit is None
and prop.external_unit
):
self._attr_temperature_unit = prop.external_unit
self._prop_temp = prop
else:
_LOGGER.error( _LOGGER.error(
'invalid temperature value_range format, %s', 'invalid temperature value_range format, %s',
self.entity_id) self.entity_id)
continue
if prop.external_unit:
self._attr_temperature_unit = prop.external_unit
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._prop_temp = prop
# target-temperature # target-temperature
if prop.name == 'target-temperature': if prop.name == 'target-temperature':
if not prop.value_range: if not prop.value_range:
@ -133,8 +133,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
'invalid target-temperature value_range format, %s', 'invalid target-temperature value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._attr_min_temp = prop.value_range.min_ self._attr_target_temperature_low = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_ self._attr_target_temperature_high = prop.value_range.max_
self._attr_precision = prop.value_range.step self._attr_precision = prop.value_range.step
if self._attr_temperature_unit is None and prop.external_unit: if self._attr_temperature_unit is None and prop.external_unit:
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
@ -166,6 +166,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature the water heater should heat water to.""" """Set the temperature the water heater should heat water to."""
if not self._prop_target_temp:
return
await self.set_property_async( await self.set_property_async(
prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE]) prop=self._prop_target_temp, value=kwargs[ATTR_TEMPERATURE])
@ -181,16 +183,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return return
if self.get_prop_value(prop=self._prop_on) is False: if self.get_prop_value(prop=self._prop_on) is False:
await self.set_property_async( await self.set_property_async(
prop=self._prop_on, value=True, update=False) prop=self._prop_on, value=True, write_ha_state=False)
await self.set_property_async( await self.set_property_async(
prop=self._prop_mode, prop=self._prop_mode,
value=self.get_map_key( value=self.get_map_key(
map_=self._mode_map, map_=self._mode_map, value=operation_mode))
value=operation_mode))
async def async_turn_away_mode_on(self) -> None:
"""Set the water heater to away mode."""
await self.hass.async_add_executor_job(self.turn_away_mode_on)
@property @property
def current_temperature(self) -> Optional[float]: def current_temperature(self) -> Optional[float]:
@ -200,6 +197,8 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
@property @property
def target_temperature(self) -> Optional[float]: def target_temperature(self) -> Optional[float]:
"""Return the target temperature.""" """Return the target temperature."""
if not self._prop_target_temp:
return None
return self.get_prop_value(prop=self._prop_target_temp) return self.get_prop_value(prop=self._prop_target_temp)
@property @property

View File

@ -98,6 +98,8 @@ footer :(可选)关联的 issue 或 pull request 编号。
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。 在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
当您第一次提交拉取请求时GitHub Action 会提示您签署贡献者许可协议Contributor License AgreementCLA。只有签署了 CLA ,本项目才会合入您的拉取请求。
## 获取帮助 ## 获取帮助
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。 如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。

View File

@ -20,6 +20,9 @@ SPEC_BOOL_TRANS_FILE = path.join(
SPEC_FILTER_FILE = path.join( SPEC_FILTER_FILE = path.join(
ROOT_PATH, ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml') '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
SPEC_MODIFY_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
def load_json_file(file_path: str) -> Optional[dict]: def load_json_file(file_path: str) -> Optional[dict]:
@ -54,7 +57,8 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
def save_yaml_file(file_path: str, data: dict) -> None: def save_yaml_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file: with open(file_path, 'w', encoding='utf-8') as file:
yaml.safe_dump( yaml.safe_dump(
data, file, default_flow_style=False, allow_unicode=True, indent=2) data, file, default_flow_style=False,
allow_unicode=True, indent=2, sort_keys=False)
def dict_str_str(d: dict) -> bool: def dict_str_str(d: dict) -> bool:
@ -135,6 +139,21 @@ def bool_trans(d: dict) -> bool:
return True return True
def spec_modify(data: dict) -> bool:
"""dict[str, str | dict[str, dict]]"""
if not isinstance(data, dict):
return False
for urn, content in data.items():
if not isinstance(urn, str) or not isinstance(content, (dict, str)):
return False
if isinstance(content, str):
continue
for key, value in content.items():
if not isinstance(key, str) or not isinstance(value, dict):
return False
return True
def compare_dict_structure(dict1: dict, dict2: dict) -> bool: def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
if not isinstance(dict1, dict) or not isinstance(dict2, dict): if not isinstance(dict1, dict) or not isinstance(dict2, dict):
_LOGGER.info('invalid type') _LOGGER.info('invalid type')
@ -181,6 +200,12 @@ def sort_spec_filter(file_path: str):
return filter_data return filter_data
def sort_spec_modify(file_path: str):
filter_data = load_yaml_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
return dict(sorted(filter_data.items()))
@pytest.mark.github @pytest.mark.github
def test_bool_trans(): def test_bool_trans():
data = load_yaml_file(SPEC_BOOL_TRANS_FILE) data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
@ -197,6 +222,14 @@ def test_spec_filter():
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error' assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_spec_modify():
data = load_yaml_file(SPEC_MODIFY_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_MODIFY_FILE} failed'
assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'
@pytest.mark.github @pytest.mark.github
def test_miot_i18n(): def test_miot_i18n():
for file_name in listdir(MIOT_I18N_RELATIVE_PATH): for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
@ -286,3 +319,6 @@ def test_sort_spec_data():
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE) _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)

View File

@ -56,7 +56,7 @@ async def test_lan_async(test_devices: dict):
# Your central hub gateway did # Your central hub gateway did
test_did = '111111' test_did = '111111'
# Your central hub gateway did # Your central hub gateway token
test_token = '11223344556677d9a03d43936fc384205' test_token = '11223344556677d9a03d43936fc384205'
test_model = 'xiaomi.gateway.hub1' test_model = 'xiaomi.gateway.hub1'
# Your computer interface list, such as enp3s0, wlp5s0 # Your computer interface list, such as enp3s0, wlp5s0
@ -152,3 +152,5 @@ async def test_lan_async(test_devices: dict):
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
await miot_lan.deinit_async() await miot_lan.deinit_async()
await mips_service.deinit_async()
await miot_network.deinit_async()