Compare commits

...

15 Commits

Author SHA1 Message Date
Paul Shawn
82fc66f659
Merge 018b213a2c into 3b89536bda 2025-01-13 07:52:30 +00:00
topsworld
018b213a2c Merge branch 'main' into refactor-miot-spec-device 2025-01-13 15:52:21 +08:00
Paul Shawn
3b89536bda
fix: fix miot cloud and mdns error (#637)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
* fix: fix miot cloud state error

* style: code format
2025-01-13 11:23:53 +08:00
Paul Shawn
045528fbf2
style: using logging for test case log print (#636)
* style: using logging for test case log print

* fix: fix miot cloud test case resource error
2025-01-13 10:54:18 +08:00
topsworld
8d5feba1e0 Merge branch 'main' into refactor-miot-spec-device 2025-01-10 21:51:43 +08:00
topsworld
60e57e863d fix: fix fan entity 2025-01-10 10:32:20 +08:00
topsworld
792d70f2ba merge: merge main to this branch 2025-01-10 10:14:01 +08:00
topsworld
fca97e03f0 fix: fix miot cloud log error 2025-01-09 15:04:45 +08:00
topsworld
93f04b1aee feat: update prop.format_ logic 2025-01-09 15:04:23 +08:00
topsworld
d25d3f6a93 feat: update value-list logic 2025-01-09 13:23:35 +08:00
topsworld
3399e3bb20 feat: update entity value-range 2025-01-08 19:29:39 +08:00
topsworld
078adfbd4c feat: update std_lib and multi_lang logic 2025-01-08 15:23:31 +08:00
topsworld
e4dfdf68ab feat: remove spec cache storage 2025-01-08 11:00:41 +08:00
topsworld
d9d8433405 fix: fix type error 2025-01-07 20:24:06 +08:00
topsworld
eddeafa3c3 fix: fix miot_device type error 2025-01-07 10:40:10 +08:00
27 changed files with 1275 additions and 1066 deletions

View File

@ -156,64 +156,56 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
_LOGGER.error( _LOGGER.error(
'unknown on property, %s', self.entity_id) 'unknown on property, %s', self.entity_id)
elif prop.name == 'mode': elif prop.name == 'mode':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'invalid mode value_list, %s', self.entity_id) 'invalid mode value_list, %s', self.entity_id)
continue continue
self._hvac_mode_map = {} self._hvac_mode_map = {}
for item in prop.value_list: for item in prop.value_list.items:
if item['name'].lower() in {'off', 'idle'}: if item.name in {'off', 'idle'}:
self._hvac_mode_map[item['value']] = HVACMode.OFF self._hvac_mode_map[item.value] = HVACMode.OFF
elif item['name'].lower() in {'auto'}: elif item.name in {'auto'}:
self._hvac_mode_map[item['value']] = HVACMode.AUTO self._hvac_mode_map[item.value] = HVACMode.AUTO
elif item['name'].lower() in {'cool'}: elif item.name in {'cool'}:
self._hvac_mode_map[item['value']] = HVACMode.COOL self._hvac_mode_map[item.value] = HVACMode.COOL
elif item['name'].lower() in {'heat'}: elif item.name in {'heat'}:
self._hvac_mode_map[item['value']] = HVACMode.HEAT self._hvac_mode_map[item.value] = HVACMode.HEAT
elif item['name'].lower() in {'dry'}: elif item.name in {'dry'}:
self._hvac_mode_map[item['value']] = HVACMode.DRY self._hvac_mode_map[item.value] = HVACMode.DRY
elif item['name'].lower() in {'fan'}: elif item.name in {'fan'}:
self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY
self._attr_hvac_modes = list(self._hvac_mode_map.values()) self._attr_hvac_modes = list(self._hvac_mode_map.values())
self._prop_mode = prop self._prop_mode = prop
elif prop.name == 'target-temperature': elif prop.name == 'target-temperature':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.error( _LOGGER.error(
'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_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range['max'] self._attr_max_temp = prop.value_range.max_
self._attr_target_temperature_step = prop.value_range['step'] self._attr_target_temperature_step = prop.value_range.step
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE) ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop self._prop_target_temp = prop
elif prop.name == 'target-humidity': elif prop.name == 'target-humidity':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.error( _LOGGER.error(
'invalid target-humidity value_range format, %s', 'invalid target-humidity value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._attr_min_humidity = prop.value_range['min'] self._attr_min_humidity = prop.value_range.min_
self._attr_max_humidity = prop.value_range['max'] self._attr_max_humidity = prop.value_range.max_
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_HUMIDITY) ClimateEntityFeature.TARGET_HUMIDITY)
self._prop_target_humi = prop self._prop_target_humi = prop
elif prop.name == 'fan-level': elif prop.name == 'fan-level':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id) 'invalid fan-level value_list, %s', self.entity_id)
continue continue
self._fan_mode_map = { self._fan_mode_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._attr_fan_modes = list(self._fan_mode_map.values()) self._attr_fan_modes = list(self._fan_mode_map.values())
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
self._prop_fan_level = prop self._prop_fan_level = prop
@ -269,8 +261,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
elif self.get_prop_value(prop=self._prop_on) is False: elif 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)
# set mode # set mode
mode_value = self.get_map_value( mode_value = self.get_map_key(
map_=self._hvac_mode_map, description=hvac_mode) map_=self._hvac_mode_map, value=hvac_mode)
if ( if (
mode_value is None or mode_value is None or
not await self.set_property_async( not await self.set_property_async(
@ -339,8 +331,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
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."""
mode_value = self.get_map_value( mode_value = self.get_map_key(
map_=self._fan_mode_map, description=fan_mode) map_=self._fan_mode_map, value=fan_mode)
if mode_value is None or not await self.set_property_async( if mode_value is None or not await self.set_property_async(
prop=self._prop_fan_level, value=mode_value): prop=self._prop_fan_level, value=mode_value):
raise RuntimeError( raise RuntimeError(
@ -376,9 +368,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_description( return self.get_map_key(
map_=self._hvac_mode_map, map_=self._hvac_mode_map,
key=self.get_prop_value(prop=self._prop_mode)) value=self.get_prop_value(prop=self._prop_mode))
@property @property
def fan_mode(self) -> Optional[str]: def fan_mode(self) -> Optional[str]:
@ -386,7 +378,7 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
Requires ClimateEntityFeature.FAN_MODE. Requires ClimateEntityFeature.FAN_MODE.
""" """
return self.get_map_description( return self.get_map_value(
map_=self._fan_mode_map, map_=self._fan_mode_map,
key=self.get_prop_value(prop=self._prop_fan_level)) key=self.get_prop_value(prop=self._prop_fan_level))
@ -446,8 +438,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
}.get(v_ac_state['M'], None) }.get(v_ac_state['M'], None)
if mode: if mode:
self.set_prop_value( self.set_prop_value(
prop=self._prop_mode, value=self.get_map_value( prop=self._prop_mode, value=self.get_map_key(
map_=self._hvac_mode_map, description=mode)) map_=self._hvac_mode_map, value=mode))
# T: target temperature # T: target temperature
if 'T' in v_ac_state and self._prop_target_temp: if 'T' in v_ac_state and self._prop_target_temp:
self.set_prop_value(prop=self._prop_target_temp, self.set_prop_value(prop=self._prop_target_temp,
@ -517,29 +509,24 @@ class Heater(MIoTServiceEntity, ClimateEntity):
ClimateEntityFeature.TURN_OFF) ClimateEntityFeature.TURN_OFF)
self._prop_on = prop self._prop_on = prop
elif prop.name == 'target-temperature': elif prop.name == 'target-temperature':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.error( _LOGGER.error(
'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_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range['max'] self._attr_max_temp = prop.value_range.max_
self._attr_target_temperature_step = prop.value_range['step'] self._attr_target_temperature_step = prop.value_range.step
self._attr_temperature_unit = prop.external_unit self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.TARGET_TEMPERATURE) ClimateEntityFeature.TARGET_TEMPERATURE)
self._prop_target_temp = prop self._prop_target_temp = prop
elif prop.name == 'heat-level': elif prop.name == 'heat-level':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'invalid heat-level value_list, %s', self.entity_id) 'invalid heat-level value_list, %s', self.entity_id)
continue continue
self._heat_level_map = { self._heat_level_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._attr_preset_modes = list(self._heat_level_map.values()) self._attr_preset_modes = list(self._heat_level_map.values())
self._attr_supported_features |= ( self._attr_supported_features |= (
ClimateEntityFeature.PRESET_MODE) ClimateEntityFeature.PRESET_MODE)
@ -582,8 +569,8 @@ class Heater(MIoTServiceEntity, ClimateEntity):
"""Set the preset mode.""" """Set the preset mode."""
await self.set_property_async( await self.set_property_async(
self._prop_heat_level, self._prop_heat_level,
value=self.get_map_value( value=self.get_map_key(
map_=self._heat_level_map, description=preset_mode)) map_=self._heat_level_map, value=preset_mode))
@property @property
def target_temperature(self) -> Optional[float]: def target_temperature(self) -> Optional[float]:
@ -613,7 +600,7 @@ class Heater(MIoTServiceEntity, ClimateEntity):
@property @property
def preset_mode(self) -> Optional[str]: def preset_mode(self) -> Optional[str]:
return ( return (
self.get_map_description( self.get_map_value(
map_=self._heat_level_map, map_=self._heat_level_map,
key=self.get_prop_value(prop=self._prop_heat_level)) key=self.get_prop_value(prop=self._prop_heat_level))
if self._prop_heat_level else None) if self._prop_heat_level else None)

View File

@ -132,53 +132,47 @@ class Cover(MIoTServiceEntity, CoverEntity):
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'motor-control': if prop.name == 'motor-control':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'motor-control value_list is None, %s', self.entity_id) 'motor-control value_list is None, %s', self.entity_id)
continue continue
for item in prop.value_list: for item in prop.value_list.items:
if item['name'].lower() in ['open']: if item.name in {'open'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.OPEN) CoverEntityFeature.OPEN)
self._prop_motor_value_open = item['value'] self._prop_motor_value_open = item.value
elif item['name'].lower() in ['close']: elif item.name in {'close'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.CLOSE) CoverEntityFeature.CLOSE)
self._prop_motor_value_close = item['value'] self._prop_motor_value_close = item.value
elif item['name'].lower() in ['pause']: elif item.name in {'pause'}:
self._attr_supported_features |= ( self._attr_supported_features |= (
CoverEntityFeature.STOP) CoverEntityFeature.STOP)
self._prop_motor_value_pause = item['value'] self._prop_motor_value_pause = item.value
self._prop_motor_control = prop self._prop_motor_control = prop
elif prop.name == 'status': elif prop.name == 'status':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'status value_list is None, %s', self.entity_id) 'status value_list is None, %s', self.entity_id)
continue continue
for item in prop.value_list: for item in prop.value_list.items:
if item['name'].lower() in ['opening', 'open']: if item.name in {'opening', 'open'}:
self._prop_status_opening = item['value'] self._prop_status_opening = item.value
elif item['name'].lower() in ['closing', 'close']: elif item.name in {'closing', 'close'}:
self._prop_status_closing = item['value'] self._prop_status_closing = item.value
elif item['name'].lower() in ['stop', 'pause']: elif item.name in {'stop', 'pause'}:
self._prop_status_stop = item['value'] self._prop_status_stop = item.value
self._prop_status = prop self._prop_status = prop
elif prop.name == 'current-position': elif prop.name == 'current-position':
self._prop_current_position = prop self._prop_current_position = prop
elif prop.name == 'target-position': elif prop.name == 'target-position':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.error( _LOGGER.error(
'invalid target-position value_range format, %s', 'invalid target-position value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._prop_position_value_min = prop.value_range['min'] self._prop_position_value_min = prop.value_range.min_
self._prop_position_value_max = prop.value_range['max'] self._prop_position_value_max = prop.value_range.max_
self._prop_position_value_range = ( self._prop_position_value_range = (
self._prop_position_value_max - self._prop_position_value_max -
self._prop_position_value_min) self._prop_position_value_min)

View File

@ -87,7 +87,7 @@ async def async_setup_entry(
class Fan(MIoTServiceEntity, FanEntity): class Fan(MIoTServiceEntity, FanEntity):
"""Fan entities for Xiaomi Home.""" """Fan entities for Xiaomi Home."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
_prop_on: Optional[MIoTSpecProperty] _prop_on: MIoTSpecProperty
_prop_fan_level: Optional[MIoTSpecProperty] _prop_fan_level: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty]
_prop_horizontal_swing: Optional[MIoTSpecProperty] _prop_horizontal_swing: Optional[MIoTSpecProperty]
@ -100,7 +100,7 @@ class Fan(MIoTServiceEntity, FanEntity):
_speed_step: int _speed_step: int
_speed_names: Optional[list] _speed_names: Optional[list]
_speed_name_map: Optional[dict[int, str]] _speed_name_map: Optional[dict[int, str]]
_mode_list: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -111,7 +111,7 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_current_direction = None self._attr_current_direction = None
self._attr_supported_features = FanEntityFeature(0) self._attr_supported_features = FanEntityFeature(0)
self._prop_on = None # _prop_on is required
self._prop_fan_level = None self._prop_fan_level = None
self._prop_mode = None self._prop_mode = None
self._prop_horizontal_swing = None self._prop_horizontal_swing = None
@ -124,7 +124,7 @@ class Fan(MIoTServiceEntity, FanEntity):
self._speed_names = [] self._speed_names = []
self._speed_name_map = {} self._speed_name_map = {}
self._mode_list = None self._mode_map = None
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -133,42 +133,34 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.TURN_OFF self._attr_supported_features |= FanEntityFeature.TURN_OFF
self._prop_on = prop self._prop_on = prop
elif prop.name == 'fan-level': elif prop.name == 'fan-level':
if isinstance(prop.value_range, dict): if prop.value_range:
# Fan level with value-range # Fan level with value-range
self._speed_min = prop.value_range['min'] self._speed_min = prop.value_range.min_
self._speed_max = prop.value_range['max'] self._speed_max = prop.value_range.max_
self._speed_step = prop.value_range['step'] self._speed_step = prop.value_range.step
self._attr_speed_count = int(( self._attr_speed_count = int((
self._speed_max - self._speed_min)/self._speed_step)+1 self._speed_max - self._speed_min)/self._speed_step)+1
self._attr_supported_features |= FanEntityFeature.SET_SPEED self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._prop_fan_level = prop self._prop_fan_level = prop
elif ( elif (
self._prop_fan_level is None self._prop_fan_level is None
and isinstance(prop.value_list, list)
and prop.value_list and prop.value_list
): ):
# Fan level with value-list # Fan level with value-list
# Fan level with value-range is prior to fan level with # Fan level with value-range is prior to fan level with
# value-list when a fan has both fan level properties. # value-list when a fan has both fan level properties.
self._speed_name_map = { self._speed_name_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._speed_names = list(self._speed_name_map.values()) self._speed_names = list(self._speed_name_map.values())
self._attr_speed_count = len(prop.value_list) self._attr_speed_count = len(self._speed_names)
self._attr_supported_features |= FanEntityFeature.SET_SPEED self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._prop_fan_level = prop self._prop_fan_level = prop
elif prop.name == 'mode': elif prop.name == 'mode':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'mode value_list is None, %s', self.entity_id) 'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_list = { self._mode_map = prop.value_list.to_map()
item['value']: item['description'] self._attr_preset_modes = list(self._mode_map.values())
for item in prop.value_list}
self._attr_preset_modes = list(self._mode_list.values())
self._attr_supported_features |= FanEntityFeature.PRESET_MODE self._attr_supported_features |= FanEntityFeature.PRESET_MODE
self._prop_mode = prop self._prop_mode = prop
elif prop.name == 'horizontal-swing': elif prop.name == 'horizontal-swing':
@ -178,16 +170,11 @@ class Fan(MIoTServiceEntity, FanEntity):
if prop.format_ == 'bool': if prop.format_ == 'bool':
self._prop_wind_reverse_forward = False self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True self._prop_wind_reverse_reverse = True
elif ( elif prop.value_list:
isinstance(prop.value_list, list) for item in prop.value_list.items:
and prop.value_list if item.name in {'foreward'}:
): self._prop_wind_reverse_forward = item.value
for item in prop.value_list: self._prop_wind_reverse_reverse = item.value
if item['name'].lower() in {'foreward'}:
self._prop_wind_reverse_forward = item['value']
elif item['name'].lower() in {
'reversal', 'reverse'}:
self._prop_wind_reverse_reverse = item['value']
if ( if (
self._prop_wind_reverse_forward is None self._prop_wind_reverse_forward is None
or self._prop_wind_reverse_reverse is None or self._prop_wind_reverse_reverse is None
@ -199,21 +186,9 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.DIRECTION self._attr_supported_features |= FanEntityFeature.DIRECTION
self._prop_wind_reverse = prop self._prop_wind_reverse = prop
def __get_mode_description(self, key: int) -> Optional[str]:
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None
async def async_turn_on( async def async_turn_on(
self, percentage: int = None, preset_mode: str = None, **kwargs: Any self, percentage: Optional[int] = None,
preset_mode: Optional[str] = None, **kwargs: Any
) -> None: ) -> None:
"""Turn the fan on. """Turn the fan on.
@ -225,12 +200,12 @@ class Fan(MIoTServiceEntity, FanEntity):
# percentage # percentage
if percentage: if percentage:
if self._speed_names: if self._speed_names:
speed = percentage_to_ordered_list_item(
self._speed_names, percentage)
speed_value = self.get_map_value(
map_=self._speed_name_map, description=speed)
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, value=speed_value) prop=self._prop_fan_level,
value=self.get_map_value(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else: else:
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, prop=self._prop_fan_level,
@ -241,7 +216,8 @@ class Fan(MIoTServiceEntity, FanEntity):
if preset_mode: if preset_mode:
await self.set_property_async( await self.set_property_async(
self._prop_mode, self._prop_mode,
value=self.__get_mode_value(description=preset_mode)) value=self.get_map_key(
map_=self._mode_map, value=preset_mode))
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off.""" """Turn the fan off."""
@ -255,12 +231,12 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Set the percentage of the fan speed.""" """Set the percentage of the fan speed."""
if percentage > 0: if percentage > 0:
if self._speed_names: if self._speed_names:
speed = percentage_to_ordered_list_item(
self._speed_names, percentage)
speed_value = self.get_map_value(
map_=self._speed_name_map, description=speed)
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, value=speed_value) prop=self._prop_fan_level,
value=self.get_map_value(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else: else:
await self.set_property_async( await self.set_property_async(
prop=self._prop_fan_level, prop=self._prop_fan_level,
@ -277,7 +253,8 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Set the preset mode.""" """Set the preset mode."""
await self.set_property_async( await self.set_property_async(
self._prop_mode, self._prop_mode,
value=self.__get_mode_value(description=preset_mode)) value=self.get_map_key(
map_=self._mode_map, value=preset_mode))
async def async_set_direction(self, direction: str) -> None: async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
@ -306,7 +283,8 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Return the current preset mode, """Return the current preset mode,
e.g., auto, smart, eco, favorite.""" e.g., auto, smart, eco, favorite."""
return ( return (
self.__get_mode_description( self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))
if self._prop_mode else None) if self._prop_mode else None)

View File

@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
_prop_target_humidity: Optional[MIoTSpecProperty] _prop_target_humidity: Optional[MIoTSpecProperty]
_prop_humidity: Optional[MIoTSpecProperty] _prop_humidity: Optional[MIoTSpecProperty]
_mode_list: dict[Any, Any] _mode_map: dict[Any, Any]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -110,7 +110,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_mode = None self._prop_mode = None
self._prop_target_humidity = None self._prop_target_humidity = None
self._prop_humidity = None self._prop_humidity = None
self._mode_list = None self._mode_map = None
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -119,28 +119,23 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_on = prop self._prop_on = prop
# target-humidity # target-humidity
elif prop.name == 'target-humidity': elif prop.name == 'target-humidity':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.error( _LOGGER.error(
'invalid target-humidity value_range format, %s', 'invalid target-humidity value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._attr_min_humidity = prop.value_range['min'] self._attr_min_humidity = prop.value_range.min_
self._attr_max_humidity = prop.value_range['max'] self._attr_max_humidity = prop.value_range.max_
self._prop_target_humidity = prop self._prop_target_humidity = prop
# mode # mode
elif prop.name == 'mode': elif prop.name == 'mode':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'mode value_list is None, %s', self.entity_id) 'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_list = { self._mode_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._attr_available_modes = list( self._attr_available_modes = list(
self._mode_list.values()) self._mode_map.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop self._prop_mode = prop
# relative-humidity # relative-humidity
@ -163,7 +158,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
async def async_set_mode(self, mode: str) -> None: async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode.""" """Set new target preset mode."""
await self.set_property_async( await self.set_property_async(
prop=self._prop_mode, value=self.__get_mode_value(description=mode)) prop=self._prop_mode,
value=self.get_map_key(map_=self._mode_map, value=mode))
@property @property
def is_on(self) -> Optional[bool]: def is_on(self) -> Optional[bool]:
@ -183,20 +179,6 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
@property @property
def mode(self) -> Optional[str]: def mode(self) -> Optional[str]:
"""Return the current preset mode.""" """Return the current preset mode."""
return self.__get_mode_description( return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None

View File

@ -96,14 +96,14 @@ 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: Optional[MIoTSpecProperty] _prop_on: 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]
_prop_mode: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty]
_brightness_scale: Optional[tuple[int, int]] _brightness_scale: Optional[tuple[int, int]]
_mode_list: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -122,7 +122,7 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_color = None self._prop_color = None
self._prop_mode = None self._prop_mode = None
self._brightness_scale = None self._brightness_scale = None
self._mode_list = None self._mode_map = None
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -131,20 +131,17 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_on = prop self._prop_on = prop
# brightness # brightness
if prop.name == 'brightness': if prop.name == 'brightness':
if isinstance(prop.value_range, dict): if prop.value_range:
self._brightness_scale = ( self._brightness_scale = (
prop.value_range['min'], prop.value_range['max']) prop.value_range.min_, prop.value_range.max_)
self._prop_brightness = prop self._prop_brightness = prop
elif ( elif (
self._mode_list is None self._mode_map is None
and isinstance(prop.value_list, list)
and prop.value_list and prop.value_list
): ):
# For value-list brightness # For value-list brightness
self._mode_list = { self._mode_map = prop.value_list.to_map()
item['value']: item['description'] self._attr_effect_list = list(self._mode_map.values())
for item in prop.value_list}
self._attr_effect_list = list(self._mode_list.values())
self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop self._prop_mode = prop
else: else:
@ -153,13 +150,13 @@ class Light(MIoTServiceEntity, LightEntity):
continue continue
# color-temperature # color-temperature
if prop.name == 'color-temperature': if prop.name == 'color-temperature':
if not isinstance(prop.value_range, dict): if not prop.value_range:
_LOGGER.info( _LOGGER.info(
'invalid color-temperature value_range format, %s', 'invalid color-temperature value_range format, %s',
self.entity_id) self.entity_id)
continue continue
self._attr_min_color_temp_kelvin = prop.value_range['min'] self._attr_min_color_temp_kelvin = prop.value_range.min_
self._attr_max_color_temp_kelvin = prop.value_range['max'] self._attr_max_color_temp_kelvin = prop.value_range.max_
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
self._prop_color_temp = prop self._prop_color_temp = prop
@ -171,20 +168,15 @@ class Light(MIoTServiceEntity, LightEntity):
# mode # mode
if prop.name == 'mode': if prop.name == 'mode':
mode_list = None mode_list = None
if ( if prop.value_list:
isinstance(prop.value_list, list) mode_list = prop.value_list.to_map()
and prop.value_list elif prop.value_range:
):
mode_list = {
item['value']: item['description']
for item in prop.value_list}
elif isinstance(prop.value_range, dict):
mode_list = {} mode_list = {}
if ( if (
int(( int((
prop.value_range['max'] prop.value_range.max_
- prop.value_range['min'] - prop.value_range.min_
) / prop.value_range['step']) ) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX > self._VALUE_RANGE_MODE_COUNT_MAX
): ):
_LOGGER.info( _LOGGER.info(
@ -192,13 +184,13 @@ class Light(MIoTServiceEntity, LightEntity):
self.entity_id, prop.name, prop.value_range) self.entity_id, prop.name, prop.value_range)
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:
self._mode_list = mode_list self._mode_map = mode_list
self._attr_effect_list = list(self._mode_list.values()) self._attr_effect_list = list(self._mode_map.values())
self._attr_supported_features |= LightEntityFeature.EFFECT self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop self._prop_mode = prop
else: else:
@ -213,21 +205,6 @@ class Light(MIoTServiceEntity, LightEntity):
self._attr_supported_color_modes.add(ColorMode.ONOFF) self._attr_supported_color_modes.add(ColorMode.ONOFF)
self._attr_color_mode = ColorMode.ONOFF self._attr_color_mode = ColorMode.ONOFF
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None
@property @property
def is_on(self) -> Optional[bool]: def is_on(self) -> Optional[bool]:
"""Return if the light is on.""" """Return if the light is on."""
@ -264,7 +241,8 @@ 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_mode_description( return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) 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:
@ -275,7 +253,7 @@ class Light(MIoTServiceEntity, LightEntity):
result: bool = False 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 value_on = True if self._prop_on.format_ == bool else 1
result = await self.set_property_async( result = await self.set_property_async(
prop=self._prop_on, value=value_on) prop=self._prop_on, value=value_on)
# brightness # brightness
@ -303,11 +281,12 @@ class Light(MIoTServiceEntity, LightEntity):
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
result = await self.set_property_async( result = await self.set_property_async(
prop=self._prop_mode, prop=self._prop_mode,
value=self.__get_mode_value(description=kwargs[ATTR_EFFECT])) value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
return result return result
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off.""" """Turn the light off."""
# 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) return await self.set_property_async(prop=self._prop_on, value=value_on)

View File

@ -45,11 +45,14 @@ off Xiaomi or its affiliates' products.
Common utilities. Common utilities.
""" """
import asyncio
import json import json
from os import path from os import path
import random import random
from typing import Any, Optional from typing import Any, Optional
import hashlib import hashlib
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from paho.mqtt.matcher import MQTTMatcher from paho.mqtt.matcher import MQTTMatcher
import yaml import yaml
@ -83,10 +86,12 @@ def randomize_int(value: int, ratio: float) -> int:
"""Randomize an integer value.""" """Randomize an integer value."""
return int(value * (1 - ratio + random.random()*2*ratio)) return int(value * (1 - ratio + random.random()*2*ratio))
def randomize_float(value: float, ratio: float) -> float: def randomize_float(value: float, ratio: float) -> float:
"""Randomize a float value.""" """Randomize a float value."""
return value * (1 - ratio + random.random()*2*ratio) return value * (1 - ratio + random.random()*2*ratio)
class MIoTMatcher(MQTTMatcher): class MIoTMatcher(MQTTMatcher):
"""MIoT Pub/Sub topic matcher.""" """MIoT Pub/Sub topic matcher."""
@ -105,3 +110,68 @@ class MIoTMatcher(MQTTMatcher):
return self[topic] return self[topic]
except KeyError: except KeyError:
return None return None
class MIoTHttp:
"""MIoT Common HTTP API."""
@staticmethod
def get(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
full_url = url
if params:
encoded_params = urlencode(params)
full_url = f'{url}?{encoded_params}'
request = Request(full_url, method='GET', headers=headers or {})
content: Optional[bytes] = None
with urlopen(request) as response:
content = response.read()
return str(content, 'utf-8') if content else None
@staticmethod
def get_json(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.get(url, params, headers)
return json.loads(response) if response else None
@staticmethod
def post(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
pass
@staticmethod
def post_json(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.post(url, data, headers)
return json.loads(response) if response else None
@staticmethod
async def get_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
# TODO: Use aiohttp
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get, url, params, headers)
@staticmethod
async def get_json_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[dict]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get_json, url, params, headers)
@ staticmethod
async def post_async(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.post, url, data, headers)

View File

@ -106,7 +106,7 @@ class MIoTOauthClient:
@property @property
def state(self) -> str: def state(self) -> str:
return self.state return self._state
async def deinit_async(self) -> None: async def deinit_async(self) -> None:
if self._session and not self._session.closed: if self._session and not self._session.closed:
@ -744,7 +744,7 @@ class MIoTHttpClient:
prop_obj['fut'].set_result(None) prop_obj['fut'].set_result(None)
if props_req: if props_req:
_LOGGER.info( _LOGGER.info(
'get prop from cloud failed, %s, %s', len(key), props_req) 'get prop from cloud failed, %s', props_req)
if self._get_prop_list: if self._get_prop_list:
self._get_prop_timer = self._main_loop.call_later( self._get_prop_timer = self._main_loop.call_later(

View File

@ -94,7 +94,9 @@ from .miot_spec import (
MIoTSpecEvent, MIoTSpecEvent,
MIoTSpecInstance, MIoTSpecInstance,
MIoTSpecProperty, MIoTSpecProperty,
MIoTSpecService MIoTSpecService,
MIoTSpecValueList,
MIoTSpecValueRange
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -142,7 +144,7 @@ class MIoTDevice:
_room_id: str _room_id: str
_room_name: str _room_name: str
_suggested_area: str _suggested_area: Optional[str]
_device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]] _device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]]
@ -153,7 +155,7 @@ class MIoTDevice:
def __init__( def __init__(
self, miot_client: MIoTClient, self, miot_client: MIoTClient,
device_info: dict[str, str], device_info: dict[str, Any],
spec_instance: MIoTSpecInstance spec_instance: MIoTSpecInstance
) -> None: ) -> None:
self.miot_client = miot_client self.miot_client = miot_client
@ -243,25 +245,29 @@ class MIoTDevice:
return True return True
def sub_property( def sub_property(
self, handler: Callable[[dict, Any], None], siid: int = None, self, handler: Callable[[dict, Any], None], siid: Optional[int] = None,
piid: int = None, handler_ctx: Any = None piid: Optional[int] = None, handler_ctx: Any = None
) -> bool: ) -> bool:
return self.miot_client.sub_prop( return self.miot_client.sub_prop(
did=self._did, handler=handler, siid=siid, piid=piid, did=self._did, handler=handler, siid=siid, piid=piid,
handler_ctx=handler_ctx) handler_ctx=handler_ctx)
def unsub_property(self, siid: int = None, piid: int = None) -> bool: def unsub_property(
self, siid: Optional[int] = None, piid: Optional[int] = None
) -> bool:
return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid) return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
def sub_event( def sub_event(
self, handler: Callable[[dict, Any], None], siid: int = None, self, handler: Callable[[dict, Any], None], siid: Optional[int] = None,
eiid: int = None, handler_ctx: Any = None eiid: Optional[int] = None, handler_ctx: Any = None
) -> bool: ) -> bool:
return self.miot_client.sub_event( return self.miot_client.sub_event(
did=self._did, handler=handler, siid=siid, eiid=eiid, did=self._did, handler=handler, siid=siid, eiid=eiid,
handler_ctx=handler_ctx) handler_ctx=handler_ctx)
def unsub_event(self, siid: int = None, eiid: int = None) -> bool: def unsub_event(
self, siid: Optional[int] = None, eiid: Optional[int] = None
) -> bool:
return self.miot_client.unsub_event( return self.miot_client.unsub_event(
did=self._did, siid=siid, eiid=eiid) did=self._did, siid=siid, eiid=eiid)
@ -507,7 +513,7 @@ class MIoTDevice:
if prop_access != (SPEC_PROP_TRANS_MAP[ if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']): 'entities'][platform]['access']):
return None return None
if prop.format_ not in SPEC_PROP_TRANS_MAP[ if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']: 'entities'][platform]['format']:
return None return None
if prop.unit: if prop.unit:
@ -560,9 +566,9 @@ class MIoTDevice:
# general conversion # general conversion
if not prop.platform: if not prop.platform:
if prop.writable: if prop.writable:
if prop.format_ == 'str': if prop.format_ == str:
prop.platform = 'text' prop.platform = 'text'
elif prop.format_ == 'bool': elif prop.format_ == bool:
prop.platform = 'switch' prop.platform = 'switch'
prop.device_class = SwitchDeviceClass.SWITCH prop.device_class = SwitchDeviceClass.SWITCH
elif prop.value_list: elif prop.value_list:
@ -703,7 +709,7 @@ class MIoTDevice:
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:
self._online = state self._online = state == MIoTDeviceState.ONLINE
for key, handler in self._device_state_sub_list.items(): for key, handler in self._device_state_sub_list.items():
self.miot_client.main_loop.call_soon_threadsafe( self.miot_client.main_loop.call_soon_threadsafe(
handler, key, state) handler, key, state)
@ -719,7 +725,8 @@ class MIoTServiceEntity(Entity):
_main_loop: asyncio.AbstractEventLoop _main_loop: asyncio.AbstractEventLoop
_prop_value_map: dict[MIoTSpecProperty, Any] _prop_value_map: dict[MIoTSpecProperty, Any]
_event_occurred_handler: Callable[[MIoTSpecEvent, dict], None] _event_occurred_handler: Optional[
Callable[[MIoTSpecEvent, dict], None]]
_prop_changed_subs: dict[ _prop_changed_subs: dict[
MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]] MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]
@ -763,7 +770,9 @@ class MIoTServiceEntity(Entity):
self.entity_id) self.entity_id)
@property @property
def event_occurred_handler(self) -> Callable[[MIoTSpecEvent, dict], None]: def event_occurred_handler(
self
) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
return self._event_occurred_handler return self._event_occurred_handler
@event_occurred_handler.setter @event_occurred_handler.setter
@ -784,7 +793,7 @@ class MIoTServiceEntity(Entity):
self._prop_changed_subs.pop(prop, None) self._prop_changed_subs.pop(prop, None)
@property @property
def device_info(self) -> dict: def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info return self.miot_device.device_info
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -829,18 +838,20 @@ class MIoTServiceEntity(Entity):
self.miot_device.unsub_event( self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid) siid=event.service.iid, eiid=event.iid)
def get_map_description(self, map_: dict[int, Any], key: int) -> Any: def get_map_value(
self, map_: 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_value( def get_map_key(
self, map_: dict[int, Any], description: Any self, map_: dict[int, Any], value: Any
) -> Optional[int]: ) -> 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():
if value == description: if value_ == value:
return key return key
return None return None
@ -999,10 +1010,9 @@ class MIoTPropertyEntity(Entity):
service: MIoTSpecService service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop _main_loop: asyncio.AbstractEventLoop
# {'min':int, 'max':int, 'step': int} _value_range: Optional[MIoTSpecValueRange]
_value_range: dict[str, int]
# {Any: Any} # {Any: Any}
_value_list: dict[Any, Any] _value_list: Optional[MIoTSpecValueList]
_value: Any _value: Any
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle] _pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
@ -1015,11 +1025,7 @@ class MIoTPropertyEntity(Entity):
self.service = spec.service self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop self._main_loop = miot_device.miot_client.main_loop
self._value_range = spec.value_range self._value_range = spec.value_range
if spec.value_list: self._value_list = spec.value_list
self._value_list = {
item['value']: item['description'] for item in spec.value_list}
else:
self._value_list = None
self._value = None self._value = None
self._pending_write_ha_state_timer = None self._pending_write_ha_state_timer = None
# Gen entity_id # Gen entity_id
@ -1042,7 +1048,7 @@ class MIoTPropertyEntity(Entity):
self._value_list) self._value_list)
@property @property
def device_info(self) -> dict: def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info return self.miot_device.device_info
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -1067,18 +1073,15 @@ class MIoTPropertyEntity(Entity):
self.miot_device.unsub_property( self.miot_device.unsub_property(
siid=self.service.iid, piid=self.spec.iid) siid=self.service.iid, piid=self.spec.iid)
def get_vlist_description(self, value: Any) -> str: def get_vlist_description(self, value: Any) -> Optional[str]:
if not self._value_list: if not self._value_list:
return None return None
return self._value_list.get(value, None) return self._value_list.get_description_by_value(value)
def get_vlist_value(self, description: str) -> Any: def get_vlist_value(self, description: str) -> Any:
if not self._value_list: if not self._value_list:
return None return None
for key, value in self._value_list.items(): return self._value_list.get_value_by_description(description)
if value == description:
return key
return None
async def set_property_async(self, value: Any) -> bool: async def set_property_async(self, value: Any) -> bool:
if not self.spec.writable: if not self.spec.writable:
@ -1184,7 +1187,7 @@ class MIoTEventEntity(Entity):
spec.device_class, self.entity_id) spec.device_class, self.entity_id)
@property @property
def device_info(self) -> dict: def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info return self.miot_device.device_info
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -1286,7 +1289,7 @@ class MIoTActionEntity(Entity):
spec.device_class, self.entity_id) spec.device_class, self.entity_id)
@property @property
def device_info(self) -> dict: def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info return self.miot_device.device_info
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -1298,7 +1301,9 @@ class MIoTActionEntity(Entity):
self.miot_device.unsub_device_state( self.miot_device.unsub_device_state(
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}') key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}')
async def action_async(self, in_list: list = None) -> Optional[list]: async def action_async(
self, 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,

View File

@ -117,7 +117,7 @@ class MipsServiceData:
self.type = service_info.type self.type = service_info.type
self.server = service_info.server or '' self.server = service_info.server or ''
# Parse profile # Parse profile
self.did = str(int.from_bytes(self.profile_bin[1:9])) self.did = str(int.from_bytes(self.profile_bin[1:9], byteorder='big'))
self.group_id = binascii.hexlify( self.group_id = binascii.hexlify(
self.profile_bin[9:17][::-1]).decode('utf-8') self.profile_bin[9:17][::-1]).decode('utf-8')
self.role = int(self.profile_bin[20] >> 4) self.role = int(self.profile_bin[20] >> 4)

File diff suppressed because it is too large Load Diff

View File

@ -719,60 +719,6 @@ class MIoTCert:
return binascii.hexlify(sha1_hash.finalize()).decode('utf-8') return binascii.hexlify(sha1_hash.finalize()).decode('utf-8')
class SpecMultiLang:
"""
MIoT-Spec-V2 multi-language for entities.
"""
MULTI_LANG_FILE = 'specs/multi_lang.json'
_main_loop: asyncio.AbstractEventLoop
_lang: str
_data: Optional[dict[str, dict]]
def __init__(
self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_event_loop()
self._lang = lang
self._data = None
async def init_async(self) -> None:
if isinstance(self._data, dict):
return
multi_lang_data = None
self._data = {}
try:
multi_lang_data = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self.MULTI_LANG_FILE))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('multi lang, load file error, %s', err)
return
# Check if the file is a valid JSON file
if not isinstance(multi_lang_data, dict):
_LOGGER.error('multi lang, invalid file data')
return
for lang_data in multi_lang_data.values():
if not isinstance(lang_data, dict):
_LOGGER.error('multi lang, invalid lang data')
return
for data in lang_data.values():
if not isinstance(data, dict):
_LOGGER.error('multi lang, invalid lang data item')
return
self._data = multi_lang_data
async def deinit_async(self) -> str:
self._data = None
async def translate_async(self, urn_key: str) -> dict[str, str]:
"""MUST call init_async() first."""
if urn_key in self._data:
return self._data[urn_key].get(self._lang, {})
return {}
class SpecBoolTranslation: class SpecBoolTranslation:
""" """
Boolean value translation. Boolean value translation.

