Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Shawn
7fd4b8017b
Merge d9d8433405 into 5d4b975f85 2025-01-07 12:25:24 +00:00
31 changed files with 559 additions and 621 deletions

View File

@ -1,16 +1,5 @@
# CHANGELOG
## v0.1.5b1
This version will cause some Xiaomi routers that do not support access (#564) to become unavailable. You can update the device list in the configuration or delete it manually.
### Added
- Fan entity support direction ctrl [#556](https://github.com/XiaoMi/ha_xiaomi_home/pull/556)
### Changed
- Filter miwifi.* devices and xiaomi.router.rd03 [#564](https://github.com/XiaoMi/ha_xiaomi_home/pull/564)
### Fixed
- Fix multi ha instance login [#560](https://github.com/XiaoMi/ha_xiaomi_home/pull/560)
- Fix fan speed [#464](https://github.com/XiaoMi/ha_xiaomi_home/pull/464)
- The number of profile models updated from 660 to 823. [#583](https://github.com/XiaoMi/ha_xiaomi_home/pull/583)
## v0.1.5b0
### Added
- Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101)

View File

@ -156,56 +156,64 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
_LOGGER.error(
'unknown on property, %s', self.entity_id)
elif prop.name == 'mode':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or 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.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
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
self._attr_hvac_modes = list(self._hvac_mode_map.values())
self._prop_mode = prop
elif prop.name == 'target-temperature':
if not prop.value_range:
if not isinstance(prop.value_range, dict):
_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 prop.value_range:
if not isinstance(prop.value_range, dict):
_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 prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
continue
self._fan_mode_map = prop.value_list.to_map()
self._fan_mode_map = {
item['value']: item['description']
for item in prop.value_list}
self._attr_fan_modes = list(self._fan_mode_map.values())
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
self._prop_fan_level = prop
@ -261,8 +269,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_key(
map_=self._hvac_mode_map, value=hvac_mode)
mode_value = self.get_map_value(
map_=self._hvac_mode_map, description=hvac_mode)
if (
mode_value is None or
not await self.set_property_async(
@ -331,8 +339,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
mode_value = self.get_map_key(
map_=self._fan_mode_map, value=fan_mode)
mode_value = self.get_map_value(
map_=self._fan_mode_map, description=fan_mode)
if mode_value is None or not await self.set_property_async(
prop=self._prop_fan_level, value=mode_value):
raise RuntimeError(
@ -368,9 +376,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_key(
return self.get_map_description(
map_=self._hvac_mode_map,
value=self.get_prop_value(prop=self._prop_mode))
key=self.get_prop_value(prop=self._prop_mode))
@property
def fan_mode(self) -> Optional[str]:
@ -378,7 +386,7 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
Requires ClimateEntityFeature.FAN_MODE.
"""
return self.get_map_value(
return self.get_map_description(
map_=self._fan_mode_map,
key=self.get_prop_value(prop=self._prop_fan_level))
@ -438,8 +446,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_key(
map_=self._hvac_mode_map, value=mode))
prop=self._prop_mode, value=self.get_map_value(
map_=self._hvac_mode_map, description=mode))
# T: target temperature
if 'T' in v_ac_state and self._prop_target_temp:
self.set_prop_value(prop=self._prop_target_temp,
@ -509,24 +517,29 @@ class Heater(MIoTServiceEntity, ClimateEntity):
ClimateEntityFeature.TURN_OFF)
self._prop_on = prop
elif prop.name == 'target-temperature':
if not prop.value_range:
if not isinstance(prop.value_range, dict):
_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 prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'invalid heat-level value_list, %s', self.entity_id)
continue
self._heat_level_map = prop.value_list.to_map()
self._heat_level_map = {
item['value']: item['description']
for item in prop.value_list}
self._attr_preset_modes = list(self._heat_level_map.values())
self._attr_supported_features |= (
ClimateEntityFeature.PRESET_MODE)
@ -569,8 +582,8 @@ class Heater(MIoTServiceEntity, ClimateEntity):
"""Set the preset mode."""
await self.set_property_async(
self._prop_heat_level,
value=self.get_map_key(
map_=self._heat_level_map, value=preset_mode))
value=self.get_map_value(
map_=self._heat_level_map, description=preset_mode))
@property
def target_temperature(self) -> Optional[float]:
@ -600,7 +613,7 @@ class Heater(MIoTServiceEntity, ClimateEntity):
@property
def preset_mode(self) -> Optional[str]:
return (
self.get_map_value(
self.get_map_description(
map_=self._heat_level_map,
key=self.get_prop_value(prop=self._prop_heat_level))
if self._prop_heat_level else None)

View File

@ -68,7 +68,6 @@ from homeassistant.components.webhook import (
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.instance_id import async_get
import homeassistant.helpers.config_validation as cv
from .miot.const import (
@ -248,13 +247,6 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input:
self._cloud_server = user_input.get(
'cloud_server', self._cloud_server)
# Gen instance uuid
ha_uuid = await async_get(self.hass)
if not ha_uuid:
raise AbortFlow(reason='ha_uuid_get_failed')
self._uuid = hashlib.sha256(
f'{ha_uuid}.{self._virtual_did}.{self._cloud_server}'.encode(
'utf-8')).hexdigest()[:32]
self._integration_language = user_input.get(
'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
self._miot_i18n = MIoTI18n(
@ -423,11 +415,9 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID,
redirect_url=self._oauth_redirect_url_full,
cloud_server=self._cloud_server,
uuid=self._uuid,
loop=self._main_loop)
state = hashlib.sha1(
f'd=ha.{self._uuid}'.encode('utf-8')).hexdigest()
cloud_server=self._cloud_server
)
state = str(secrets.randbits(64))
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state
self._cc_oauth_auth_url = miot_oauth.gen_auth_url(
redirect_url=self._oauth_redirect_url_full, state=state)
@ -508,6 +498,11 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
client_id=OAUTH2_CLIENT_ID,
access_token=auth_info['access_token'])
self._auth_info = auth_info
# Gen uuid
self._uuid = hashlib.sha256(
f'{self._virtual_did}.{auth_info["access_token"]}'.encode(
'utf-8')
).hexdigest()[:32]
try:
self._nick_name = (
await self._miot_http.get_user_info_async() or {}
@ -1150,9 +1145,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_oauth(self, user_input=None):
try:
if self._cc_task_oauth is None:
state = hashlib.sha1(
f'd=ha.{self._entry_data["uuid"]}'.encode('utf-8')
).hexdigest()
state = str(secrets.randbits(64))
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state
self._miot_oauth.set_redirect_url(
redirect_url=self._oauth_redirect_url_full)

View File

@ -132,47 +132,53 @@ class Cover(MIoTServiceEntity, CoverEntity):
# properties
for prop in entity_data.props:
if prop.name == 'motor-control':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'motor-control value_list is None, %s', self.entity_id)
continue
for item in prop.value_list.items:
if item.name in {'open'}:
for item in prop.value_list:
if item['name'].lower() in ['open']:
self._attr_supported_features |= (
CoverEntityFeature.OPEN)
self._prop_motor_value_open = item.value
elif item.name in {'close'}:
self._prop_motor_value_open = item['value']
elif item['name'].lower() in ['close']:
self._attr_supported_features |= (
CoverEntityFeature.CLOSE)
self._prop_motor_value_close = item.value
elif item.name in {'pause'}:
self._prop_motor_value_close = item['value']
elif item['name'].lower() 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 prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'status value_list is None, %s', self.entity_id)
continue
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
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']
self._prop_status = prop
elif prop.name == 'current-position':
self._prop_current_position = prop
elif prop.name == 'target-position':
if not prop.value_range:
if not isinstance(prop.value_range, dict):
_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: MIoTSpecProperty
_prop_on: Optional[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_map: Optional[dict[Any, Any]]
_mode_list: 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)
# _prop_on is required
self._prop_on = None
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_map = None
self._mode_list = None
# properties
for prop in entity_data.props:
@ -133,34 +133,42 @@ class Fan(MIoTServiceEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.TURN_OFF
self._prop_on = prop
elif prop.name == 'fan-level':
if prop.value_range:
if isinstance(prop.value_range, dict):
# 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 = prop.value_list.to_map()
self._speed_name_map = {
item['value']: item['description']
for item in prop.value_list}
self._speed_names = list(self._speed_name_map.values())
self._attr_speed_count = len(self._speed_names)
self._attr_speed_count = len(prop.value_list)
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._prop_fan_level = prop
elif prop.name == 'mode':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_map = prop.value_list.to_map()
self._attr_preset_modes = list(self._mode_map.values())
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_preset_modes = list(self._mode_list.values())
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
self._prop_mode = prop
elif prop.name == 'horizontal-swing':
@ -170,11 +178,16 @@ class Fan(MIoTServiceEntity, FanEntity):
if prop.format_ == 'bool':
self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True
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
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']
if (
self._prop_wind_reverse_forward is None
or self._prop_wind_reverse_reverse is None
@ -186,9 +199,21 @@ 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: Optional[int] = None,
preset_mode: Optional[str] = None, **kwargs: Any
self, percentage: int = None, preset_mode: str = None, **kwargs: Any
) -> None:
"""Turn the fan on.
@ -200,12 +225,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=self.get_map_value(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
prop=self._prop_fan_level, value=speed_value)
else:
await self.set_property_async(
prop=self._prop_fan_level,
@ -216,8 +241,7 @@ class Fan(MIoTServiceEntity, FanEntity):
if preset_mode:
await self.set_property_async(
self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=preset_mode))
value=self.__get_mode_value(description=preset_mode))
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
@ -231,12 +255,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=self.get_map_value(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
prop=self._prop_fan_level, value=speed_value)
else:
await self.set_property_async(
prop=self._prop_fan_level,
@ -253,8 +277,7 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Set the preset mode."""
await self.set_property_async(
self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=preset_mode))
value=self.__get_mode_value(description=preset_mode))
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
@ -283,8 +306,7 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Return the current preset mode,
e.g., auto, smart, eco, favorite."""
return (
self.get_map_value(
map_=self._mode_map,
self.__get_mode_description(
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_map: dict[Any, Any]
_mode_list: 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_map = None
self._mode_list = None
# properties
for prop in entity_data.props:
@ -119,23 +119,28 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_on = prop
# target-humidity
elif prop.name == 'target-humidity':
if not prop.value_range:
if not isinstance(prop.value_range, dict):
_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 prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_map = prop.value_list.to_map()
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_available_modes = list(
self._mode_map.values())
self._mode_list.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop
# relative-humidity
@ -158,8 +163,7 @@ 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_map_key(map_=self._mode_map, value=mode))
prop=self._prop_mode, value=self.__get_mode_value(description=mode))
@property
def is_on(self) -> Optional[bool]:
@ -179,6 +183,20 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
@property
def mode(self) -> Optional[str]:
"""Return the current preset mode."""
return self.get_map_value(
map_=self._mode_map,
return self.__get_mode_description(
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: MIoTSpecProperty
_prop_on: Optional[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_map: Optional[dict[Any, Any]]
_mode_list: 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_map = None
self._mode_list = None
# properties
for prop in entity_data.props:
@ -131,17 +131,20 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_on = prop
# brightness
if prop.name == 'brightness':
if prop.value_range:
if isinstance(prop.value_range, dict):
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_map is None
self._mode_list is None
and isinstance(prop.value_list, list)
and prop.value_list
):
# For value-list brightness
self._mode_map = prop.value_list.to_map()
self._attr_effect_list = list(self._mode_map.values())
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_effect_list = list(self._mode_list.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -150,13 +153,13 @@ class Light(MIoTServiceEntity, LightEntity):
continue
# color-temperature
if prop.name == 'color-temperature':
if not prop.value_range:
if not isinstance(prop.value_range, dict):
_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
@ -168,15 +171,20 @@ class Light(MIoTServiceEntity, LightEntity):
# mode
if prop.name == 'mode':
mode_list = None
if prop.value_list:
mode_list = prop.value_list.to_map()
elif prop.value_range:
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):
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(
@ -184,13 +192,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_map = mode_list
self._attr_effect_list = list(self._mode_map.values())
self._mode_list = mode_list
self._attr_effect_list = list(self._mode_list.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -205,6 +213,21 @@ 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."""
@ -241,8 +264,7 @@ class Light(MIoTServiceEntity, LightEntity):
@property
def effect(self) -> Optional[str]:
"""Return the current mode."""
return self.get_map_value(
map_=self._mode_map,
return self.__get_mode_description(
key=self.get_prop_value(prop=self._prop_mode))
async def async_turn_on(self, **kwargs) -> None:
@ -253,7 +275,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
@ -281,12 +303,11 @@ class Light(MIoTServiceEntity, LightEntity):
if ATTR_EFFECT in kwargs:
result = await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
value=self.__get_mode_value(description=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

@ -25,7 +25,7 @@
"cryptography",
"psutil"
],
"version": "v0.1.5b1",
"version": "v0.1.5b0",
"zeroconf": [
"_miot-central._tcp.local."
]

View File

@ -257,7 +257,6 @@ class MIoTClient:
client_id=OAUTH2_CLIENT_ID,
redirect_url=self._entry_data['oauth_redirect_url'],
cloud_server=self._cloud_server,
uuid=self._entry_data["uuid"],
loop=self._main_loop)
# MIoT http client instance
self._http = MIoTHttpClient(

View File

@ -75,11 +75,10 @@ class MIoTOauthClient:
_oauth_host: str
_client_id: int
_redirect_url: str
_device_id: str
def __init__(
self, client_id: str, redirect_url: str, cloud_server: str,
uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
if client_id is None or client_id.strip() == '':
@ -88,8 +87,6 @@ class MIoTOauthClient:
raise MIoTOauthError('invalid redirect_url')
if not cloud_server:
raise MIoTOauthError('invalid cloud_server')
if not uuid:
raise MIoTOauthError('invalid uuid')
self._client_id = int(client_id)
self._redirect_url = redirect_url
@ -97,7 +94,6 @@ class MIoTOauthClient:
self._oauth_host = DEFAULT_OAUTH2_API_HOST
else:
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._device_id = f'ha.{uuid}'
self._session = aiohttp.ClientSession(loop=self._main_loop)
async def deinit_async(self) -> None:
@ -136,7 +132,6 @@ class MIoTOauthClient:
'redirect_uri': redirect_url or self._redirect_url,
'client_id': self._client_id,
'response_type': 'code',
'device_id': self._device_id
}
if state:
params['state'] = state
@ -196,7 +191,6 @@ class MIoTOauthClient:
'client_id': self._client_id,
'redirect_uri': self._redirect_url,
'code': code,
'device_id': self._device_id
})
async def refresh_access_token_async(self, refresh_token: str) -> dict:
@ -735,7 +729,7 @@ class MIoTHttpClient:
prop_obj['fut'].set_result(None)
if props_req:
_LOGGER.info(
'get prop from cloud failed, %s', props_req)
'get prop from cloud failed, %s, %s', len(key), props_req)
if self._get_prop_list:
self._get_prop_timer = self._main_loop.call_later(

View File

@ -94,9 +94,7 @@ from .miot_spec import (
MIoTSpecEvent,
MIoTSpecInstance,
MIoTSpecProperty,
MIoTSpecService,
MIoTSpecValueList,
MIoTSpecValueRange
MIoTSpecService
)
_LOGGER = logging.getLogger(__name__)
@ -513,7 +511,7 @@ class MIoTDevice:
if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']):
return None
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
if prop.format_ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']:
return None
if prop.unit:
@ -566,9 +564,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:
@ -838,20 +836,18 @@ class MIoTServiceEntity(Entity):
self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid)
def get_map_value(
self, map_: dict[int, Any], key: int
) -> Any:
def get_map_description(self, map_: dict[int, Any], key: int) -> Any:
if map_ is None:
return None
return map_.get(key, None)
def get_map_key(
self, map_: dict[int, Any], value: Any
def get_map_value(
self, map_: dict[int, Any], description: Any
) -> Optional[int]:
if map_ is None:
return None
for key, value_ in map_.items():
if value_ == value:
for key, value in map_.items():
if value == description:
return key
return None
@ -1010,9 +1006,10 @@ class MIoTPropertyEntity(Entity):
service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop
_value_range: Optional[MIoTSpecValueRange]
# {'min':int, 'max':int, 'step': int}
_value_range: dict[str, int]
# {Any: Any}
_value_list: Optional[MIoTSpecValueList]
_value_list: Optional[dict[Any, Any]]
_value: Any
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
@ -1025,7 +1022,11 @@ class MIoTPropertyEntity(Entity):
self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop
self._value_range = spec.value_range
self._value_list = spec.value_list
if 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._pending_write_ha_state_timer = None
# Gen entity_id
@ -1076,12 +1077,15 @@ class MIoTPropertyEntity(Entity):
def get_vlist_description(self, value: Any) -> Optional[str]:
if not self._value_list:
return None
return self._value_list.get_description_by_value(value)
return self._value_list.get(value, None)
def get_vlist_value(self, description: str) -> Any:
if not self._value_list:
return None
return self._value_list.get_value_by_description(description)
for key, value in self._value_list.items():
if value == description:
return key
return None
async def set_property_async(self, value: Any) -> bool:
if not self.spec.writable:

View File

@ -46,11 +46,13 @@ off Xiaomi or its affiliates' products.
MIoT-Spec-V2 parser.
"""
import asyncio
import json
import platform
import time
from typing import Any, Optional, Type, Union
from typing import Any, Optional
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import logging
from slugify import slugify
# pylint: disable=relative-beyond-top-level
@ -60,162 +62,53 @@ from .miot_error import MIoTSpecError
from .miot_storage import (
MIoTStorage,
SpecBoolTranslation,
SpecFilter)
SpecFilter,
SpecMultiLang)
_LOGGER = logging.getLogger(__name__)
class MIoTSpecValueRange:
class _MIoTSpecValueRange:
"""MIoT SPEC value range class."""
min_: int
max_: int
step: int
def __init__(self, value_range: Union[dict, list]) -> None:
if isinstance(value_range, dict):
self.load(value_range)
elif isinstance(value_range, list):
self.from_spec(value_range)
else:
raise MIoTSpecError('invalid value range format')
def load(self, value_range: dict) -> None:
if (
'min' not in value_range
or 'max' not in value_range
or 'step' not in value_range
):
raise MIoTSpecError('invalid value range')
self.min_ = value_range['min']
self.max_ = value_range['max']
self.step = value_range['step']
def from_spec(self, value_range: list) -> None:
if len(value_range) != 3:
raise MIoTSpecError('invalid value range')
def from_list(self, value_range: list) -> None:
self.min_ = value_range[0]
self.max_ = value_range[1]
self.step = value_range[2]
def dump(self) -> dict:
return {
'min': self.min_,
'max': self.max_,
'step': self.step
}
def __str__(self) -> str:
return f'[{self.min_}, {self.max_}, {self.step}'
def to_list(self) -> list:
return [self.min_, self.max_, self.step]
class MIoTSpecValueListItem:
class _MIoTSpecValueListItem:
"""MIoT SPEC value list item class."""
# NOTICE: bool type without name
# All lower-case SPEC description.
name: str
# Value
value: Any
# Descriptions after multilingual conversion.
description: str
def __init__(self, item: dict) -> None:
self.load(item)
def load(self, item: dict) -> None:
if 'value' not in item or 'description' not in item:
raise MIoTSpecError('invalid value list item, %s')
self.name = item.get('name', None)
self.value = item['value']
self.description = item['description']
@staticmethod
def from_spec(item: dict) -> 'MIoTSpecValueListItem':
if (
'name' not in item
or 'value' not in item
or 'description' not in item
):
raise MIoTSpecError('invalid value list item, %s')
# Slugify name and convert to lower-case.
cache = {
'name': slugify(text=item['name'], separator='_').lower(),
'value': item['value'],
'description': item['description']
}
return MIoTSpecValueListItem(cache)
def dump(self) -> dict:
def to_dict(self) -> dict:
return {
'name': self.name,
'value': self.value,
'description': self.description
}
def __str__(self) -> str:
return f'{self.name}: {self.value} - {self.description}'
class MIoTSpecValueList:
class _MIoTSpecValueList:
"""MIoT SPEC value list class."""
items: list[MIoTSpecValueListItem]
def __init__(self, value_list: list[dict]) -> None:
if not isinstance(value_list, list):
raise MIoTSpecError('invalid value list format')
self.items = []
self.load(value_list)
@property
def names(self) -> list[str]:
return [item.name for item in self.items]
@property
def values(self) -> list[Any]:
return [item.value for item in self.items]
@property
def descriptions(self) -> list[str]:
return [item.description for item in self.items]
@staticmethod
def from_spec(value_list: list[dict]) -> 'MIoTSpecValueList':
result = MIoTSpecValueList([])
dup_desc: dict[str, int] = {}
for item in value_list:
# Handle duplicate descriptions.
count = 0
if item['description'] in dup_desc:
count = dup_desc[item['description']]
count += 1
dup_desc[item['description']] = count
if count > 1:
item['name'] = f'{item["name"]}_{count}'
item['description'] = f'{item["description"]}_{count}'
result.items.append(MIoTSpecValueListItem.from_spec(item))
return result
def load(self, value_list: list[dict]) -> None:
for item in value_list:
self.items.append(MIoTSpecValueListItem(item))
items: list[_MIoTSpecValueListItem]
def to_map(self) -> dict:
return {item.value: item.description for item in self.items}
def get_value_by_description(self, description: str) -> Any:
for item in self.items:
if item.description == description:
return item.value
return None
def get_description_by_value(self, value: Any) -> Optional[str]:
for item in self.items:
if item.value == value:
return item.description
return None
def dump(self) -> list:
return [item.dump() for item in self.items]
def to_list(self) -> list:
return [item.to_dict() for item in self.items]
class _SpecStdLib:
@ -239,7 +132,7 @@ class _SpecStdLib:
self._spec_std_lib = None
def load(self, std_lib: dict[str, dict[str, dict[str, str]]]) -> None:
def from_dict(self, std_lib: dict[str, dict[str, dict[str, str]]]) -> None:
if (
not isinstance(std_lib, dict)
or 'devices' not in std_lib
@ -318,7 +211,7 @@ class _SpecStdLib:
async def refresh_async(self) -> bool:
std_lib_new = await self.__request_from_cloud_async()
if std_lib_new:
self.load(std_lib_new)
self.from_dict(std_lib_new)
return True
return False
@ -460,7 +353,7 @@ class _SpecStdLib:
return result
class _MIoTSpecBase:
class MIoTSpecBase:
"""MIoT SPEC base class."""
iid: int
type_: str
@ -504,32 +397,26 @@ class _MIoTSpecBase:
return self.spec_id == value.spec_id
class MIoTSpecProperty(_MIoTSpecBase):
class MIoTSpecProperty(MIoTSpecBase):
"""MIoT SPEC property class."""
unit: Optional[str]
format_: str
precision: int
unit: Optional[str]
_format_: Type
_value_range: Optional[MIoTSpecValueRange]
_value_list: Optional[MIoTSpecValueList]
value_range: Optional[list]
value_list: Optional[list[dict]]
_access: list
_writable: bool
_readable: bool
_notifiable: bool
service: 'MIoTSpecService'
service: MIoTSpecBase
def __init__(
self,
spec: dict,
service: 'MIoTSpecService',
format_: str,
access: list,
unit: Optional[str] = None,
value_range: Optional[dict] = None,
value_list: Optional[list[dict]] = None,
precision: Optional[int] = None
self, spec: dict, service: MIoTSpecBase, format_: str, access: list,
unit: Optional[str] = None, value_range: Optional[list] = None,
value_list: Optional[list[dict]] = None, precision: int = 0
) -> None:
super().__init__(spec=spec)
self.service = service
@ -538,24 +425,11 @@ class MIoTSpecProperty(_MIoTSpecBase):
self.unit = unit
self.value_range = value_range
self.value_list = value_list
self.precision = precision or 1
self.precision = precision
self.spec_id = hash(
f'p.{self.name}.{self.service.iid}.{self.iid}')
@property
def format_(self) -> Type:
return self._format_
@format_.setter
def format_(self, value: str) -> None:
self._format_ = {
'string': str,
'str': str,
'bool': bool,
'float': float}.get(
value, int)
@property
def access(self) -> list:
return self._access
@ -580,46 +454,15 @@ class MIoTSpecProperty(_MIoTSpecBase):
def notifiable(self):
return self._notifiable
@property
def value_range(self) -> Optional[MIoTSpecValueRange]:
return self._value_range
@value_range.setter
def value_range(self, value: Union[dict, list, None]) -> None:
"""Set value-range, precision."""
if not value:
self._value_range = None
return
self._value_range = MIoTSpecValueRange(value_range=value)
if isinstance(value, list):
self.precision = len(str(value[2]).split(
'.')[1].rstrip('0')) if '.' in str(value[2]) else 0
@property
def value_list(self) -> Optional[MIoTSpecValueList]:
return self._value_list
@value_list.setter
def value_list(
self, value: Union[list[dict], MIoTSpecValueList, None]
) -> None:
if not value:
self._value_list = None
return
if isinstance(value, list):
self._value_list = MIoTSpecValueList(value_list=value)
elif isinstance(value, MIoTSpecValueList):
self._value_list = value
def value_format(self, value: Any) -> Any:
if value is None:
return None
if self.format_ == int:
if self.format_ == 'int':
return int(value)
if self.format_ == float:
if self.format_ == 'float':
return round(value, self.precision)
if self.format_ == bool:
return bool(value in [True, 1, 'True', 'true', '1'])
if self.format_ == 'bool':
return bool(value in [True, 1, 'true', '1'])
return value
def dump(self) -> dict:
@ -631,27 +474,26 @@ class MIoTSpecProperty(_MIoTSpecBase):
'description_trans': self.description_trans,
'proprietary': self.proprietary,
'need_filter': self.need_filter,
'format': self.format_.__name__,
'format': self.format_,
'access': self._access,
'unit': self.unit,
'value_range': (
self._value_range.dump() if self._value_range else None),
'value_list': self._value_list.dump() if self._value_list else None,
'value_range': self.value_range,
'value_list': self.value_list,
'precision': self.precision
}
class MIoTSpecEvent(_MIoTSpecBase):
class MIoTSpecEvent(MIoTSpecBase):
"""MIoT SPEC event class."""
argument: list[MIoTSpecProperty]
service: 'MIoTSpecService'
service: MIoTSpecBase
def __init__(
self, spec: dict, service: 'MIoTSpecService',
argument: Optional[list[MIoTSpecProperty]] = None
self, spec: dict, service: MIoTSpecBase,
argument: list[MIoTSpecProperty] = None
) -> None:
super().__init__(spec=spec)
self.argument = argument or []
self.argument = argument
self.service = service
self.spec_id = hash(
@ -665,25 +507,25 @@ class MIoTSpecEvent(_MIoTSpecBase):
'description': self.description,
'description_trans': self.description_trans,
'proprietary': self.proprietary,
'need_filter': self.need_filter,
'argument': [prop.iid for prop in self.argument],
'need_filter': self.need_filter
}
class MIoTSpecAction(_MIoTSpecBase):
class MIoTSpecAction(MIoTSpecBase):
"""MIoT SPEC action class."""
in_: list[MIoTSpecProperty]
out: list[MIoTSpecProperty]
service: 'MIoTSpecService'
service: MIoTSpecBase
def __init__(
self, spec: dict, service: 'MIoTSpecService',
in_: Optional[list[MIoTSpecProperty]] = None,
out: Optional[list[MIoTSpecProperty]] = None
self, spec: dict, service: MIoTSpecBase = None,
in_: list[MIoTSpecProperty] = None,
out: list[MIoTSpecProperty] = None
) -> None:
super().__init__(spec=spec)
self.in_ = in_ or []
self.out = out or []
self.in_ = in_
self.out = out
self.service = service
self.spec_id = hash(
@ -696,14 +538,14 @@ class MIoTSpecAction(_MIoTSpecBase):
'iid': self.iid,
'description': self.description,
'description_trans': self.description_trans,
'in': [prop.iid for prop in self.in_],
'out': [prop.iid for prop in self.out],
'proprietary': self.proprietary,
'need_filter': self.need_filter
'need_filter': self.need_filter,
'in': [prop.iid for prop in self.in_],
'out': [prop.iid for prop in self.out]
}
class MIoTSpecService(_MIoTSpecBase):
class MIoTSpecService(MIoTSpecBase):
"""MIoT SPEC service class."""
properties: list[MIoTSpecProperty]
events: list[MIoTSpecEvent]
@ -724,9 +566,9 @@ class MIoTSpecService(_MIoTSpecBase):
'description_trans': self.description_trans,
'proprietary': self.proprietary,
'properties': [prop.dump() for prop in self.properties],
'need_filter': self.need_filter,
'events': [event.dump() for event in self.events],
'actions': [action.dump() for action in self.actions],
'need_filter': self.need_filter
}
@ -746,7 +588,8 @@ class MIoTSpecInstance:
icon: str
def __init__(
self, urn: str, name: str, description: str, description_trans: str
self, urn: str = None, name: str = None,
description: str = None, description_trans: str = None
) -> None:
self.urn = urn
self.name = name
@ -754,13 +597,12 @@ class MIoTSpecInstance:
self.description_trans = description_trans
self.services = []
@staticmethod
def load(specs: dict) -> 'MIoTSpecInstance':
instance = MIoTSpecInstance(
urn=specs['urn'],
name=specs['name'],
description=specs['description'],
description_trans=specs['description_trans'])
def load(self, specs: dict) -> 'MIoTSpecInstance':
self.urn = specs['urn']
self.name = specs['name']
self.description = specs['description']
self.description_trans = specs['description_trans']
self.services = []
for service in specs['services']:
spec_service = MIoTSpecService(spec=service)
for prop in service['properties']:
@ -772,7 +614,7 @@ class MIoTSpecInstance:
unit=prop['unit'],
value_range=prop['value_range'],
value_list=prop['value_list'],
precision=prop.get('precision', None))
precision=prop.get('precision', 0))
spec_service.properties.append(spec_prop)
for event in service['events']:
spec_event = MIoTSpecEvent(
@ -803,8 +645,8 @@ class MIoTSpecInstance:
break
spec_action.out = out_list
spec_service.actions.append(spec_action)
instance.services.append(spec_service)
return instance
self.services.append(spec_service)
return self
def dump(self) -> dict:
return {
@ -816,130 +658,21 @@ class MIoTSpecInstance:
}
class _MIoTSpecMultiLang:
"""MIoT SPEC multi lang class."""
# pylint: disable=broad-exception-caught
_DOMAIN: str = 'miot_specs_multi_lang'
_lang: str
_storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop
_custom_cache: dict[str, dict]
_current_data: Optional[dict[str, str]]
def __init__(
self, lang: Optional[str],
storage: MIoTStorage,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE
self._storage = storage
self._main_loop = loop or asyncio.get_running_loop()
self._custom_cache = {}
self._current_data = None
async def set_spec_async(self, urn: str) -> None:
if urn in self._custom_cache:
self._current_data = self._custom_cache[urn]
return
trans_cache: dict[str, str] = {}
trans_cloud: dict = {}
trans_local: dict = {}
# Get multi lang from cloud
try:
trans_cloud = await self.__get_multi_lang_async(urn)
if self._lang == 'zh-Hans':
# Simplified Chinese
trans_cache = trans_cloud.get('zh_cn', {})
elif self._lang == 'zh-Hant':
# Traditional Chinese, zh_hk or zh_tw
trans_cache = trans_cloud.get('zh_hk', {})
if not trans_cache:
trans_cache = trans_cloud.get('zh_tw', {})
else:
trans_cache = trans_cloud.get(self._lang, {})
except Exception as err:
trans_cloud = {}
_LOGGER.info('get multi lang from cloud failed, %s, %s', urn, err)
# Get multi lang from local
try:
trans_local = await self._storage.load_async(
domain=self._DOMAIN, name=urn, type_=dict) # type: ignore
if (
isinstance(trans_local, dict)
and self._lang in trans_local
):
trans_cache.update(trans_local[self._lang])
except Exception as err:
trans_local = {}
_LOGGER.info('get multi lang from local failed, %s, %s', urn, err)
# Default language
if not trans_cache:
if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
trans_cache = trans_cloud[DEFAULT_INTEGRATION_LANGUAGE]
if trans_local and DEFAULT_INTEGRATION_LANGUAGE in trans_local:
trans_cache.update(
trans_local[DEFAULT_INTEGRATION_LANGUAGE])
trans_data: dict[str, str] = {}
for tag, value in trans_cache.items():
if value is None or value.strip() == '':
continue
# The dict key is like:
# 'service:002:property:001:valuelist:000' or
# 'service:002:property:001' or 'service:002'
strs: list = tag.split(':')
strs_len = len(strs)
if strs_len == 2:
trans_data[f's:{int(strs[1])}'] = value
elif strs_len == 4:
type_ = 'p' if strs[2] == 'property' else (
'a' if strs[2] == 'action' else 'e')
trans_data[
f'{type_}:{int(strs[1])}:{int(strs[3])}'
] = value
elif strs_len == 6:
trans_data[
f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}'
] = value
self._custom_cache[urn] = trans_data
self._current_data = trans_data
def translate(self, key: str) -> Optional[str]:
if not self._current_data:
return None
return self._current_data.get(key, None)
async def __get_multi_lang_async(self, urn: str) -> dict:
res_trans = await MIoTHttp.get_json_async(
url='https://miot-spec.org/instance/v2/multiLanguage',
params={'urn': urn})
if (
not isinstance(res_trans, dict)
or 'data' not in res_trans
or not isinstance(res_trans['data'], dict)
):
raise MIoTSpecError('invalid translation data')
return res_trans['data']
class MIoTSpecParser:
"""MIoT SPEC parser."""
# pylint: disable=inconsistent-quotes
VERSION: int = 1
_DOMAIN: str = 'miot_specs'
DOMAIN: str = 'miot_specs'
_lang: str
_storage: MIoTStorage
_main_loop: asyncio.AbstractEventLoop
_std_lib: _SpecStdLib
_multi_lang: _MIoTSpecMultiLang
_init_done: bool
_ram_cache: dict
_std_lib: _SpecStdLib
_bool_trans: SpecBoolTranslation
_multi_lang: SpecMultiLang
_spec_filter: SpecFilter
def __init__(
@ -950,23 +683,24 @@ class MIoTSpecParser:
self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE
self._storage = storage
self._main_loop = loop or asyncio.get_running_loop()
self._std_lib = _SpecStdLib(lang=self._lang)
self._multi_lang = _MIoTSpecMultiLang(
lang=self._lang, storage=self._storage, loop=self._main_loop)
self._init_done = False
self._ram_cache = {}
self._std_lib = _SpecStdLib(lang=self._lang)
self._bool_trans = SpecBoolTranslation(
lang=self._lang, loop=self._main_loop)
self._multi_lang = SpecMultiLang(lang=self._lang, loop=self._main_loop)
self._spec_filter = SpecFilter(loop=self._main_loop)
async def init_async(self) -> None:
if self._init_done is True:
return
await self._bool_trans.init_async()
await self._multi_lang.init_async()
await self._spec_filter.init_async()
std_lib_cache = await self._storage.load_async(
domain=self._DOMAIN, name='spec_std_lib', type_=dict)
domain=self.DOMAIN, name='spec_std_lib', type_=dict)
if (
isinstance(std_lib_cache, dict)
and 'data' in std_lib_cache
@ -978,13 +712,13 @@ class MIoTSpecParser:
# Use the cache if the update time is less than 14 day
_LOGGER.debug(
'use local spec std cache, ts->%s', std_lib_cache['ts'])
self._std_lib.load(std_lib_cache['data'])
self._std_lib.from_dict(std_lib_cache['data'])
self._init_done = True
return
# Update spec std lib
if await self._std_lib.refresh_async():
if not await self._storage.save_async(
domain=self._DOMAIN, name='spec_std_lib',
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
@ -993,7 +727,7 @@ class MIoTSpecParser:
_LOGGER.error('save spec std lib failed')
else:
if isinstance(std_lib_cache, dict) and 'data' in std_lib_cache:
self._std_lib.load(std_lib_cache['data'])
self._std_lib.from_dict(std_lib_cache['data'])
_LOGGER.info('get spec std lib failed, use local cache')
else:
_LOGGER.error('load spec std lib failed')
@ -1003,17 +737,19 @@ class MIoTSpecParser:
self._init_done = False
# self._std_lib.deinit()
await self._bool_trans.deinit_async()
await self._multi_lang.deinit_async()
await self._spec_filter.deinit_async()
self._ram_cache.clear()
async def parse(
self, urn: str, skip_cache: bool = False,
) -> Optional[MIoTSpecInstance]:
) -> MIoTSpecInstance:
"""MUST await init first !!!"""
if not skip_cache:
cache_result = await self.__cache_get(urn=urn)
if isinstance(cache_result, dict):
_LOGGER.debug('get from cache, %s', urn)
return MIoTSpecInstance.load(specs=cache_result)
return MIoTSpecInstance().load(specs=cache_result)
# Retry three times
for index in range(3):
try:
@ -1029,7 +765,7 @@ class MIoTSpecParser:
return False
if await self._std_lib.refresh_async():
if not await self._storage.save_async(
domain=self._DOMAIN, name='spec_std_lib',
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
@ -1048,24 +784,38 @@ class MIoTSpecParser:
return success_count
async def __cache_get(self, urn: str) -> Optional[dict]:
if platform.system() == 'Windows':
urn = urn.replace(':', '_')
return await self._storage.load_async(
domain=self._DOMAIN,
name=f'{urn}_{self._lang}',
type_=dict) # type: ignore
if self._storage is not None:
if platform.system() == 'Windows':
urn = urn.replace(':', '_')
return await self._storage.load_async(
domain=self.DOMAIN, name=f'{urn}_{self._lang}', type_=dict)
return self._ram_cache.get(urn, None)
async def __cache_set(self, urn: str, data: dict) -> bool:
if platform.system() == 'Windows':
urn = urn.replace(':', '_')
return await self._storage.save_async(
domain=self._DOMAIN, name=f'{urn}_{self._lang}', data=data)
if self._storage is not None:
if platform.system() == 'Windows':
urn = urn.replace(':', '_')
return await self._storage.save_async(
domain=self.DOMAIN, name=f'{urn}_{self._lang}', data=data)
self._ram_cache[urn] = data
return True
def __spec_format2dtype(self, format_: str) -> str:
# 'string'|'bool'|'uint8'|'uint16'|'uint32'|
# 'int8'|'int16'|'int32'|'int64'|'float'
return {'string': 'str', 'bool': 'bool', 'float': 'float'}.get(
format_, 'int')
async def __get_instance(self, urn: str) -> Optional[dict]:
return await MIoTHttp.get_json_async(
url='https://miot-spec.org/miot-spec-v2/instance',
params={'type': urn})
async def __get_translation(self, urn: str) -> Optional[dict]:
return await MIoTHttp.get_json_async(
url='https://miot-spec.org/instance/v2/multiLanguage',
params={'urn': urn})
async def __parse(self, urn: str) -> MIoTSpecInstance:
_LOGGER.debug('parse urn, %s', urn)
# Load spec instance
@ -1077,11 +827,68 @@ class MIoTSpecParser:
or 'services' not in instance
):
raise MIoTSpecError(f'invalid urn instance, {urn}')
translation: dict = {}
urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6])
# Set translation cache
await self._multi_lang.set_spec_async(urn=urn_key)
# Set spec filter
try:
# Load multiple language configuration.
res_trans = await self.__get_translation(urn=urn)
if (
not isinstance(res_trans, dict)
or 'data' not in res_trans
or not isinstance(res_trans['data'], dict)
):
raise MIoTSpecError('invalid translation data')
trans_data: dict[str, str] = {}
if self._lang == 'zh-Hans':
# Simplified Chinese
trans_data = res_trans['data'].get('zh_cn', {})
elif self._lang == 'zh-Hant':
# Traditional Chinese, zh_hk or zh_tw
trans_data = res_trans['data'].get('zh_hk', {})
if not trans_data:
trans_data = res_trans['data'].get('zh_tw', {})
else:
trans_data = res_trans['data'].get(self._lang, {})
# Load local multiple language configuration.
multi_lang: dict = await self._multi_lang.translate_async(
urn_key=urn_key)
if multi_lang:
trans_data.update(multi_lang)
if not trans_data:
trans_data = res_trans['data'].get(
DEFAULT_INTEGRATION_LANGUAGE, {})
if not trans_data:
raise MIoTSpecError(
f'the language is not supported, {self._lang}')
else:
_LOGGER.error(
'the language is not supported, %s, try using the '
'default language, %s, %s',
self._lang, DEFAULT_INTEGRATION_LANGUAGE, urn)
for tag, value in trans_data.items():
if value is None or value.strip() == '':
continue
# The dict key is like:
# 'service:002:property:001:valuelist:000' or
# 'service:002:property:001' or 'service:002'
strs: list = tag.split(':')
strs_len = len(strs)
if strs_len == 2:
translation[f's:{int(strs[1])}'] = value
elif strs_len == 4:
type_ = 'p' if strs[2] == 'property' else (
'a' if strs[2] == 'action' else 'e')
translation[
f'{type_}:{int(strs[1])}:{int(strs[3])}'
] = value
elif strs_len == 6:
translation[
f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}'
] = value
except MIoTSpecError as e:
_LOGGER.error('get translation error, %s, %s', urn, e)
# Spec filter
self._spec_filter.filter_spec(urn_key=urn_key)
# Parse device type
spec_instance: MIoTSpecInstance = MIoTSpecInstance(
@ -1112,7 +919,7 @@ class MIoTSpecParser:
if type_strs[1] != 'miot-spec-v2':
spec_service.proprietary = True
spec_service.description_trans = (
self._multi_lang.translate(f's:{service["iid"]}')
translation.get(f's:{service["iid"]}', None)
or self._std_lib.service_translate(key=':'.join(type_strs[:5]))
or service['description']
or spec_service.name
@ -1131,7 +938,7 @@ class MIoTSpecParser:
spec_prop: MIoTSpecProperty = MIoTSpecProperty(
spec=property_,
service=spec_service,
format_=property_['format'],
format_=self.__spec_format2dtype(property_['format']),
access=property_['access'],
unit=property_.get('unit', None))
spec_prop.name = p_type_strs[3]
@ -1143,33 +950,41 @@ class MIoTSpecParser:
if p_type_strs[1] != 'miot-spec-v2':
spec_prop.proprietary = spec_service.proprietary or True
spec_prop.description_trans = (
self._multi_lang.translate(
f'p:{service["iid"]}:{property_["iid"]}')
translation.get(
f'p:{service["iid"]}:{property_["iid"]}', None)
or self._std_lib.property_translate(
key=':'.join(p_type_strs[:5]))
or property_['description']
or spec_prop.name)
if 'value-range' in property_:
spec_prop.value_range = property_['value-range']
spec_prop.value_range = {
'min': property_['value-range'][0],
'max': property_['value-range'][1],
'step': property_['value-range'][2]
}
spec_prop.precision = len(str(
property_['value-range'][2]).split(
'.')[1].rstrip('0')) if '.' in str(
property_['value-range'][2]) else 0
elif 'value-list' in property_:
v_list: list[dict] = property_['value-list']
for index, v in enumerate(v_list):
v['name'] = v['description']
v['description'] = (
self._multi_lang.translate(
translation.get(
f'v:{service["iid"]}:{property_["iid"]}:'
f'{index}')
f'{index}', None)
or self._std_lib.value_translate(
key=f'{type_strs[:5]}|{p_type_strs[3]}|'
f'{v["description"]}')
or v['name'])
spec_prop.value_list = MIoTSpecValueList.from_spec(v_list)
or v['name']
)
spec_prop.value_list = v_list
elif property_['format'] == 'bool':
v_tag = ':'.join(p_type_strs[:5])
v_descriptions = (
v_descriptions: dict = (
await self._bool_trans.translate_async(urn=v_tag))
if v_descriptions:
# bool without value-list.name
spec_prop.value_list = v_descriptions
spec_service.properties.append(spec_prop)
# Parse service event
@ -1193,8 +1008,8 @@ class MIoTSpecParser:
if e_type_strs[1] != 'miot-spec-v2':
spec_event.proprietary = spec_service.proprietary or True
spec_event.description_trans = (
self._multi_lang.translate(
f'e:{service["iid"]}:{event["iid"]}')
translation.get(
f'e:{service["iid"]}:{event["iid"]}', None)
or self._std_lib.event_translate(
key=':'.join(e_type_strs[:5]))
or event['description']
@ -1229,8 +1044,8 @@ class MIoTSpecParser:
if a_type_strs[1] != 'miot-spec-v2':
spec_action.proprietary = spec_service.proprietary or True
spec_action.description_trans = (
self._multi_lang.translate(
f'a:{service["iid"]}:{action["iid"]}')
translation.get(
f'a:{service["iid"]}:{action["iid"]}', None)
or self._std_lib.action_translate(
key=':'.join(a_type_strs[:5]))
or action['description']

View File

@ -719,6 +719,60 @@ 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_.__name__})'
f'{prop.description_trans}({prop.format_})'
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,8 +82,7 @@ class Select(MIoTPropertyEntity, SelectEntity):
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
"""Initialize the Select."""
super().__init__(miot_device=miot_device, spec=spec)
if self._value_list:
self._attr_options = self._value_list.descriptions
self._attr_options = list(self._value_list.values())
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 = self._value_list.descriptions
self._attr_options = list(self._value_list.values())
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.get_vlist_description(self._value)
return self._value_list.get(self._value, None)
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_.__name__})'
f'{prop.description_trans}({prop.format_})'
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

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration."
},
"abort": {
"ha_uuid_get_failed": "Fehler beim Abrufen der Home Assistant-UUID.",
"network_connect_error": "Konfiguration fehlgeschlagen. Netzwerkverbindung fehlgeschlagen. Überprüfen Sie die Netzwerkkonfiguration des Geräts.",
"already_configured": "Dieser Benutzer hat die Konfiguration bereits abgeschlossen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Konfiguration zu ändern.",
"invalid_auth_info": "Authentifizierungsinformationen sind abgelaufen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Authentifizierung erneut durchzuführen.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Unable to reach Xiaomi MQTT Broker address, please check network configuration."
},
"abort": {
"ha_uuid_get_failed": "Failed to get Home Assistant UUID.",
"network_connect_error": "Configuration failed. The network connection is abnormal. Please check the equipment network configuration.",
"already_configured": "Configuration for this user is already completed. Please go to the integration page and click the CONFIGURE button for modifications.",
"invalid_auth_info": "Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "No se puede acceder a la dirección del Broker MQTT de Xiaomi, por favor verifique la configuración de la red."
},
"abort": {
"ha_uuid_get_failed": "Error al obtener el UUID de Home Assistant.",
"network_connect_error": "La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.",
"already_configured": "Esta cuenta ya ha finalizado la configuración. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para modificar la configuración.",
"invalid_auth_info": "La información de autorización ha caducado. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para volver a autenticarse.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Impossible d'atteindre l'adresse du Broker MQTT de Xiaomi, veuillez vérifier la configuration réseau."
},
"abort": {
"ha_uuid_get_failed": "Échec de l'obtention de l'UUID de Home Assistant.",
"network_connect_error": "La configuration a échoué. Erreur de connexion réseau. Veuillez vérifier la configuration du réseau de l'appareil.",
"already_configured": "Cet utilisateur a déjà terminé la configuration. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour modifier la configuration.",
"invalid_auth_info": "Les informations d'authentification ont expiré. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour vous authentifier à nouveau.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Xiaomi MQTT ブローカーアドレスにアクセスできません。ネットワーク設定を確認してください。"
},
"abort": {
"ha_uuid_get_failed": "Home Assistant インスタンスIDを取得できませんでした。",
"network_connect_error": "設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。",
"already_configured": "このユーザーはすでに設定が完了しています。統合ページにアクセスして、「設定」ボタンをクリックして設定を変更してください。",
"invalid_auth_info": "認証情報が期限切れになりました。統合ページにアクセスして、「設定」ボタンをクリックして再度認証してください。",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie."
},
"abort": {
"ha_uuid_get_failed": "Mislukt bij het ophalen van Home Assistant UUID.",
"network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.",
"already_configured": "Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.",
"invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
},
"abort": {
"ha_uuid_get_failed": "Falha ao obter o UUID do Home Assistant.",
"network_connect_error": "Configuração falhou. A conexão de rede está anormal. Verifique a configuração de rede do equipamento.",
"already_configured": "A configuração para este usuário já foi concluída. Vá para a página de integrações e clique no botão CONFIGURAR para modificações.",
"invalid_auth_info": "As informações de autenticação expiraram. Vá para a página de integrações e clique em CONFIGURAR para reautenticar.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
},
"abort": {
"ha_uuid_get_failed": "Não foi possível obter o UUID do Home Assistant.",
"network_connect_error": "A configuração falhou. A ligação de rede é anormal. Verifique a configuração de rede do equipamento.",
"already_configured": "A configuração para este utilizador já foi concluída. Vá à página de integrações e clique em CONFIGURAR para efetuar alterações.",
"invalid_auth_info": "A informação de autenticação expirou. Vá à página de integrações e clique em CONFIGURAR para reautenticar.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети."
},
"abort": {
"ha_uuid_get_failed": "Не удалось получить UUID Home Assistant.",
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "无法访问小米 MQTT Broker 地址,请检查网络配置。"
},
"abort": {
"ha_uuid_get_failed": "获取 Home Assistant UUID 失败。",
"network_connect_error": "配置失败。网络连接异常,请检查设备网络配置。",
"already_configured": "该用户已配置完成。请进入集成页面,点击“配置”按钮修改配置。",
"invalid_auth_info": "认证信息已过期。请进入集成页面,点击“配置”按钮重新认证。",

View File

@ -90,7 +90,6 @@
"unreachable_mqtt_broker": "無法訪問小米 MQTT Broker 地址,請檢查網絡配置。"
},
"abort": {
"ha_uuid_get_failed": "獲取 Home Assistant UUID 失敗。",
"network_connect_error": "配置失敗。網絡連接異常,請檢查設備網絡配置。",
"already_configured": "該用戶已配置完成。請進入集成頁面,點擊“配置”按鈕修改配置。",
"invalid_auth_info": "認證信息已過期。請進入集成頁面,點擊“配置”按鈕重新認證。",

View File

@ -120,18 +120,28 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
# properties
for prop in entity_data.props:
if prop.name == 'status':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'invalid status value_list, %s', self.entity_id)
continue
self._status_map = prop.value_list.to_map()
self._status_map = {
item['value']: item['description']
for item in prop.value_list}
self._prop_status = prop
elif prop.name == 'fan-level':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
continue
self._fan_level_map = prop.value_list.to_map()
self._fan_level_map = {
item['value']: item['description']
for item in prop.value_list}
self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop
@ -192,7 +202,7 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
return self.get_map_value(
return self.get_map_description(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
@ -204,6 +214,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_value(
return self.get_map_description(
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_map: Optional[dict[Any, Any]]
_mode_list: 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_map = None
self._mode_list = 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 prop.value_range:
if isinstance(prop.value_range, dict):
if (
self._attr_temperature_unit is None
and prop.external_unit
@ -128,14 +128,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self.entity_id)
# target-temperature
if prop.name == 'target-temperature':
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
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 |= (
@ -143,12 +138,17 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_target_temp = prop
# mode
if prop.name == 'mode':
if not prop.value_list:
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values())
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_operation_list = list(self._mode_list.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop
@ -184,9 +184,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
prop=self._prop_on, value=True, update=False)
await self.set_property_async(
prop=self._prop_mode,
value=self.get_map_key(
map_=self._mode_map,
value=operation_mode))
value=self.__get_mode_value(description=operation_mode))
async def async_turn_away_mode_on(self) -> None:
"""Set the water heater to away mode."""
@ -209,6 +207,20 @@ 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_map_value(
map_=self._mode_map,
return self.__get_mode_description(
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