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

View File

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

View File

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

View File

@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
_prop_target_humidity: Optional[MIoTSpecProperty]
_prop_humidity: Optional[MIoTSpecProperty]
_mode_list: dict[Any, Any]
_mode_map: dict[Any, Any]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -110,7 +110,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_mode = None
self._prop_target_humidity = None
self._prop_humidity = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -119,28 +119,23 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_on = prop
# target-humidity
elif prop.name == 'target-humidity':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.error(
'invalid target-humidity value_range format, %s',
self.entity_id)
continue
self._attr_min_humidity = prop.value_range['min']
self._attr_max_humidity = prop.value_range['max']
self._attr_min_humidity = prop.value_range.min_
self._attr_max_humidity = prop.value_range.max_
self._prop_target_humidity = prop
# mode
elif prop.name == 'mode':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._mode_map = prop.value_list.to_map()
self._attr_available_modes = list(
self._mode_list.values())
self._mode_map.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop
# relative-humidity
@ -163,7 +158,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode."""
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
def is_on(self) -> Optional[bool]:
@ -183,20 +179,6 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
@property
def mode(self) -> Optional[str]:
"""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))
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."""
# pylint: disable=unused-argument
_VALUE_RANGE_MODE_COUNT_MAX = 30
_prop_on: Optional[MIoTSpecProperty]
_prop_on: MIoTSpecProperty
_prop_brightness: Optional[MIoTSpecProperty]
_prop_color_temp: Optional[MIoTSpecProperty]
_prop_color: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty]
_brightness_scale: Optional[tuple[int, int]]
_mode_list: Optional[dict[Any, Any]]
_mode_map: Optional[dict[Any, Any]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -122,7 +122,7 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_color = None
self._prop_mode = None
self._brightness_scale = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -131,20 +131,17 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_on = prop
# brightness
if prop.name == 'brightness':
if isinstance(prop.value_range, dict):
if prop.value_range:
self._brightness_scale = (
prop.value_range['min'], prop.value_range['max'])
prop.value_range.min_, prop.value_range.max_)
self._prop_brightness = prop
elif (
self._mode_list is None
and isinstance(prop.value_list, list)
self._mode_map is None
and prop.value_list
):
# For value-list brightness
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_effect_list = list(self._mode_list.values())
self._mode_map = prop.value_list.to_map()
self._attr_effect_list = list(self._mode_map.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -153,13 +150,13 @@ class Light(MIoTServiceEntity, LightEntity):
continue
# color-temperature
if prop.name == 'color-temperature':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.info(
'invalid color-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_color_temp_kelvin = prop.value_range['min']
self._attr_max_color_temp_kelvin = prop.value_range['max']
self._attr_min_color_temp_kelvin = prop.value_range.min_
self._attr_max_color_temp_kelvin = prop.value_range.max_
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_color_mode = ColorMode.COLOR_TEMP
self._prop_color_temp = prop
@ -171,20 +168,15 @@ class Light(MIoTServiceEntity, LightEntity):
# mode
if prop.name == 'mode':
mode_list = None
if (
isinstance(prop.value_list, list)
and prop.value_list
):
mode_list = {
item['value']: item['description']
for item in prop.value_list}
elif isinstance(prop.value_range, dict):
if prop.value_list:
mode_list = prop.value_list.to_map()
elif prop.value_range:
mode_list = {}
if (
int((
prop.value_range['max']
- prop.value_range['min']
) / prop.value_range['step'])
prop.value_range.max_
- prop.value_range.min_
) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX
):
_LOGGER.info(
@ -192,13 +184,13 @@ class Light(MIoTServiceEntity, LightEntity):
self.entity_id, prop.name, prop.value_range)
else:
for value in range(
prop.value_range['min'],
prop.value_range['max'],
prop.value_range['step']):
prop.value_range.min_,
prop.value_range.max_,
prop.value_range.step):
mode_list[value] = f'mode {value}'
if mode_list:
self._mode_list = mode_list
self._attr_effect_list = list(self._mode_list.values())
self._mode_map = mode_list
self._attr_effect_list = list(self._mode_map.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -213,21 +205,6 @@ class Light(MIoTServiceEntity, LightEntity):
self._attr_supported_color_modes.add(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
def is_on(self) -> Optional[bool]:
"""Return if the light is on."""
@ -264,7 +241,8 @@ class Light(MIoTServiceEntity, LightEntity):
@property
def effect(self) -> Optional[str]:
"""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))
async def async_turn_on(self, **kwargs) -> None:
@ -275,7 +253,7 @@ class Light(MIoTServiceEntity, LightEntity):
result: bool = False
# on
# 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(
prop=self._prop_on, value=value_on)
# brightness
@ -303,11 +281,12 @@ class Light(MIoTServiceEntity, LightEntity):
if ATTR_EFFECT in kwargs:
result = await self.set_property_async(
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
async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off."""
# 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)

View File

@ -45,11 +45,14 @@ off Xiaomi or its affiliates' products.
Common utilities.
"""
import asyncio
import json
from os import path
import random
from typing import Any, Optional
import hashlib
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from paho.mqtt.matcher import MQTTMatcher
import yaml
@ -83,10 +86,12 @@ def randomize_int(value: int, ratio: float) -> int:
"""Randomize an integer value."""
return int(value * (1 - ratio + random.random()*2*ratio))
def randomize_float(value: float, ratio: float) -> float:
"""Randomize a float value."""
return value * (1 - ratio + random.random()*2*ratio)
class MIoTMatcher(MQTTMatcher):
"""MIoT Pub/Sub topic matcher."""
@ -105,3 +110,68 @@ class MIoTMatcher(MQTTMatcher):
return self[topic]
except KeyError:
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
def state(self) -> str:
return self.state
return self._state
async def deinit_async(self) -> None:
if self._session and not self._session.closed:
@ -744,7 +744,7 @@ class MIoTHttpClient:
prop_obj['fut'].set_result(None)
if props_req:
_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:
self._get_prop_timer = self._main_loop.call_later(

View File

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

View File

@ -117,7 +117,7 @@ class MipsServiceData:
self.type = service_info.type
self.server = service_info.server or ''
# 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.profile_bin[9:17][::-1]).decode('utf-8')
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')
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:
"""
Boolean value translation.

View File

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

View File

@ -92,9 +92,9 @@ class Number(MIoTPropertyEntity, NumberEntity):
self._attr_icon = self.spec.icon
# Set value range
if self._value_range:
self._attr_native_min_value = self._value_range['min']
self._attr_native_max_value = self._value_range['max']
self._attr_native_step = self._value_range['step']
self._attr_native_min_value = self._value_range.min_
self._attr_native_max_value = self._value_range.max_
self._attr_native_step = self._value_range.step
@property
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:
"""Initialize the Select."""
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:
"""Change the selected option."""

View File

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

View File

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

View File

@ -120,28 +120,18 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
# properties
for prop in entity_data.props:
if prop.name == 'status':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid status value_list, %s', self.entity_id)
continue
self._status_map = {
item['value']: item['description']
for item in prop.value_list}
self._status_map = prop.value_list.to_map()
self._prop_status = prop
elif prop.name == 'fan-level':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
continue
self._fan_level_map = {
item['value']: item['description']
for item in prop.value_list}
self._fan_level_map = prop.value_list.to_map()
self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop
@ -202,7 +192,7 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
return self.get_map_description(
return self.get_map_value(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
@ -214,6 +204,6 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def fan_speed(self) -> Optional[str]:
"""Return the current fan speed of the vacuum cleaner."""
return self.get_map_description(
return self.get_map_value(
map_=self._fan_level_map,
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_mode: Optional[MIoTSpecProperty]
_mode_list: Optional[dict[Any, Any]]
_mode_map: Optional[dict[Any, Any]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -106,7 +106,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_temp = None
self._prop_target_temp = None
self._prop_mode = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -115,7 +115,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_on = prop
# temperature
if prop.name == 'temperature':
if isinstance(prop.value_range, dict):
if prop.value_range:
if (
self._attr_temperature_unit is None
and prop.external_unit
@ -128,9 +128,14 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self.entity_id)
# target-temperature
if prop.name == 'target-temperature':
self._attr_min_temp = prop.value_range['min']
self._attr_max_temp = prop.value_range['max']
self._attr_precision = prop.value_range['step']
if not prop.value_range:
_LOGGER.error(
'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:
self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= (
@ -138,17 +143,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_target_temp = prop
# mode
if prop.name == 'mode':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_operation_list = list(self._mode_list.values())
self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop
@ -184,7 +184,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
prop=self._prop_on, value=True, update=False)
await self.set_property_async(
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:
"""Set the water heater to away mode."""
@ -207,20 +209,6 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_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))
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 -*-
"""Test rule format."""
import json
import logging
from os import listdir, path
from typing import Optional
import pytest
import yaml
_LOGGER = logging.getLogger(__name__)
ROOT_PATH: str = path.dirname(path.abspath(__file__))
TRANS_RELATIVE_PATH: str = path.join(
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:
return json.load(file)
except FileNotFoundError:
print(file_path, 'is not found.')
_LOGGER.info('%s is not found.', file_path,)
return None
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
@ -44,10 +47,10 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
with open(file_path, 'r', encoding='utf-8') as file:
return yaml.safe_load(file)
except FileNotFoundError:
print(file_path, 'is not found.')
_LOGGER.info('%s is not found.', file_path)
return None
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
@ -116,37 +119,43 @@ def bool_trans(d: dict) -> bool:
return False
default_trans: dict = d['translate'].pop('default')
if not default_trans:
print('default trans is empty')
_LOGGER.info('default trans is empty')
return False
default_keys: set[str] = set(default_trans.keys())
for key, trans in d['translate'].items():
trans_keys: set[str] = set(trans.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 True
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
print('invalid type')
_LOGGER.info('invalid type')
return False
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
for key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
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
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
if not all(
isinstance(i, type(j))
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
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 True
@ -239,7 +248,8 @@ def test_miot_lang_integrity():
compare_dict: dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, name))
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
# Check i18n files structure
default_dict = load_json_file(
@ -248,7 +258,8 @@ def test_miot_lang_integrity():
compare_dict: dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, name))
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
@ -284,10 +295,10 @@ def test_miot_data_sort():
def test_sort_spec_data():
sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)
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)
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)
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 -*-
"""Pytest fixtures."""
import logging
import random
import shutil
import pytest
@ -17,6 +18,21 @@ TEST_CLOUD_SERVER: str = 'cn'
DOMAIN_OAUTH2: str = 'oauth2_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)
def load_py_file():
@ -41,28 +57,28 @@ def load_py_file():
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot',
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
shutil.copytree(
src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/specs'),
dst=path.join(TEST_FILES_PATH, 'specs'),
dirs_exist_ok=True)
print('loaded spec test folder, specs')
_LOGGER.info('loaded spec test folder, specs')
# Copy lan files to test folder
shutil.copytree(
src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),
dst=path.join(TEST_FILES_PATH, 'lan'),
dirs_exist_ok=True)
print('loaded lan test folder, lan')
_LOGGER.info('loaded lan test folder, lan')
# Copy i18n files to test folder
shutil.copytree(
src=path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n'),
dst=path.join(TEST_FILES_PATH, 'i18n'),
dirs_exist_ok=True)
print('loaded i18n test folder, i18n')
_LOGGER.info('loaded i18n test folder, i18n')
yield
@ -127,6 +143,11 @@ def test_domain_oauth2() -> str:
return DOMAIN_OAUTH2
@pytest.fixture(scope='session')
def test_name_uuid() -> str:
return f'{TEST_CLOUD_SERVER}_uuid'
@pytest.fixture(scope='session')
def test_domain_user_info() -> str:
return DOMAIN_USER_INFO

View File

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_cloud.py."""
import asyncio
import logging
import time
import webbrowser
import pytest
# pylint: disable=import-outside-toplevel, unused-argument
_LOGGER = logging.getLogger(__name__)
@pytest.mark.asyncio
@ -15,18 +17,18 @@ async def test_miot_oauth_async(
test_cloud_server: str,
test_oauth2_redirect_url: str,
test_domain_oauth2: str,
test_uuid: str
test_uuid: str,
test_name_uuid: str
) -> dict:
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTOauthClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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)
print(f'uuid: {uuid}')
_LOGGER.info('uuid: %s', uuid)
miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID,
redirect_url=test_oauth2_redirect_url,
@ -42,13 +44,13 @@ async def test_miot_oauth_async(
and 'expires_ts' in load_info
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
if oauth_info is None:
# gen oauth url
auth_url: str = miot_oauth.gen_auth_url()
assert isinstance(auth_url, str)
print('auth url: ', auth_url)
_LOGGER.info('auth url: %s', auth_url)
# get code
webbrowser.open(auth_url)
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)
assert res_obj is not None
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(
test_domain_oauth2, test_cloud_server, oauth_info)
assert rc
print('save oauth info')
_LOGGER.info('save oauth info')
rc = await miot_storage.save_async(
test_domain_oauth2, f'{test_cloud_server}_uuid', uuid)
test_domain_oauth2, test_name_uuid, uuid)
assert rc
print('save uuid')
_LOGGER.info('save uuid')
access_token = oauth_info.get('access_token', None)
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)
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
@ -82,16 +86,16 @@ async def test_miot_oauth_refresh_token(
test_cache_path: str,
test_cloud_server: 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.miot_cloud import MIoTOauthClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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)
oauth_info = await miot_storage.load_async(
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 'expires_ts' in oauth_info
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
miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID,
@ -117,12 +121,14 @@ async def test_miot_oauth_refresh_token(
assert 'expires_ts' in update_info
remaining_time = update_info['expires_ts'] - int(time.time())
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
rc = await miot_storage.save_async(
test_domain_oauth2, test_cloud_server, update_info)
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
@ -135,7 +141,6 @@ async def test_miot_cloud_get_nickname_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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()
assert isinstance(user_info, dict) and 'miliaoNick' in user_info
nickname = user_info['miliaoNick']
print(f'your nickname: {nickname}\n')
_LOGGER.info('your nickname: %s', nickname)
await miot_http.deinit_async()
@pytest.mark.asyncio
@ -163,7 +170,6 @@ async def test_miot_cloud_get_uid_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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()
assert isinstance(uid, str)
print(f'your uid: {uid}\n')
_LOGGER.info('your uid: %s', uid)
# Save uid
rc = await miot_storage.save_async(
domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', data=uid)
assert rc
await miot_http.deinit_async()
@pytest.mark.asyncio
@pytest.mark.dependency()
@ -194,7 +202,6 @@ async def test_miot_cloud_get_homeinfos_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async(
@ -219,13 +226,15 @@ async def test_miot_cloud_get_homeinfos_async(
domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', type_=str)
assert uid == uid2
print(f'your uid: {uid}\n')
_LOGGER.info('your uid: %s', uid)
# Get homes
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
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
@ -239,7 +248,6 @@ async def test_miot_cloud_get_devices_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async(
@ -261,13 +269,13 @@ async def test_miot_cloud_get_devices_async(
domain=test_domain_user_info,
name=f'uid_{test_cloud_server}', type_=str)
assert uid == uid2
print(f'your uid: {uid}\n')
_LOGGER.info('your uid: %s', uid)
# Get homes
homes = devices['homes']
print(f'your homes: {homes}\n')
_LOGGER.info('your homes: %s', homes)
# Get devices
devices = devices['devices']
print(f'your devices count: {len(devices)}\n')
_LOGGER.info('your devices count: %s', len(devices))
# Storage homes and devices
rc = await miot_storage.save_async(
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)
assert rc
await miot_http.deinit_async()
@pytest.mark.asyncio
@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.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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(
dids=test_list)
assert isinstance(devices_info, dict)
print(f'test did list, {len(test_list)}, {test_list}\n')
print(f'test result: {len(devices_info)}, {list(devices_info.keys())}\n')
_LOGGER.info('test did list, %s, %s', len(test_list), test_list)
_LOGGER.info(
'test result: %s, %s', len(devices_info), list(devices_info.keys()))
await miot_http.deinit_async()
@pytest.mark.asyncio
@ -327,7 +339,6 @@ async def test_miot_cloud_get_prop_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async(
@ -349,7 +360,9 @@ async def test_miot_cloud_get_prop_async(
for did in test_list:
prop_value = await miot_http.get_prop_async(did=did, siid=2, piid=1)
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
@ -363,7 +376,6 @@ async def test_miot_cloud_get_props_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async(
@ -384,8 +396,11 @@ async def test_miot_cloud_get_props_async(
test_list = did_list[:6]
prop_values = await miot_http.get_props_async(params=[
{'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')
@ -404,7 +419,6 @@ async def test_miot_cloud_set_prop_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
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'
result = await miot_http.set_prop_async(params=[{
'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)
result = await miot_http.set_prop_async(params=[{
'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')
@ -454,7 +470,6 @@ async def test_miot_cloud_action_async(
from miot.const import OAUTH2_CLIENT_ID
from miot.miot_cloud import MIoTHttpClient
from miot.miot_storage import MIoTStorage
print('') # separate from previous output
miot_storage = MIoTStorage(test_cache_path)
oauth_info = await miot_storage.load_async(
@ -482,4 +497,6 @@ async def test_miot_cloud_action_async(
result = await miot_http.action_async(
did=test_did, siid=4, aiid=1,
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}'):
matcher[f'test/+/{l2}'] = f'test/+/{l2}'
# 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
match_result: list[str] = list(matcher.iter_match(topic='test/1/1'))
assert len(match_result) == 3

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_lan.py."""
import logging
from typing import Any
import pytest
import asyncio
from zeroconf import IPVersion
from zeroconf.asyncio import AsyncZeroconf
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument
@ -67,7 +70,7 @@ async def test_lan_async(test_devices: dict):
miot_network = MIoTNetwork()
await miot_network.init_async()
print('miot_network, ', miot_network.network_info)
_LOGGER.info('miot_network, %s', miot_network.network_info)
mips_service = MipsService(
aiozc=AsyncZeroconf(ip_version=IPVersion.V4Only))
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)
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:
return
if (
@ -91,10 +94,10 @@ async def test_lan_async(test_devices: dict):
# Test sub prop
miot_lan.sub_prop(
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(
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()
else:
# 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()
async def lan_state_change(state: bool):
print('lan state change, ', state)
_LOGGER.info('lan state change, %s', state)
if not state:
return
miot_lan.update_devices(devices={

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_mdns.py."""
import logging
import pytest
from zeroconf import IPVersion
from zeroconf.asyncio import AsyncZeroconf
_LOGGER = logging.getLogger(__name__)
# pylint: disable=import-outside-toplevel, unused-argument
@ -13,7 +16,7 @@ async def test_service_loop_async():
async def on_service_state_change(
group_id: str, state: MipsServiceState, data: MipsServiceData):
print(
_LOGGER.info(
'on_service_state_change, %s, %s, %s', group_id, state, data)
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)
await mips_service.init_async()
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():
print(
'\tinfo, ', name, data['did'], data['addresses'], data['port'])
_LOGGER.info(
'\tinfo, %s, %s, %s, %s',
name, data['did'], data['addresses'], data['port'])
await mips_service.deinit_async()

View File

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

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_spec.py."""
import json
import logging
import random
import time
from urllib.request import Request, urlopen
import pytest
_LOGGER = logging.getLogger(__name__)
# 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)
spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)
await spec_parser.init_async()
start_ts: int = time.time()*1000
start_ts = time.time()*1000
for index in test_urn_index:
urn: str = test_urns[int(index)]
result = await spec_parser.parse(urn=urn, skip_cache=True)
assert result is not None
end_ts: int = time.time()*1000
print(f'takes time, {test_count}, {end_ts-start_ts}')
end_ts = time.time()*1000
_LOGGER.info('takes time, %s, %s', test_count, end_ts-start_ts)

View File

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_storage.py."""
import asyncio
import logging
from os import path
import pytest
_LOGGER = logging.getLogger(__name__)
# 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):
task_list.append(asyncio.create_task(storage.load_async(
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)
assert None not in result
@ -178,28 +181,28 @@ async def test_user_config_async(
config=config_update, replace=True)
assert (config_replace := await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server)) == config_update
print('replace result, ', config_replace)
_LOGGER.info('replace result, %s', config_replace)
# Test query
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(
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(
uid=test_uid, cloud_server=test_cloud_server,
config=config_base, replace=True)
query_result = await storage.load_user_config_async(
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(
uid=test_uid, cloud_server=test_cloud_server)
print('query result all, ', query_result)
_LOGGER.info('query result all, %s', query_result)
# Remove config
assert await storage.update_user_config_async(
uid=test_uid, cloud_server=test_cloud_server, config=None)
query_result = await storage.load_user_config_async(
uid=test_uid, cloud_server=test_cloud_server)
print('remove result, ', query_result)
_LOGGER.info('remove result, %s', query_result)
# Remove domain
assert await storage.remove_domain_async(domain='miot_config')