View File

@ -90,7 +90,7 @@ class Notify(MIoTActionEntity, NotifyEntity):
super().__init__(miot_device=miot_device, spec=spec) super().__init__(miot_device=miot_device, spec=spec)
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
action_in: str = ', '.join([ action_in: str = ', '.join([
f'{prop.description_trans}({prop.format_})' f'{prop.description_trans}({prop.format_.__name__})'
for prop in self.spec.in_]) for prop in self.spec.in_])
self._attr_extra_state_attributes['action params'] = f'[{action_in}]' self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
@ -122,24 +122,24 @@ class Notify(MIoTActionEntity, NotifyEntity):
return return
in_value: list[dict] = [] in_value: list[dict] = []
for index, prop in enumerate(self.spec.in_): for index, prop in enumerate(self.spec.in_):
if prop.format_ == 'str': if prop.format_ == str:
if isinstance(in_list[index], (bool, int, float, str)): if isinstance(in_list[index], (bool, int, float, str)):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': str(in_list[index])}) {'piid': prop.iid, 'value': str(in_list[index])})
continue continue
elif prop.format_ == 'bool': elif prop.format_ == bool:
if isinstance(in_list[index], (bool, int)): if isinstance(in_list[index], (bool, int)):
# yes, no, on, off, true, false and other bool types # yes, no, on, off, true, false and other bool types
# will also be parsed as 0 and 1 of int. # will also be parsed as 0 and 1 of int.
in_value.append( in_value.append(
{'piid': prop.iid, 'value': bool(in_list[index])}) {'piid': prop.iid, 'value': bool(in_list[index])})
continue continue
elif prop.format_ == 'float': elif prop.format_ == float:
if isinstance(in_list[index], (int, float)): if isinstance(in_list[index], (int, float)):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': in_list[index]}) {'piid': prop.iid, 'value': in_list[index]})
continue continue
elif prop.format_ == 'int': elif prop.format_ == int:
if isinstance(in_list[index], int): if isinstance(in_list[index], int):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': in_list[index]}) {'piid': prop.iid, 'value': in_list[index]})

