This commit is contained in:
Paul Shawn 2025-01-14 09:35:57 +08:00 committed by GitHub
commit 4952c3a8a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1120 additions and 976 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

@ -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,

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