View File

@ -92,9 +92,9 @@ class Number(MIoTPropertyEntity, NumberEntity):
self._attr_icon = self.spec.icon self._attr_icon = self.spec.icon
# Set value range # Set value range
if self._value_range: if self._value_range:
self._attr_native_min_value = self._value_range['min'] self._attr_native_min_value = self._value_range.min_
self._attr_native_max_value = self._value_range['max'] self._attr_native_max_value = self._value_range.max_
self._attr_native_step = self._value_range['step'] self._attr_native_step = self._value_range.step
@property @property
def native_value(self) -> Optional[float]: def native_value(self) -> Optional[float]:

View File

@ -82,7 +82,8 @@ class Select(MIoTPropertyEntity, SelectEntity):
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None: def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
"""Initialize the Select.""" """Initialize the Select."""
super().__init__(miot_device=miot_device, spec=spec) super().__init__(miot_device=miot_device, spec=spec)
self._attr_options = list(self._value_list.values()) if self._value_list:
self._attr_options = self._value_list.descriptions
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."""

View File

@ -91,7 +91,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
self._attr_device_class = SensorDeviceClass.ENUM self._attr_device_class = SensorDeviceClass.ENUM
self._attr_icon = 'mdi:message-text' self._attr_icon = 'mdi:message-text'
self._attr_native_unit_of_measurement = None self._attr_native_unit_of_measurement = None
self._attr_options = list(self._value_list.values()) self._attr_options = self._value_list.descriptions
else: else:
self._attr_device_class = spec.device_class self._attr_device_class = spec.device_class
if spec.external_unit: if spec.external_unit:
@ -115,14 +115,14 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
"""Return the current value of the sensor.""" """Return the current value of the sensor."""
if self._value_range and isinstance(self._value, (int, float)): if self._value_range and isinstance(self._value, (int, float)):
if ( if (
self._value < self._value_range['min'] self._value < self._value_range.min_
or self._value > self._value_range['max'] or self._value > self._value_range.max_
): ):
_LOGGER.info( _LOGGER.info(
'%s, data exception, out of range, %s, %s', '%s, data exception, out of range, %s, %s',
self.entity_id, self._value, self._value_range) self.entity_id, self._value, self._value_range)
if self._value_list: if self._value_list:
return self._value_list.get(self._value, None) return self.get_vlist_description(self._value)
if isinstance(self._value, str): if isinstance(self._value, str):
return self._value[:255] return self._value[:255]
return self._value return self._value

View File

@ -111,7 +111,7 @@ class ActionText(MIoTActionEntity, TextEntity):
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._attr_native_value = '' self._attr_native_value = ''
action_in: str = ', '.join([ action_in: str = ', '.join([
f'{prop.description_trans}({prop.format_})' f'{prop.description_trans}({prop.format_.__name__})'
for prop in self.spec.in_]) for prop in self.spec.in_])
self._attr_extra_state_attributes['action params'] = f'[{action_in}]' self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
# For action debug # For action debug
@ -141,24 +141,24 @@ class ActionText(MIoTActionEntity, TextEntity):
f'invalid action params, {value}') f'invalid action params, {value}')
in_value: list[dict] = [] in_value: list[dict] = []
for index, prop in enumerate(self.spec.in_): for index, prop in enumerate(self.spec.in_):
if prop.format_ == 'str': if prop.format_ == str:
if isinstance(in_list[index], (bool, int, float, str)): if isinstance(in_list[index], (bool, int, float, str)):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': str(in_list[index])}) {'piid': prop.iid, 'value': str(in_list[index])})
continue continue
elif prop.format_ == 'bool': elif prop.format_ == bool:
if isinstance(in_list[index], (bool, int)): if isinstance(in_list[index], (bool, int)):
# yes, no, on, off, true, false and other bool types # yes, no, on, off, true, false and other bool types
# will also be parsed as 0 and 1 of int. # will also be parsed as 0 and 1 of int.
in_value.append( in_value.append(
{'piid': prop.iid, 'value': bool(in_list[index])}) {'piid': prop.iid, 'value': bool(in_list[index])})
continue continue
elif prop.format_ == 'float': elif prop.format_ == float:
if isinstance(in_list[index], (int, float)): if isinstance(in_list[index], (int, float)):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': in_list[index]}) {'piid': prop.iid, 'value': in_list[index]})
continue continue
elif prop.format_ == 'int': elif prop.format_ == int:
if isinstance(in_list[index], int): if isinstance(in_list[index], int):
in_value.append( in_value.append(
{'piid': prop.iid, 'value': in_list[index]}) {'piid': prop.iid, 'value': in_list[index]})

View File

@ -120,28 +120,18 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
if prop.name == 'status': if prop.name == 'status':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'invalid status value_list, %s', self.entity_id) 'invalid status value_list, %s', self.entity_id)
continue continue
self._status_map = { self._status_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._prop_status = prop self._prop_status = prop
elif prop.name == 'fan-level': elif prop.name == 'fan-level':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id) 'invalid fan-level value_list, %s', self.entity_id)
continue continue
self._fan_level_map = { self._fan_level_map = prop.value_list.to_map()
item['value']: item['description']
for item in prop.value_list}
self._attr_fan_speed_list = list(self._fan_level_map.values()) self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop self._prop_fan_level = prop
@ -202,7 +192,7 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property @property
def state(self) -> Optional[str]: def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner.""" """Return the current state of the vacuum cleaner."""
return self.get_map_description( return self.get_map_value(
map_=self._status_map, map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status)) key=self.get_prop_value(prop=self._prop_status))
@ -214,6 +204,6 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property @property
def fan_speed(self) -> Optional[str]: def fan_speed(self) -> Optional[str]:
"""Return the current fan speed of the vacuum cleaner.""" """Return the current fan speed of the vacuum cleaner."""
return self.get_map_description( return self.get_map_value(
map_=self._fan_level_map, map_=self._fan_level_map,
key=self.get_prop_value(prop=self._prop_fan_level)) key=self.get_prop_value(prop=self._prop_fan_level))

View File

@ -93,7 +93,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
_prop_target_temp: Optional[MIoTSpecProperty] _prop_target_temp: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty] _prop_mode: Optional[MIoTSpecProperty]
_mode_list: Optional[dict[Any, Any]] _mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -106,7 +106,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_temp = None self._prop_temp = None
self._prop_target_temp = None self._prop_target_temp = None
self._prop_mode = None self._prop_mode = None
self._mode_list = None self._mode_map = None
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -115,7 +115,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_on = prop self._prop_on = prop
# temperature # temperature
if prop.name == 'temperature': if prop.name == 'temperature':
if isinstance(prop.value_range, dict): if prop.value_range:
if ( if (
self._attr_temperature_unit is None self._attr_temperature_unit is None
and prop.external_unit and prop.external_unit
@ -128,9 +128,14 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self.entity_id) self.entity_id)
# target-temperature # target-temperature
if prop.name == 'target-temperature': if prop.name == 'target-temperature':
self._attr_min_temp = prop.value_range['min'] if not prop.value_range:
self._attr_max_temp = prop.value_range['max'] _LOGGER.error(
self._attr_precision = prop.value_range['step'] 'invalid target-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
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
self._attr_supported_features |= ( self._attr_supported_features |= (
@ -138,17 +143,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_target_temp = prop self._prop_target_temp = prop
# mode # mode
if prop.name == 'mode': if prop.name == 'mode':
if ( if not prop.value_list:
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error( _LOGGER.error(
'mode value_list is None, %s', self.entity_id) 'mode value_list is None, %s', self.entity_id)
continue continue
self._mode_list = { self._mode_map = prop.value_list.to_map()
item['value']: item['description'] self._attr_operation_list = list(self._mode_map.values())
for item in prop.value_list}
self._attr_operation_list = list(self._mode_list.values())
self._attr_supported_features |= ( self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE) WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop self._prop_mode = prop
@ -184,7 +184,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
prop=self._prop_on, value=True, update=False) prop=self._prop_on, value=True, update=False)
await self.set_property_async( await self.set_property_async(
prop=self._prop_mode, prop=self._prop_mode,
value=self.__get_mode_value(description=operation_mode)) value=self.get_map_key(
map_=self._mode_map,
value=operation_mode))
async def async_turn_away_mode_on(self) -> None: async def async_turn_away_mode_on(self) -> None:
"""Set the water heater to away mode.""" """Set the water heater to away mode."""
@ -207,20 +209,6 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return STATE_OFF return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on): if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON return STATE_ON
return self.__get_mode_description( return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode)) key=self.get_prop_value(prop=self._prop_mode))
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test rule format.""" """Test rule format."""
import json import json
import logging
from os import listdir, path from os import listdir, path
from typing import Optional from typing import Optional
import pytest import pytest
import yaml import yaml
_LOGGER = logging.getLogger(__name__)
ROOT_PATH: str = path.dirname(path.abspath(__file__)) ROOT_PATH: str = path.dirname(path.abspath(__file__))
TRANS_RELATIVE_PATH: str = path.join( TRANS_RELATIVE_PATH: str = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/translations') ROOT_PATH, '../custom_components/xiaomi_home/translations')
@ -27,10 +30,10 @@ def load_json_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file: with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file) return json.load(file)
except FileNotFoundError: except FileNotFoundError:
print(file_path, 'is not found.') _LOGGER.info('%s is not found.', file_path,)
return None return None
except json.JSONDecodeError: except json.JSONDecodeError:
print(file_path, 'is not a valid JSON file.') _LOGGER.info('%s is not a valid JSON file.', file_path)
return None return None
@ -44,10 +47,10 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file: with open(file_path, 'r', encoding='utf-8') as file:
return yaml.safe_load(file) return yaml.safe_load(file)
except FileNotFoundError: except FileNotFoundError:
print(file_path, 'is not found.') _LOGGER.info('%s is not found.', file_path)
return None return None
except yaml.YAMLError: except yaml.YAMLError:
print(file_path, 'is not a valid YAML file.') _LOGGER.info('%s, is not a valid YAML file.', file_path)
return None return None
@ -116,37 +119,43 @@ def bool_trans(d: dict) -> bool:
return False return False
default_trans: dict = d['translate'].pop('default') default_trans: dict = d['translate'].pop('default')
if not default_trans: if not default_trans:
print('default trans is empty') _LOGGER.info('default trans is empty')
return False return False
default_keys: set[str] = set(default_trans.keys()) default_keys: set[str] = set(default_trans.keys())
for key, trans in d['translate'].items(): for key, trans in d['translate'].items():
trans_keys: set[str] = set(trans.keys()) trans_keys: set[str] = set(trans.keys())
if set(trans.keys()) != default_keys: if set(trans.keys()) != default_keys:
print('bool trans inconsistent', key, default_keys, trans_keys) _LOGGER.info(
'bool trans inconsistent, %s, %s, %s',
key, default_keys, trans_keys)
return False return False
return True 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):
print('invalid type') _LOGGER.info('invalid type')
return False return False
if dict1.keys() != dict2.keys(): if dict1.keys() != dict2.keys():
print('inconsistent key values, ', dict1.keys(), dict2.keys()) _LOGGER.info(
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys())
return False return False
for key in dict1: for key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
if not compare_dict_structure(dict1[key], dict2[key]): if not compare_dict_structure(dict1[key], dict2[key]):
print('inconsistent key values, dict, ', key) _LOGGER.info(
'inconsistent key values, dict, %s', key)
return False return False
elif isinstance(dict1[key], list) and isinstance(dict2[key], list): elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if not all( if not all(
isinstance(i, type(j)) isinstance(i, type(j))
for i, j in zip(dict1[key], dict2[key])): for i, j in zip(dict1[key], dict2[key])):
print('inconsistent key values, list, ', key) _LOGGER.info(
'inconsistent key values, list, %s', key)
return False return False
elif not isinstance(dict1[key], type(dict2[key])): elif not isinstance(dict1[key], type(dict2[key])):
print('inconsistent key values, type, ', key) _LOGGER.info(
'inconsistent key values, type, %s', key)
return False return False
return True return True
@ -239,7 +248,8 @@ def test_miot_lang_integrity():
compare_dict: dict = load_json_file( compare_dict: dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, name)) path.join(TRANS_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
print('compare_dict_structure failed /translations, ', name) _LOGGER.info(
'compare_dict_structure failed /translations, %s', name)
assert False assert False
# Check i18n files structure # Check i18n files structure
default_dict = load_json_file( default_dict = load_json_file(
@ -248,7 +258,8 @@ def test_miot_lang_integrity():
compare_dict: dict = load_json_file( compare_dict: dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, name)) path.join(MIOT_I18N_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict): if not compare_dict_structure(default_dict, compare_dict):
print('compare_dict_structure failed /miot/i18n, ', name) _LOGGER.info(
'compare_dict_structure failed /miot/i18n, %s', name)
assert False assert False
@ -284,10 +295,10 @@ def test_miot_data_sort():
def test_sort_spec_data(): def test_sort_spec_data():
sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE) sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)
save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data) save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)
print(SPEC_BOOL_TRANS_FILE, 'formatted.') _LOGGER.info('%s formatted.', SPEC_BOOL_TRANS_FILE)
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE) sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data) save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
print(SPEC_MULTI_LANG_FILE, 'formatted.') _LOGGER.info('%s formatted.', SPEC_MULTI_LANG_FILE)
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data) save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data)
print(SPEC_FILTER_FILE, 'formatted.') _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Pytest fixtures.""" """Pytest fixtures."""
import logging
import random import random
import shutil import shutil
import pytest import pytest
@ -17,6 +18,21 @@ TEST_CLOUD_SERVER: str = 'cn'
DOMAIN_OAUTH2: str = 'oauth2_info' DOMAIN_OAUTH2: str = 'oauth2_info'
DOMAIN_USER_INFO: str = 'user_info' DOMAIN_USER_INFO: str = 'user_info'
_LOGGER = logging.getLogger(__name__)
@pytest.fixture(scope='session', autouse=True)
def set_logger():
logger = logging.getLogger()
logger.setLevel(logging.INFO)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
_LOGGER.info('set logger, %s', logger)
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
def load_py_file(): def load_py_file():
@ -41,28 +57,28 @@ def load_py_file():
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot', TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot',
file_name), file_name),
path.join(TEST_FILES_PATH, file_name)) path.join(TEST_FILES_PATH, file_name))
print('\nloaded test py files, ', file_list) _LOGGER.info('\nloaded test py files, %s', file_list)
# Copy spec files to test folder # Copy spec files to test folder
shutil.copytree( shutil.copytree(
src=path.join( src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/specs'), TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/specs'),
dst=path.join(TEST_FILES_PATH, 'specs'), dst=path.join(TEST_FILES_PATH, 'specs'),
dirs_exist_ok=True) dirs_exist_ok=True)
print('loaded spec test folder, specs') _LOGGER.info('loaded spec test folder, specs')
# Copy lan files to test folder # Copy lan files to test folder
shutil.copytree( shutil.copytree(
src=path.join( src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'), TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),
dst=path.join(TEST_FILES_PATH, 'lan'), dst=path.join(TEST_FILES_PATH, 'lan'),
dirs_exist_ok=True) dirs_exist_ok=True)
print('loaded lan test folder, lan') _LOGGER.info('loaded lan test folder, lan')
# Copy i18n files to test folder # Copy i18n files to test folder
shutil.copytree( shutil.copytree(
src=path.join( src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n'), TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n'),
dst=path.join(TEST_FILES_PATH, 'i18n'), dst=path.join(TEST_FILES_PATH, 'i18n'),
dirs_exist_ok=True) dirs_exist_ok=True)
print('loaded i18n test folder, i18n') _LOGGER.info('loaded i18n test folder, i18n')
yield yield
@ -127,6 +143,11 @@ def test_domain_oauth2() -> str:
return DOMAIN_OAUTH2 return DOMAIN_OAUTH2
@pytest.fixture(scope='session')
def test_name_uuid() -> str:
return f'{TEST_CLOUD_SERVER}_uuid'
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def test_domain_user_info() -> str: def test_domain_user_info() -> str:
return DOMAIN_USER_INFO return DOMAIN_USER_INFO

View File

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_cloud.py.""" """Unit test for miot_cloud.py."""
import asyncio import asyncio
import logging
import time import time
import webbrowser import webbrowser
import pytest import pytest
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
_LOGGER = logging.getLogger(__name__)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -15,18 +17,18 @@ async def test_miot_oauth_async(
test_cloud_server: str, test_cloud_server: str,
test_oauth2_redirect_url: str, test_oauth2_redirect_url: str,
test_domain_oauth2: str, test_domain_oauth2: str,
test_uuid: str test_uuid: str,
test_name_uuid: str
) -> dict: ) -> dict:
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTOauthClient from miot.miot_cloud import MIoTOauthClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
local_uuid = await miot_storage.load_async( local_uuid = await miot_storage.load_async(
domain=test_domain_oauth2, name=f'{test_cloud_server}_uuid', type_=str) domain=test_domain_oauth2, name=test_name_uuid, type_=str)
uuid = str(local_uuid or test_uuid) uuid = str(local_uuid or test_uuid)
print(f'uuid: {uuid}') _LOGGER.info('uuid: %s', uuid)
miot_oauth = MIoTOauthClient( miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
redirect_url=test_oauth2_redirect_url, redirect_url=test_oauth2_redirect_url,
@ -42,13 +44,13 @@ async def test_miot_oauth_async(
and 'expires_ts' in load_info and 'expires_ts' in load_info
and load_info['expires_ts'] > int(time.time()) and load_info['expires_ts'] > int(time.time())
): ):
print(f'load oauth info, {load_info}') _LOGGER.info('load oauth info, %s', load_info)
oauth_info = load_info oauth_info = load_info
if oauth_info is None: if oauth_info is None:
# gen oauth url # gen oauth url
auth_url: str = miot_oauth.gen_auth_url() auth_url: str = miot_oauth.gen_auth_url()
assert isinstance(auth_url, str) assert isinstance(auth_url, str)
print('auth url: ', auth_url) _LOGGER.info('auth url: %s', auth_url)
# get code # get code
webbrowser.open(auth_url) webbrowser.open(auth_url)
code: str = input('input code: ') code: str = input('input code: ')
@ -57,22 +59,24 @@ async def test_miot_oauth_async(
res_obj = await miot_oauth.get_access_token_async(code=code) res_obj = await miot_oauth.get_access_token_async(code=code)
assert res_obj is not None assert res_obj is not None
oauth_info = res_obj oauth_info = res_obj
print(f'get_access_token result: {res_obj}') _LOGGER.info('get_access_token result: %s', res_obj)
rc = await miot_storage.save_async( rc = await miot_storage.save_async(
test_domain_oauth2, test_cloud_server, oauth_info) test_domain_oauth2, test_cloud_server, oauth_info)
assert rc assert rc
print('save oauth info') _LOGGER.info('save oauth info')
rc = await miot_storage.save_async( rc = await miot_storage.save_async(
test_domain_oauth2, f'{test_cloud_server}_uuid', uuid) test_domain_oauth2, test_name_uuid, uuid)
assert rc assert rc
print('save uuid') _LOGGER.info('save uuid')
access_token = oauth_info.get('access_token', None) access_token = oauth_info.get('access_token', None)
assert isinstance(access_token, str) assert isinstance(access_token, str)
print(f'access_token: {access_token}') _LOGGER.info('access_token: %s', access_token)
refresh_token = oauth_info.get('refresh_token', None) refresh_token = oauth_info.get('refresh_token', None)
assert isinstance(refresh_token, str) assert isinstance(refresh_token, str)
print(f'refresh_token: {refresh_token}') _LOGGER.info('refresh_token: %s', refresh_token)
await miot_oauth.deinit_async()
return oauth_info return oauth_info
@ -82,16 +86,16 @@ async def test_miot_oauth_refresh_token(
test_cache_path: str, test_cache_path: str,
test_cloud_server: str, test_cloud_server: str,
test_oauth2_redirect_url: str, test_oauth2_redirect_url: str,
test_domain_oauth2: str test_domain_oauth2: str,
test_name_uuid: str
): ):
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTOauthClient from miot.miot_cloud import MIoTOauthClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
uuid = await miot_storage.load_async( uuid = await miot_storage.load_async(
domain=test_domain_oauth2, name=f'{test_cloud_server}_uuid', type_=str) domain=test_domain_oauth2, name=test_name_uuid, type_=str)
assert isinstance(uuid, str) assert isinstance(uuid, str)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
domain=test_domain_oauth2, name=test_cloud_server, type_=dict) domain=test_domain_oauth2, name=test_cloud_server, type_=dict)
@ -100,7 +104,7 @@ async def test_miot_oauth_refresh_token(
assert 'refresh_token' in oauth_info assert 'refresh_token' in oauth_info
assert 'expires_ts' in oauth_info assert 'expires_ts' in oauth_info
remaining_time = oauth_info['expires_ts'] - int(time.time()) remaining_time = oauth_info['expires_ts'] - int(time.time())
print(f'token remaining valid time: {remaining_time}s') _LOGGER.info('token remaining valid time: %ss', remaining_time)
# Refresh token # Refresh token
miot_oauth = MIoTOauthClient( miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
@ -117,12 +121,14 @@ async def test_miot_oauth_refresh_token(
assert 'expires_ts' in update_info assert 'expires_ts' in update_info
remaining_time = update_info['expires_ts'] - int(time.time()) remaining_time = update_info['expires_ts'] - int(time.time())
assert remaining_time > 0 assert remaining_time > 0
print(f'refresh token, remaining valid time: {remaining_time}s') _LOGGER.info('refresh token, remaining valid time: %ss', remaining_time)
# Save token # Save token
rc = await miot_storage.save_async( rc = await miot_storage.save_async(
test_domain_oauth2, test_cloud_server, update_info) test_domain_oauth2, test_cloud_server, update_info)
assert rc assert rc
print(f'refresh token success, {update_info}') _LOGGER.info('refresh token success, %s', update_info)
await miot_oauth.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@ -135,7 +141,6 @@ async def test_miot_cloud_get_nickname_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -149,7 +154,9 @@ async def test_miot_cloud_get_nickname_async(
user_info = await miot_http.get_user_info_async() user_info = await miot_http.get_user_info_async()
assert isinstance(user_info, dict) and 'miliaoNick' in user_info assert isinstance(user_info, dict) and 'miliaoNick' in user_info
nickname = user_info['miliaoNick'] nickname = user_info['miliaoNick']
print(f'your nickname: {nickname}\n') _LOGGER.info('your nickname: %s', nickname)
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@ -163,7 +170,6 @@ async def test_miot_cloud_get_uid_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -175,13 +181,15 @@ async def test_miot_cloud_get_uid_async(
uid = await miot_http.get_uid_async() uid = await miot_http.get_uid_async()
assert isinstance(uid, str) assert isinstance(uid, str)
print(f'your uid: {uid}\n') _LOGGER.info('your uid: %s', uid)
# Save uid # Save uid
rc = await miot_storage.save_async( rc = await miot_storage.save_async(
domain=test_domain_user_info, domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', data=uid) name=f'uid_{test_cloud_server}', data=uid)
assert rc assert rc
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.dependency() @pytest.mark.dependency()
@ -194,7 +202,6 @@ async def test_miot_cloud_get_homeinfos_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -219,13 +226,15 @@ async def test_miot_cloud_get_homeinfos_async(
domain=test_domain_user_info, domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', type_=str) name=f'uid_{test_cloud_server}', type_=str)
assert uid == uid2 assert uid == uid2
print(f'your uid: {uid}\n') _LOGGER.info('your uid: %s', uid)
# Get homes # Get homes
home_list = homeinfos.get('home_list', {}) home_list = homeinfos.get('home_list', {})
print(f'your home_list: {home_list}\n') _LOGGER.info('your home_list: ,%s', home_list)
# Get share homes # Get share homes
share_home_list = homeinfos.get('share_home_list', {}) share_home_list = homeinfos.get('share_home_list', {})
print(f'your share_home_list: {share_home_list}\n') _LOGGER.info('your share_home_list: %s', share_home_list)
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@ -239,7 +248,6 @@ async def test_miot_cloud_get_devices_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -261,13 +269,13 @@ async def test_miot_cloud_get_devices_async(
domain=test_domain_user_info, domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', type_=str) name=f'uid_{test_cloud_server}', type_=str)
assert uid == uid2 assert uid == uid2
print(f'your uid: {uid}\n') _LOGGER.info('your uid: %s', uid)
# Get homes # Get homes
homes = devices['homes'] homes = devices['homes']
print(f'your homes: {homes}\n') _LOGGER.info('your homes: %s', homes)
# Get devices # Get devices
devices = devices['devices'] devices = devices['devices']
print(f'your devices count: {len(devices)}\n') _LOGGER.info('your devices count: %s', len(devices))
# Storage homes and devices # Storage homes and devices
rc = await miot_storage.save_async( rc = await miot_storage.save_async(
domain=test_domain_user_info, domain=test_domain_user_info,
@ -278,6 +286,8 @@ async def test_miot_cloud_get_devices_async(
name=f'devices_{test_cloud_server}', data=devices) name=f'devices_{test_cloud_server}', data=devices)
assert rc assert rc
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.dependency() @pytest.mark.dependency()
@ -290,7 +300,6 @@ async def test_miot_cloud_get_devices_with_dids_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -312,8 +321,11 @@ async def test_miot_cloud_get_devices_with_dids_async(
devices_info = await miot_http.get_devices_with_dids_async( devices_info = await miot_http.get_devices_with_dids_async(
dids=test_list) dids=test_list)
assert isinstance(devices_info, dict) assert isinstance(devices_info, dict)
print(f'test did list, {len(test_list)}, {test_list}\n') _LOGGER.info('test did list, %s, %s', len(test_list), test_list)
print(f'test result: {len(devices_info)}, {list(devices_info.keys())}\n') _LOGGER.info(
'test result: %s, %s', len(devices_info), list(devices_info.keys()))
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@ -327,7 +339,6 @@ async def test_miot_cloud_get_prop_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -349,7 +360,9 @@ async def test_miot_cloud_get_prop_async(
for did in test_list: for did in test_list:
prop_value = await miot_http.get_prop_async(did=did, siid=2, piid=1) prop_value = await miot_http.get_prop_async(did=did, siid=2, piid=1)
device_name = local_devices[did]['name'] device_name = local_devices[did]['name']
print(f'{device_name}({did}), prop.2.1: {prop_value}\n') _LOGGER.info('%s(%s), prop.2.1: %s', device_name, did, prop_value)
await miot_http.deinit_async()
@pytest.mark.asyncio @pytest.mark.asyncio
@ -363,7 +376,6 @@ async def test_miot_cloud_get_props_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -384,8 +396,11 @@ async def test_miot_cloud_get_props_async(
test_list = did_list[:6] test_list = did_list[:6]
prop_values = await miot_http.get_props_async(params=[ prop_values = await miot_http.get_props_async(params=[
{'did': did, 'siid': 2, 'piid': 1} for did in test_list]) {'did': did, 'siid': 2, 'piid': 1} for did in test_list])
print(f'test did list, {len(test_list)}, {test_list}\n')
print(f'test result: {len(prop_values)}, {prop_values}\n') _LOGGER.info('test did list, %s, %s', len(test_list), test_list)
_LOGGER.info('test result, %s, %s', len(prop_values), prop_values)
await miot_http.deinit_async()
@pytest.mark.skip(reason='skip danger operation') @pytest.mark.skip(reason='skip danger operation')
@ -404,7 +419,6 @@ async def test_miot_cloud_set_prop_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -431,11 +445,13 @@ async def test_miot_cloud_set_prop_async(
assert test_did != '', 'no central hub gateway found' assert test_did != '', 'no central hub gateway found'
result = await miot_http.set_prop_async(params=[{ result = await miot_http.set_prop_async(params=[{
'did': test_did, 'siid': 3, 'piid': 1, 'value': False}]) 'did': test_did, 'siid': 3, 'piid': 1, 'value': False}])
print(f'test did, {test_did}, prop.3.1=False -> {result}\n') _LOGGER.info('test did, %s, prop.3.1=False -> %s', test_did, result)
await asyncio.sleep(1) await asyncio.sleep(1)
result = await miot_http.set_prop_async(params=[{ result = await miot_http.set_prop_async(params=[{
'did': test_did, 'siid': 3, 'piid': 1, 'value': True}]) 'did': test_did, 'siid': 3, 'piid': 1, 'value': True}])
print(f'test did, {test_did}, prop.3.1=True -> {result}\n') _LOGGER.info('test did, %s, prop.3.1=True -> %s', test_did, result)
await miot_http.deinit_async()
@pytest.mark.skip(reason='skip danger operation') @pytest.mark.skip(reason='skip danger operation')
@ -454,7 +470,6 @@ async def test_miot_cloud_action_async(
from miot.const import OAUTH2_CLIENT_ID from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path) miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async( oauth_info = await miot_storage.load_async(
@ -482,4 +497,6 @@ async def test_miot_cloud_action_async(
result = await miot_http.action_async( result = await miot_http.action_async(
did=test_did, siid=4, aiid=1, did=test_did, siid=4, aiid=1,
in_list=[{'piid': 1, 'value': 'hello world.'}]) in_list=[{'piid': 1, 'value': 'hello world.'}])
print(f'test did, {test_did}, action.4.1 -> {result}\n') _LOGGER.info('test did, %s, action.4.1 -> %s', test_did, result)
await miot_http.deinit_async()

View File

@ -18,7 +18,7 @@ def test_miot_matcher():
if not matcher.get(topic=f'test/+/{l2}'): if not matcher.get(topic=f'test/+/{l2}'):
matcher[f'test/+/{l2}'] = f'test/+/{l2}' matcher[f'test/+/{l2}'] = f'test/+/{l2}'
# Match # Match
match_result: list[(str, dict)] = list(matcher.iter_all_nodes()) match_result: list[str] = list(matcher.iter_all_nodes())
assert len(match_result) == 120 assert len(match_result) == 120
match_result: list[str] = list(matcher.iter_match(topic='test/1/1')) match_result: list[str] = list(matcher.iter_match(topic='test/1/1'))
assert len(match_result) == 3 assert len(match_result) == 3

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_lan.py.""" """Unit test for miot_lan.py."""
import logging
from typing import Any from typing import Any
import pytest import pytest
import asyncio import asyncio
from zeroconf import IPVersion from zeroconf import IPVersion
from zeroconf.asyncio import AsyncZeroconf from zeroconf.asyncio import AsyncZeroconf
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
@ -67,7 +70,7 @@ async def test_lan_async(test_devices: dict):
miot_network = MIoTNetwork() miot_network = MIoTNetwork()
await miot_network.init_async() await miot_network.init_async()
print('miot_network, ', miot_network.network_info) _LOGGER.info('miot_network, %s', miot_network.network_info)
mips_service = MipsService( mips_service = MipsService(
aiozc=AsyncZeroconf(ip_version=IPVersion.V4Only)) aiozc=AsyncZeroconf(ip_version=IPVersion.V4Only))
await mips_service.init_async() await mips_service.init_async()
@ -81,7 +84,7 @@ async def test_lan_async(test_devices: dict):
await miot_lan.vote_for_lan_ctrl_async(key='test', vote=True) await miot_lan.vote_for_lan_ctrl_async(key='test', vote=True)
async def device_state_change(did: str, state: dict, ctx: Any): async def device_state_change(did: str, state: dict, ctx: Any):
print('device state change, ', did, state) _LOGGER.info('device state change, %s, %s', did, state)
if did != test_did: if did != test_did:
return return
if ( if (
@ -91,10 +94,10 @@ async def test_lan_async(test_devices: dict):
# Test sub prop # Test sub prop
miot_lan.sub_prop( miot_lan.sub_prop(
did=did, siid=3, piid=1, handler=lambda msg, ctx: did=did, siid=3, piid=1, handler=lambda msg, ctx:
print(f'sub prop.3.1 msg, {did}={msg}')) _LOGGER.info('sub prop.3.1 msg, %s=%s', did, msg))
miot_lan.sub_prop( miot_lan.sub_prop(
did=did, handler=lambda msg, ctx: did=did, handler=lambda msg, ctx:
print(f'sub all device msg, {did}={msg}')) _LOGGER.info('sub all device msg, %s=%s', did, msg))
evt_push_available.set() evt_push_available.set()
else: else:
# miot_lan.unsub_prop(did=did, siid=3, piid=1) # miot_lan.unsub_prop(did=did, siid=3, piid=1)
@ -102,7 +105,7 @@ async def test_lan_async(test_devices: dict):
evt_push_unavailable.set() evt_push_unavailable.set()
async def lan_state_change(state: bool): async def lan_state_change(state: bool):
print('lan state change, ', state) _LOGGER.info('lan state change, %s', state)
if not state: if not state:
return return
miot_lan.update_devices(devices={ miot_lan.update_devices(devices={

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_mdns.py.""" """Unit test for miot_mdns.py."""
import logging
import pytest import pytest
from zeroconf import IPVersion from zeroconf import IPVersion
from zeroconf.asyncio import AsyncZeroconf from zeroconf.asyncio import AsyncZeroconf
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
@ -13,7 +16,7 @@ async def test_service_loop_async():
async def on_service_state_change( async def on_service_state_change(
group_id: str, state: MipsServiceState, data: MipsServiceData): group_id: str, state: MipsServiceState, data: MipsServiceData):
print( _LOGGER.info(
'on_service_state_change, %s, %s, %s', group_id, state, data) 'on_service_state_change, %s, %s, %s', group_id, state, data)
async with AsyncZeroconf(ip_version=IPVersion.V4Only) as aiozc: async with AsyncZeroconf(ip_version=IPVersion.V4Only) as aiozc:
@ -21,8 +24,9 @@ async def test_service_loop_async():
mips_service.sub_service_change('test', '*', on_service_state_change) mips_service.sub_service_change('test', '*', on_service_state_change)
await mips_service.init_async() await mips_service.init_async()
services_detail = mips_service.get_services() services_detail = mips_service.get_services()
print('get all service, ', services_detail.keys()) _LOGGER.info('get all service, %s', services_detail.keys())
for name, data in services_detail.items(): for name, data in services_detail.items():
print( _LOGGER.info(
'\tinfo, ', name, data['did'], data['addresses'], data['port']) '\tinfo, %s, %s, %s, %s',
name, data['did'], data['addresses'], data['port'])
await mips_service.deinit_async() await mips_service.deinit_async()

View File

@ -1,8 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_network.py.""" """Unit test for miot_network.py."""
import logging
import pytest import pytest
import asyncio import asyncio
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
@ -12,16 +15,16 @@ async def test_network_monitor_loop_async():
miot_net = MIoTNetwork() miot_net = MIoTNetwork()
async def on_network_status_changed(status: bool): async def on_network_status_changed(status: bool):
print(f'on_network_status_changed, {status}') _LOGGER.info('on_network_status_changed, %s', status)
miot_net.sub_network_status(key='test', handler=on_network_status_changed) miot_net.sub_network_status(key='test', handler=on_network_status_changed)
async def on_network_info_changed( async def on_network_info_changed(
status: InterfaceStatus, info: NetworkInfo): status: InterfaceStatus, info: NetworkInfo):
print(f'on_network_info_changed, {status}, {info}') _LOGGER.info('on_network_info_changed, %s, %s', status, info)
miot_net.sub_network_info(key='test', handler=on_network_info_changed) miot_net.sub_network_info(key='test', handler=on_network_info_changed)
await miot_net.init_async(3) await miot_net.init_async()
await asyncio.sleep(3) await asyncio.sleep(3)
print(f'net status: {miot_net.network_status}') _LOGGER.info('net status: %s', miot_net.network_status)
print(f'net info: {miot_net.network_info}') _LOGGER.info('net info: %s', miot_net.network_info)
await miot_net.deinit_async() await miot_net.deinit_async()

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_spec.py.""" """Unit test for miot_spec.py."""
import json import json
import logging
import random import random
import time import time
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
import pytest import pytest
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
@ -79,10 +82,10 @@ async def test_spec_random_parse_async(test_cache_path, test_lang):
storage = MIoTStorage(test_cache_path) storage = MIoTStorage(test_cache_path)
spec_parser = MIoTSpecParser(lang=test_lang, storage=storage) spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)
await spec_parser.init_async() await spec_parser.init_async()
start_ts: int = time.time()*1000 start_ts = time.time()*1000
for index in test_urn_index: for index in test_urn_index:
urn: str = test_urns[int(index)] urn: str = test_urns[int(index)]
result = await spec_parser.parse(urn=urn, skip_cache=True) result = await spec_parser.parse(urn=urn, skip_cache=True)
assert result is not None assert result is not None
end_ts: int = time.time()*1000 end_ts = time.time()*1000
print(f'takes time, {test_count}, {end_ts-start_ts}') _LOGGER.info('takes time, %s, %s', test_count, end_ts-start_ts)

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Unit test for miot_storage.py.""" """Unit test for miot_storage.py."""
import asyncio import asyncio
import logging
from os import path from os import path
import pytest import pytest
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument # pylint: disable=import-outside-toplevel, unused-argument
@ -101,7 +104,7 @@ async def test_multi_task_load_async(test_cache_path):
for _ in range(task_count): for _ in range(task_count):
task_list.append(asyncio.create_task(storage.load_async( task_list.append(asyncio.create_task(storage.load_async(
domain=test_domain, name=name, type_=dict))) domain=test_domain, name=name, type_=dict)))
print(f'\ntask count, {len(task_list)}') _LOGGER.info('task count, %s', len(task_list))
result: list = await asyncio.gather(*task_list) result: list = await asyncio.gather(*task_list)
assert None not in result assert None not in result
@ -178,28 +181,28 @@ async def test_user_config_async(
config=config_update, replace=True) config=config_update, replace=True)
assert (config_replace := await storage.load_user_config_async( assert (config_replace := await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server)) == config_update uid=test_uid, cloud_server=test_cloud_server)) == config_update
print('replace result, ', config_replace) _LOGGER.info('replace result, %s', config_replace)
# Test query # Test query
query_keys = list(config_base.keys()) query_keys = list(config_base.keys())
print('query keys, ', query_keys) _LOGGER.info('query keys, %s', query_keys)
query_result = await storage.load_user_config_async( query_result = await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server, keys=query_keys) uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)
print('query result 1, ', query_result) _LOGGER.info('query result 1, %s', query_result)
assert await storage.update_user_config_async( assert await storage.update_user_config_async(
uid=test_uid, cloud_server=test_cloud_server, uid=test_uid, cloud_server=test_cloud_server,
config=config_base, replace=True) config=config_base, replace=True)
query_result = await storage.load_user_config_async( query_result = await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server, keys=query_keys) uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)
print('query result 2, ', query_result) _LOGGER.info('query result 2, %s', query_result)
query_result = await storage.load_user_config_async( query_result = await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server) uid=test_uid, cloud_server=test_cloud_server)
print('query result all, ', query_result) _LOGGER.info('query result all, %s', query_result)
# Remove config # Remove config
assert await storage.update_user_config_async( assert await storage.update_user_config_async(
uid=test_uid, cloud_server=test_cloud_server, config=None) uid=test_uid, cloud_server=test_cloud_server, config=None)
query_result = await storage.load_user_config_async( query_result = await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server) uid=test_uid, cloud_server=test_cloud_server)
print('remove result, ', query_result) _LOGGER.info('remove result, %s', query_result)
# Remove domain # Remove domain
assert await storage.remove_domain_async(domain='miot_config') assert await storage.remove_domain_async(domain='miot_config')