Compare commits

...

15 Commits

Author SHA1 Message Date
Paul Shawn
d53250366e
Merge 60e57e863d into 152933a223 2025-01-10 02:33:59 +00:00
topsworld
60e57e863d fix: fix fan entity 2025-01-10 10:32:20 +08:00
topsworld
792d70f2ba merge: merge main to this branch 2025-01-10 10:14:01 +08:00
Paul Shawn
152933a223
docs: update changelog and version to v0.1.5b1 (#616)
Some checks are pending
Tests / check-rule-format (push) Waiting to run
Validate / validate-hassfest (push) Waiting to run
Validate / validate-hacs (push) Waiting to run
Validate / validate-lint (push) Waiting to run
Validate / validate-setup (push) Waiting to run
2025-01-10 09:46:34 +08:00
Paul Shawn
6557b22a52
fix: fix multi ha instance login (#560)
* fix: fix multi ha instance login

* fix: fix option flow oauth
2025-01-10 09:19:24 +08:00
topsworld
fca97e03f0 fix: fix miot cloud log error 2025-01-09 15:04:45 +08:00
topsworld
93f04b1aee feat: update prop.format_ logic 2025-01-09 15:04:23 +08:00
topsworld
d25d3f6a93 feat: update value-list logic 2025-01-09 13:23:35 +08:00
topsworld
3399e3bb20 feat: update entity value-range 2025-01-08 19:29:39 +08:00
topsworld
078adfbd4c feat: update std_lib and multi_lang logic 2025-01-08 15:23:31 +08:00
topsworld
e4dfdf68ab feat: remove spec cache storage 2025-01-08 11:00:41 +08:00
Paul Shawn
5d4b975f85
fix: the number of profile models updated from 660 to 823 (#583)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-01-07 20:22:06 +08:00
Paul Shawn
0566546a99
feat: filter miwifi.* devices (#564)
* feat: filter miwifi.* devices

* feat: update log level

* feat: filter special xiaomi router model, xiaomi.router.rd03
2025-01-07 20:21:43 +08:00
Paul Shawn
c0d100ce2b
feat: fan entity support direction ctrl (#556)
* feat: fan entity support direction

* fix: fix value judgement logic
2025-01-07 20:21:24 +08:00
Li Shuzhen
ce7ce7af4b
fix: fan speed (#464)
* fix: fan speed

* fix: fan speed names map

* fix: set percentage

* docs: the instance code format of valuelist

* fix: fan level property

* fix: pylint too long line.

* style: code format

---------

Co-authored-by: topsworld <sworldtop@gmail.com>
2025-01-07 20:21:04 +08:00
35 changed files with 1054 additions and 574 deletions

View File

@ -1,5 +1,16 @@
# CHANGELOG # 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 ## v0.1.5b0
### Added ### Added
- Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101) - Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101)

View File

@ -351,7 +351,7 @@ The instance code is the code of the MIoT-Spec-V2 instance, which is in the form
``` ```
service:<siid> # service service:<siid> # service
service:<siid>:property:<piid> # property service:<siid>:property:<piid> # property
service:<siid>:property:<piid>:valuelist:<value> # the value in value-list of a property service:<siid>:property:<piid>:valuelist:<index> # The index of a value in the value-list of a property
service:<siid>:event:<eiid> # event service:<siid>:event:<eiid> # event
service:<siid>:action:<aiid> # action service:<siid>:action:<aiid> # action
``` ```

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

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

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

@ -55,7 +55,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.util.percentage import ( from homeassistant.util.percentage import (
percentage_to_ranged_value, percentage_to_ranged_value,
ranged_value_to_percentage ranged_value_to_percentage,
ordered_list_item_to_percentage,
percentage_to_ordered_list_item
) )
from .miot.miot_spec import MIoTSpecProperty from .miot.miot_spec import MIoTSpecProperty
@ -85,15 +87,20 @@ 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]
_prop_wind_reverse: Optional[MIoTSpecProperty]
_prop_wind_reverse_forward: Any
_prop_wind_reverse_reverse: Any
_speed_min: Optional[int] _speed_min: int
_speed_max: Optional[int] _speed_max: int
_speed_step: Optional[int] _speed_step: int
_mode_list: Optional[dict[Any, Any]] _speed_names: Optional[list]
_speed_name_map: Optional[dict[int, str]]
_mode_map: Optional[dict[Any, Any]]
def __init__( def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -101,16 +108,23 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Initialize the Fan.""" """Initialize the Fan."""
super().__init__(miot_device=miot_device, entity_data=entity_data) super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_preset_modes = [] self._attr_preset_modes = []
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
self._prop_wind_reverse = None
self._prop_wind_reverse_forward = None
self._prop_wind_reverse_reverse = None
self._speed_min = 65535 self._speed_min = 65535
self._speed_max = 0 self._speed_max = 0
self._speed_step = 1 self._speed_step = 1
self._mode_list = None self._speed_names = []
self._speed_name_map = {}
self._mode_map = None
# properties # properties
for prop in entity_data.props: for prop in entity_data.props:
@ -119,59 +133,62 @@ 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 = self._speed_max - self._speed_min+1 self._attr_speed_count = int((
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
for item in prop.value_list: # Fan level with value-range is prior to fan level with
self._speed_min = min(self._speed_min, item['value']) # value-list when a fan has both fan level properties.
self._speed_max = max(self._speed_max, item['value']) self._speed_name_map = prop.value_list.to_map()
self._attr_speed_count = self._speed_max - self._speed_min+1 self._speed_names = list(self._speed_name_map.values())
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':
self._attr_supported_features |= FanEntityFeature.OSCILLATE self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._prop_horizontal_swing = prop self._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse':
def __get_mode_description(self, key: int) -> Optional[str]: if prop.format_ == 'bool':
if self._mode_list is None: self._prop_wind_reverse_forward = False
return None self._prop_wind_reverse_reverse = True
return self._mode_list.get(key, None) elif prop.value_list:
for item in prop.value_list.items:
def __get_mode_value(self, description: str) -> Optional[int]: if item.name in {'foreward'}:
if self._mode_list is None: self._prop_wind_reverse_forward = item.value
return None self._prop_wind_reverse_reverse = item.value
for key, value in self._mode_list.items(): if (
if value == description: self._prop_wind_reverse_forward is None
return key or self._prop_wind_reverse_reverse is None
return None ):
# NOTICE: Value may be 0 or False
_LOGGER.info(
'invalid wind-reverse, %s', self.entity_id)
continue
self._attr_supported_features |= FanEntityFeature.DIRECTION
self._prop_wind_reverse = prop
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.
@ -182,14 +199,25 @@ class Fan(MIoTServiceEntity, FanEntity):
await self.set_property_async(prop=self._prop_on, value=True) await self.set_property_async(prop=self._prop_on, value=True)
# percentage # percentage
if percentage: if percentage:
await self.set_property_async( if self._speed_names:
prop=self._prop_fan_level, await self.set_property_async(
value=int(percentage*self._attr_speed_count/100)) prop=self._prop_fan_level,
value=self.get_map_value(
map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
# preset_mode # preset_mode
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."""
@ -202,11 +230,19 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan speed.""" """Set the percentage of the fan speed."""
if percentage > 0: if percentage > 0:
await self.set_property_async( if self._speed_names:
prop=self._prop_fan_level, await self.set_property_async(
value=int(percentage_to_ranged_value( prop=self._prop_fan_level,
low_high_range=(self._speed_min, self._speed_max), value=self.get_map_value(
percentage=percentage))) map_=self._speed_name_map,
key=percentage_to_ordered_list_item(
self._speed_names, percentage)))
else:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
if not self.is_on: if not self.is_on:
# If the fan is off, turn it on. # If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True) await self.set_property_async(prop=self._prop_on, value=True)
@ -217,10 +253,19 @@ 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."""
if not self._prop_wind_reverse:
return
await self.set_property_async(
prop=self._prop_wind_reverse,
value=(
self._prop_wind_reverse_reverse
if self.current_direction == 'reverse'
else self._prop_wind_reverse_forward))
async def async_oscillate(self, oscillating: bool) -> None: async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan.""" """Oscillate the fan."""
@ -238,17 +283,33 @@ 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)
@property
def current_direction(self) -> Optional[str]:
"""Return the current direction of the fan."""
if not self._prop_wind_reverse:
return None
return 'reverse' if self.get_prop_value(
prop=self._prop_wind_reverse
) == self._prop_wind_reverse_reverse else 'forward'
@property @property
def percentage(self) -> Optional[int]: def percentage(self) -> Optional[int]:
"""Return the current percentage of the fan speed.""" """Return the current percentage of the fan speed."""
fan_level = self.get_prop_value(prop=self._prop_fan_level) fan_level = self.get_prop_value(prop=self._prop_fan_level)
return ranged_value_to_percentage( if fan_level is None:
low_high_range=(self._speed_min, self._speed_max), return None
value=fan_level) if fan_level else None if self._speed_names:
return ordered_list_item_to_percentage(
self._speed_names, self._speed_name_map[fan_level])
else:
return ranged_value_to_percentage(
low_high_range=(self._speed_min, self._speed_max),
value=fan_level)
@property @property
def oscillating(self) -> Optional[bool]: def oscillating(self) -> Optional[bool]:
@ -257,8 +318,3 @@ class Fan(MIoTServiceEntity, FanEntity):
self.get_prop_value( self.get_prop_value(
prop=self._prop_horizontal_swing) prop=self._prop_horizontal_swing)
if self._prop_horizontal_swing else None) if self._prop_horizontal_swing else None)
@property
def percentage_step(self) -> float:
"""Return the step of the fan speed."""
return self._speed_step

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

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

View File

@ -18,6 +18,10 @@
ts: 1603967572 ts: 1603967572
1245.airpurifier.dl01: 1245.airpurifier.dl01:
ts: 1607502661 ts: 1607502661
17216.magic_touch.d150:
ts: 1575097876
17216.magic_touch.d152:
ts: 1575097876
17216.massage.ec1266a: 17216.massage.ec1266a:
ts: 1615881124 ts: 1615881124
397.light.hallight: 397.light.hallight:
@ -56,6 +60,10 @@ bj352.airmonitor.m30:
ts: 1686644541 ts: 1686644541
bj352.waterpuri.s100cm: bj352.waterpuri.s100cm:
ts: 1615795630 ts: 1615795630
bymiot.gateway.v1:
ts: 1575097876
bymiot.gateway.v2:
ts: 1575097876
cgllc.airmonitor.b1: cgllc.airmonitor.b1:
ts: 1676339912 ts: 1676339912
cgllc.airmonitor.s1: cgllc.airmonitor.s1:
@ -64,6 +72,8 @@ cgllc.clock.cgc1:
ts: 1686644422 ts: 1686644422
cgllc.clock.dove: cgllc.clock.dove:
ts: 1619607474 ts: 1619607474
cgllc.gateway.s1:
ts: 1575097876
cgllc.magnet.hodor: cgllc.magnet.hodor:
ts: 1724329476 ts: 1724329476
cgllc.motion.cgpr1: cgllc.motion.cgpr1:
@ -120,8 +130,14 @@ chuangmi.cateye.ipc018:
ts: 1632735241 ts: 1632735241
chuangmi.cateye.ipc508: chuangmi.cateye.ipc508:
ts: 1633677521 ts: 1633677521
chuangmi.door.hmi508:
ts: 1611733437
chuangmi.door.hmi515: chuangmi.door.hmi515:
ts: 1640334316 ts: 1640334316
chuangmi.gateway.ipc011:
ts: 1575097876
chuangmi.ir.v2:
ts: 1575097876
chuangmi.lock.hmi501: chuangmi.lock.hmi501:
ts: 1614742147 ts: 1614742147
chuangmi.lock.hmi501b01: chuangmi.lock.hmi501b01:
@ -142,10 +158,18 @@ chuangmi.plug.v1:
ts: 1621925183 ts: 1621925183
chuangmi.plug.v3: chuangmi.plug.v3:
ts: 1644480255 ts: 1644480255
chuangmi.plug.vtl_v1:
ts: 1575097876
chuangmi.radio.v1: chuangmi.radio.v1:
ts: 1531108800 ts: 1531108800
chuangmi.radio.v2: chuangmi.radio.v2:
ts: 1531108800 ts: 1531108800
chuangmi.remote.h102a03:
ts: 1575097876
chuangmi.remote.h102c01:
ts: 1575097876
chuangmi.remote.v2:
ts: 1575097876
chunmi.cooker.eh1: chunmi.cooker.eh1:
ts: 1607339278 ts: 1607339278
chunmi.cooker.eh402: chunmi.cooker.eh402:
@ -204,6 +228,8 @@ dmaker.airfresh.t2017:
ts: 1686731233 ts: 1686731233
dmaker.fan.p5: dmaker.fan.p5:
ts: 1655793784 ts: 1655793784
doco.fcb.docov001:
ts: 1575097876
dsm.lock.h3: dsm.lock.h3:
ts: 1615283790 ts: 1615283790
dsm.lock.q3: dsm.lock.q3:
@ -218,6 +244,30 @@ fawad.airrtc.fwd20011:
ts: 1610607149 ts: 1610607149
fbs.airmonitor.pth02: fbs.airmonitor.pth02:
ts: 1686644918 ts: 1686644918
fengmi.projector.fm05:
ts: 1575097876
fengmi.projector.fm15:
ts: 1575097876
fengmi.projector.fm154k:
ts: 1575097876
fengmi.projector.l166:
ts: 1650352923
fengmi.projector.l176:
ts: 1649936204
fengmi.projector.l246:
ts: 1575097876
fengmi.projector.m055:
ts: 1652839826
fengmi.projector.m055d:
ts: 1654067980
fengyu.intercom.beebird:
ts: 1575097876
fengyu.intercom.sharkv1:
ts: 1575097876
fotile.hood.emd1tmi:
ts: 1607483642
guoshi.other.sem01:
ts: 1602662080
hannto.printer.anise: hannto.printer.anise:
ts: 1618989537 ts: 1618989537
hannto.printer.honey: hannto.printer.honey:
@ -226,14 +276,26 @@ hannto.printer.honey1s:
ts: 1614332725 ts: 1614332725
hfjh.fishbowl.v1: hfjh.fishbowl.v1:
ts: 1615278556 ts: 1615278556
hhcc.bleflowerpot.v2:
ts: 1575097876
hhcc.plantmonitor.v1: hhcc.plantmonitor.v1:
ts: 1664163526 ts: 1664163526
hith.foot_bath.q2: hith.foot_bath.q2:
ts: 1531108800 ts: 1531108800
hmpace.bracelet.v4:
ts: 1575097876
hmpace.scales.mibfs:
ts: 1575097876
hmpace.scales.miscale2:
ts: 1575097876
huohe.lock.m1: huohe.lock.m1:
ts: 1635410938 ts: 1635410938
huoman.litter_box.co1:
ts: 1687165034
hutlon.lock.v0001: hutlon.lock.v0001:
ts: 1634799698 ts: 1634799698
idelan.aircondition.g1:
ts: 1575097876
idelan.aircondition.v1: idelan.aircondition.v1:
ts: 1614666973 ts: 1614666973
idelan.aircondition.v2: idelan.aircondition.v2:
@ -248,14 +310,22 @@ ikea.light.led1537r6:
ts: 1605162872 ts: 1605162872
ikea.light.led1545g12: ikea.light.led1545g12:
ts: 1605162937 ts: 1605162937
ikea.light.led1546g12:
ts: 1575097876
ikea.light.led1623g12: ikea.light.led1623g12:
ts: 1605163009 ts: 1605163009
ikea.light.led1649c5: ikea.light.led1649c5:
ts: 1605163064 ts: 1605163064
ikea.light.led1650r5:
ts: 1575097876
imibar.cooker.mbihr3: imibar.cooker.mbihr3:
ts: 1624620659 ts: 1624620659
imou99.camera.tp2: imou99.camera.tp2:
ts: 1531108800 ts: 1531108800
inovel.projector.me2:
ts: 1575097876
iracc.aircondition.d19:
ts: 1609914362
isa.camera.df3: isa.camera.df3:
ts: 1531108800 ts: 1531108800
isa.camera.hl5: isa.camera.hl5:
@ -266,18 +336,34 @@ isa.camera.isc5:
ts: 1531108800 ts: 1531108800
isa.camera.isc5c1: isa.camera.isc5c1:
ts: 1621238175 ts: 1621238175
isa.camera.qf3:
ts: 1575097876
isa.cateye.hldb6:
ts: 1575097876
isa.magnet.dw2hl: isa.magnet.dw2hl:
ts: 1638274655 ts: 1638274655
jieman.magic_touch.js78:
ts: 1575097876
jiqid.mistory.ipen1:
ts: 1575097876
jiqid.mistory.pro: jiqid.mistory.pro:
ts: 1531108800 ts: 1531108800
jiqid.mistory.v1: jiqid.mistory.v1:
ts: 1531108800 ts: 1531108800
jiqid.mistudy.v2: jiqid.mistudy.v2:
ts: 1610612349 ts: 1610612349
jiqid.robot.cube:
ts: 1575097876
jiwu.lock.jwp01: jiwu.lock.jwp01:
ts: 1614752632 ts: 1614752632
jyaiot.cm.ccj01: jyaiot.cm.ccj01:
ts: 1611824545 ts: 1611824545
k0918.toothbrush.kid01:
ts: 1575097876
kejia.airer.th001:
ts: 1575097876
ksmb.treadmill.k12:
ts: 1575097876
ksmb.treadmill.v1: ksmb.treadmill.v1:
ts: 1611211447 ts: 1611211447
ksmb.treadmill.v2: ksmb.treadmill.v2:
@ -390,6 +476,8 @@ loock.lock.xfvl10:
ts: 1632814256 ts: 1632814256
loock.safe.v1: loock.safe.v1:
ts: 1619607755 ts: 1619607755
lumi.acpartner.mcn02:
ts: 1655791626
lumi.acpartner.v1: lumi.acpartner.v1:
ts: 1531108800 ts: 1531108800
lumi.acpartner.v2: lumi.acpartner.v2:
@ -462,6 +550,8 @@ lumi.lock.acn02:
ts: 1623928631 ts: 1623928631
lumi.lock.acn03: lumi.lock.acn03:
ts: 1614752574 ts: 1614752574
lumi.lock.aq1:
ts: 1612518044
lumi.lock.bacn01: lumi.lock.bacn01:
ts: 1614741699 ts: 1614741699
lumi.lock.bmcn02: lumi.lock.bmcn02:
@ -482,6 +572,8 @@ lumi.lock.mcn007:
ts: 1650446757 ts: 1650446757
lumi.lock.mcn01: lumi.lock.mcn01:
ts: 1679881881 ts: 1679881881
lumi.lock.v1:
ts: 1575097876
lumi.lock.wbmcn1: lumi.lock.wbmcn1:
ts: 1619422072 ts: 1619422072
lumi.motion.bmgl01: lumi.motion.bmgl01:
@ -510,14 +602,20 @@ lumi.sensor_86sw1.v1:
ts: 1609311038 ts: 1609311038
lumi.sensor_86sw2.v1: lumi.sensor_86sw2.v1:
ts: 1608795035 ts: 1608795035
lumi.sensor_cube.aqgl01:
ts: 1575097876
lumi.sensor_ht.v1: lumi.sensor_ht.v1:
ts: 1621239877 ts: 1621239877
lumi.sensor_magnet.aq2: lumi.sensor_magnet.aq2:
ts: 1641112867 ts: 1641112867
lumi.sensor_magnet.v1:
ts: 1606120416
lumi.sensor_magnet.v2: lumi.sensor_magnet.v2:
ts: 1641113779 ts: 1641113779
lumi.sensor_motion.aq2: lumi.sensor_motion.aq2:
ts: 1676433994 ts: 1676433994
lumi.sensor_motion.v1:
ts: 1605093075
lumi.sensor_motion.v2: lumi.sensor_motion.v2:
ts: 1672818550 ts: 1672818550
lumi.sensor_natgas.v1: lumi.sensor_natgas.v1:
@ -530,6 +628,8 @@ lumi.sensor_switch.aq2:
ts: 1615256430 ts: 1615256430
lumi.sensor_switch.aq3: lumi.sensor_switch.aq3:
ts: 1607399487 ts: 1607399487
lumi.sensor_switch.v1:
ts: 1606874434
lumi.sensor_switch.v2: lumi.sensor_switch.v2:
ts: 1609310683 ts: 1609310683
lumi.sensor_wleak.aq1: lumi.sensor_wleak.aq1:
@ -574,6 +674,20 @@ miaomiaoce.sensor_ht.t1:
ts: 1616057242 ts: 1616057242
miaomiaoce.sensor_ht.t2: miaomiaoce.sensor_ht.t2:
ts: 1636603553 ts: 1636603553
miaomiaoce.thermo.t01:
ts: 1575097876
midea.aircondition.v1:
ts: 1575097876
midea.aircondition.xa1:
ts: 1575097876
midea.aircondition.xa2:
ts: 1575097876
midr.rv_mirror.m2:
ts: 1575097876
midr.rv_mirror.m5:
ts: 1575097876
midr.rv_mirror.v1:
ts: 1575097876
miir.aircondition.ir01: miir.aircondition.ir01:
ts: 1531108800 ts: 1531108800
miir.aircondition.ir02: miir.aircondition.ir02:
@ -612,6 +726,8 @@ minij.washer.v5:
ts: 1622792196 ts: 1622792196
minij.washer.v8: minij.washer.v8:
ts: 1615777868 ts: 1615777868
minuo.tracker.lm001:
ts: 1575097876
miot.light.plato2: miot.light.plato2:
ts: 1685518142 ts: 1685518142
miot.light.plato3: miot.light.plato3:
@ -624,18 +740,32 @@ mmgg.feeder.snack:
ts: 1607503182 ts: 1607503182
moyu.washer.s1hm: moyu.washer.s1hm:
ts: 1624620888 ts: 1624620888
mrbond.airer.m0:
ts: 1575097876
mrbond.airer.m1pro: mrbond.airer.m1pro:
ts: 1646393746 ts: 1646393746
mrbond.airer.m1s: mrbond.airer.m1s:
ts: 1646393874 ts: 1646393874
mrbond.airer.m1super:
ts: 1575097876
msj.f_washer.m1: msj.f_washer.m1:
ts: 1614914340 ts: 1614914340
mxiang.cateye.mdb10: mxiang.cateye.mdb10:
ts: 1616140362 ts: 1616140362
mxiang.cateye.xmcatt1: mxiang.cateye.xmcatt1:
ts: 1616140207 ts: 1616140207
nhy.airrtc.v1:
ts: 1575097876
ninebot.scooter.v1:
ts: 1602662395
ninebot.scooter.v6:
ts: 1575097876
nuwa.robot.minikiwi:
ts: 1575097876
nwt.derh.wdh318efw1: nwt.derh.wdh318efw1:
ts: 1611822375 ts: 1611822375
onemore.wifispeaker.sm4:
ts: 1575097876
opple.light.bydceiling: opple.light.bydceiling:
ts: 1608187619 ts: 1608187619
opple.light.fanlight: opple.light.fanlight:
@ -646,6 +776,8 @@ opple.remote.5pb112:
ts: 1627453840 ts: 1627453840
opple.remote.5pb113: opple.remote.5pb113:
ts: 1636599905 ts: 1636599905
orion.wifispeaker.cm1:
ts: 1575097876
ows.towel_w.mj1x0: ows.towel_w.mj1x0:
ts: 1610604939 ts: 1610604939
philips.light.bceiling1: philips.light.bceiling1:
@ -696,6 +828,8 @@ pwzn.relay.apple:
ts: 1611217196 ts: 1611217196
pwzn.relay.banana: pwzn.relay.banana:
ts: 1646647255 ts: 1646647255
qicyc.bike.tdp02z:
ts: 1575097876
qike.bhf_light.qk201801: qike.bhf_light.qk201801:
ts: 1608174909 ts: 1608174909
qmi.powerstrip.v1: qmi.powerstrip.v1:
@ -726,8 +860,32 @@ roborock.vacuum.t6:
ts: 1619423841 ts: 1619423841
rockrobo.vacuum.v1: rockrobo.vacuum.v1:
ts: 1531108800 ts: 1531108800
roidmi.carairpuri.pro:
ts: 1575097876
roidmi.carairpuri.v1:
ts: 1575097876
roidmi.cleaner.f8pro:
ts: 1575097876
roidmi.cleaner.v1:
ts: 1575097876
roidmi.cleaner.v2:
ts: 1638514177
roidmi.cleaner.v382:
ts: 1575097876
roidmi.vacuum.v1:
ts: 1575097876
rokid.robot.me:
ts: 1575097876
rokid.robot.mini:
ts: 1575097876
rokid.robot.pebble:
ts: 1575097876
rokid.robot.pebble2:
ts: 1575097876
roome.bhf_light.yf6002: roome.bhf_light.yf6002:
ts: 1531108800 ts: 1531108800
rotai.magic_touch.sx300:
ts: 1602662578
rotai.massage.rt5728: rotai.massage.rt5728:
ts: 1610607000 ts: 1610607000
rotai.massage.rt5850: rotai.massage.rt5850:
@ -738,22 +896,42 @@ rotai.massage.rt5863:
ts: 1611827937 ts: 1611827937
rotai.massage.rt5870: rotai.massage.rt5870:
ts: 1632376570 ts: 1632376570
runmi.suitcase.v1:
ts: 1575097876
scishare.coffee.s1102: scishare.coffee.s1102:
ts: 1611824402 ts: 1611824402
shjszn.gateway.c1:
ts: 1575097876
shjszn.lock.c1:
ts: 1575097876
shjszn.lock.kx:
ts: 1575097876
shuii.humidifier.jsq001:
ts: 1575097876
shuii.humidifier.jsq002: shuii.humidifier.jsq002:
ts: 1606376290 ts: 1606376290
skyrc.feeder.dfeed:
ts: 1626082349
skyrc.pet_waterer.fre1: skyrc.pet_waterer.fre1:
ts: 1608186812 ts: 1608186812
smith.w_soften.cxs05ta1:
ts: 1575097876
smith.waterheater.cxea1: smith.waterheater.cxea1:
ts: 1611826349 ts: 1611826349
smith.waterheater.cxeb1: smith.waterheater.cxeb1:
ts: 1611826388 ts: 1611826388
smith.waterpuri.jnt600: smith.waterpuri.jnt600:
ts: 1531108800 ts: 1531108800
soocare.toothbrush.m1:
ts: 1575097876
soocare.toothbrush.m1s: soocare.toothbrush.m1s:
ts: 1610611310 ts: 1610611310
soocare.toothbrush.mc1:
ts: 1575097876
soocare.toothbrush.t501: soocare.toothbrush.t501:
ts: 1672192586 ts: 1672192586
soocare.toothbrush.x3:
ts: 1575097876
sxds.pillow.pillow02: sxds.pillow.pillow02:
ts: 1611222235 ts: 1611222235
syniot.curtain.syc1: syniot.curtain.syc1:
@ -778,6 +956,10 @@ tokit.oven.tk32pro1:
ts: 1617002408 ts: 1617002408
tokit.pre_cooker.tkih1: tokit.pre_cooker.tkih1:
ts: 1607410832 ts: 1607410832
trios1.bleshoes.v02:
ts: 1602662599
txdd.wifispeaker.x1:
ts: 1575097876
viomi.aircondition.v10: viomi.aircondition.v10:
ts: 1606375041 ts: 1606375041
viomi.aircondition.v21: viomi.aircondition.v21:
@ -830,12 +1012,16 @@ viomi.fridge.u13:
ts: 1614667152 ts: 1614667152
viomi.fridge.u15: viomi.fridge.u15:
ts: 1607505693 ts: 1607505693
viomi.fridge.u17:
ts: 1575097876
viomi.fridge.u18: viomi.fridge.u18:
ts: 1614655755 ts: 1614655755
viomi.fridge.u2: viomi.fridge.u2:
ts: 1531108800 ts: 1531108800
viomi.fridge.u24: viomi.fridge.u24:
ts: 1614667214 ts: 1614667214
viomi.fridge.u25:
ts: 1575097876
viomi.fridge.u4: viomi.fridge.u4:
ts: 1614667295 ts: 1614667295
viomi.fridge.u6: viomi.fridge.u6:
@ -992,6 +1178,82 @@ xiaomi.aircondition.ma6:
ts: 1721629272 ts: 1721629272
xiaomi.aircondition.ma9: xiaomi.aircondition.ma9:
ts: 1721629362 ts: 1721629362
xiaomi.plc.v1:
ts: 1575097876
xiaomi.repeater.v1:
ts: 1575097876
xiaomi.repeater.v2:
ts: 1575097876
xiaomi.repeater.v3:
ts: 1575097876
xiaomi.router.d01:
ts: 1575097876
xiaomi.router.lv1:
ts: 1575097876
xiaomi.router.lv3:
ts: 1575097876
xiaomi.router.mv1:
ts: 1575097876
xiaomi.router.r2100:
ts: 1575097876
xiaomi.router.r3600:
ts: 1575097876
xiaomi.router.r3a:
ts: 1575097876
xiaomi.router.r3d:
ts: 1575097876
xiaomi.router.r3g:
ts: 1575097876
xiaomi.router.r3gv2:
ts: 1575097876
xiaomi.router.r3gv2n:
ts: 1575097876
xiaomi.router.r3p:
ts: 1575097876
xiaomi.router.r4:
ts: 1575097876
xiaomi.router.r4a:
ts: 1575097876
xiaomi.router.r4ac:
ts: 1575097876
xiaomi.router.r4c:
ts: 1575097876
xiaomi.router.r4cm:
ts: 1575097876
xiaomi.router.rm1800:
ts: 1575097876
xiaomi.router.v1:
ts: 1575097876
xiaomi.router.v2:
ts: 1575097876
xiaomi.router.v3:
ts: 1575097876
xiaomi.split_tv.b1:
ts: 1575097876
xiaomi.split_tv.v1:
ts: 1575097876
xiaomi.tv.b1:
ts: 1661248580
xiaomi.tv.h1:
ts: 1575097876
xiaomi.tv.i1:
ts: 1661248572
xiaomi.tv.v1:
ts: 1670811870
xiaomi.tvbox.b1:
ts: 1694503508
xiaomi.tvbox.i1:
ts: 1694503515
xiaomi.tvbox.v1:
ts: 1694503501
xiaomi.watch.band1:
ts: 1575097876
xiaomi.watch.band1A:
ts: 1575097876
xiaomi.watch.band1S:
ts: 1575097876
xiaomi.watch.band2:
ts: 1575097876
xiaomi.wifispeaker.l04m: xiaomi.wifispeaker.l04m:
ts: 1658817956 ts: 1658817956
xiaomi.wifispeaker.l06a: xiaomi.wifispeaker.l06a:
@ -1012,6 +1274,10 @@ xiaomi.wifispeaker.lx5a:
ts: 1672299577 ts: 1672299577
xiaomi.wifispeaker.s12: xiaomi.wifispeaker.s12:
ts: 1672299594 ts: 1672299594
xiaomi.wifispeaker.v1:
ts: 1575097876
xiaomi.wifispeaker.v3:
ts: 1575097876
xiaomi.wifispeaker.x08a: xiaomi.wifispeaker.x08a:
ts: 1672818945 ts: 1672818945
xiaomi.wifispeaker.x08c: xiaomi.wifispeaker.x08c:
@ -1028,6 +1294,44 @@ xiaovv.camera.xvd5:
ts: 1531108800 ts: 1531108800
xiaovv.camera.xvsnowman: xiaovv.camera.xvsnowman:
ts: 1531108800 ts: 1531108800
xiaoxun.robot.v1:
ts: 1575097876
xiaoxun.tracker.v1:
ts: 1575097876
xiaoxun.watch.sw306:
ts: 1575097876
xiaoxun.watch.sw560:
ts: 1575097876
xiaoxun.watch.sw705:
ts: 1575097876
xiaoxun.watch.sw710a2:
ts: 1575097876
xiaoxun.watch.sw760:
ts: 1575097876
xiaoxun.watch.sw900:
ts: 1575097876
xiaoxun.watch.sw960:
ts: 1575097876
xiaoxun.watch.v1:
ts: 1575097876
xiaoxun.watch.v10:
ts: 1575097876
xiaoxun.watch.v11:
ts: 1575097876
xiaoxun.watch.v2:
ts: 1575097876
xiaoxun.watch.v3:
ts: 1575097876
xiaoxun.watch.v4:
ts: 1575097876
xiaoxun.watch.v5:
ts: 1575097876
xiaoxun.watch.v7:
ts: 1575097876
xiaoxun.watch.v8:
ts: 1575097876
xiaoxun.watch.v9:
ts: 1575097876
xjx.toilet.pro: xjx.toilet.pro:
ts: 1615965466 ts: 1615965466
xjx.toilet.pure: xjx.toilet.pure:
@ -1054,6 +1358,8 @@ yeelink.bhf_light.v3:
ts: 1608790102 ts: 1608790102
yeelink.bhf_light.v5: yeelink.bhf_light.v5:
ts: 1601292562 ts: 1601292562
yeelink.gateway.v1:
ts: 1575097876
yeelink.light.bslamp1: yeelink.light.bslamp1:
ts: 1703120679 ts: 1703120679
yeelink.light.bslamp2: yeelink.light.bslamp2:
@ -1192,6 +1498,10 @@ yunmi.kettle.r2:
ts: 1606372087 ts: 1606372087
yunmi.kettle.r3: yunmi.kettle.r3:
ts: 1637309534 ts: 1637309534
yunmi.kettle.v1:
ts: 1575097876
yunmi.kettle.v9:
ts: 1602662686
yunmi.plmachine.mg2: yunmi.plmachine.mg2:
ts: 1611833658 ts: 1611833658
yunmi.waterpuri.c5: yunmi.waterpuri.c5:
@ -1230,18 +1540,26 @@ yunmi.waterpurifier.v2:
ts: 1632377061 ts: 1632377061
yunmi.waterpurifier.v3: yunmi.waterpurifier.v3:
ts: 1611221428 ts: 1611221428
yunyi.camera.v1:
ts: 1575097876
yyunyi.wopener.yypy24: yyunyi.wopener.yypy24:
ts: 1616741966 ts: 1616741966
yyzhn.gateway.yn181126:
ts: 1610689325
zdeer.ajh.a8: zdeer.ajh.a8:
ts: 1531108800 ts: 1531108800
zdeer.ajh.a9: zdeer.ajh.a9:
ts: 1531108800 ts: 1531108800
zdeer.ajh.ajb:
ts: 1608276454
zdeer.ajh.zda10: zdeer.ajh.zda10:
ts: 1531108800 ts: 1531108800
zdeer.ajh.zda9: zdeer.ajh.zda9:
ts: 1531108800 ts: 1531108800
zdeer.ajh.zjy: zdeer.ajh.zjy:
ts: 1531108800 ts: 1531108800
zhij.toothbrush.bv1:
ts: 1575097876
zhimi.aircondition.ma1: zhimi.aircondition.ma1:
ts: 1615185265 ts: 1615185265
zhimi.aircondition.ma3: zhimi.aircondition.ma3:
@ -1250,6 +1568,8 @@ zhimi.aircondition.ma4:
ts: 1626334057 ts: 1626334057
zhimi.aircondition.v1: zhimi.aircondition.v1:
ts: 1610610931 ts: 1610610931
zhimi.aircondition.v2:
ts: 1575097876
zhimi.aircondition.va1: zhimi.aircondition.va1:
ts: 1609924720 ts: 1609924720
zhimi.aircondition.za1: zhimi.aircondition.za1:
@ -1276,8 +1596,12 @@ zhimi.airpurifier.sa2:
ts: 1635820002 ts: 1635820002
zhimi.airpurifier.v1: zhimi.airpurifier.v1:
ts: 1635855633 ts: 1635855633
zhimi.airpurifier.v2:
ts: 1575097876
zhimi.airpurifier.v3: zhimi.airpurifier.v3:
ts: 1676339933 ts: 1676339933
zhimi.airpurifier.v5:
ts: 1575097876
zhimi.airpurifier.v6: zhimi.airpurifier.v6:
ts: 1636978652 ts: 1636978652
zhimi.airpurifier.v7: zhimi.airpurifier.v7:
@ -1318,3 +1642,5 @@ zimi.mosq.v1:
ts: 1620728957 ts: 1620728957
zimi.powerstrip.v2: zimi.powerstrip.v2:
ts: 1620812714 ts: 1620812714
zimi.projector.v1:
ts: 1575097876

View File

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

View File

@ -75,10 +75,11 @@ class MIoTOauthClient:
_oauth_host: str _oauth_host: str
_client_id: int _client_id: int
_redirect_url: str _redirect_url: str
_device_id: str
def __init__( def __init__(
self, client_id: str, redirect_url: str, cloud_server: str, self, client_id: str, redirect_url: str, cloud_server: str,
loop: Optional[asyncio.AbstractEventLoop] = None uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None: ) -> None:
self._main_loop = loop or asyncio.get_running_loop() self._main_loop = loop or asyncio.get_running_loop()
if client_id is None or client_id.strip() == '': if client_id is None or client_id.strip() == '':
@ -87,6 +88,8 @@ class MIoTOauthClient:
raise MIoTOauthError('invalid redirect_url') raise MIoTOauthError('invalid redirect_url')
if not cloud_server: if not cloud_server:
raise MIoTOauthError('invalid cloud_server') raise MIoTOauthError('invalid cloud_server')
if not uuid:
raise MIoTOauthError('invalid uuid')
self._client_id = int(client_id) self._client_id = int(client_id)
self._redirect_url = redirect_url self._redirect_url = redirect_url
@ -94,6 +97,7 @@ class MIoTOauthClient:
self._oauth_host = DEFAULT_OAUTH2_API_HOST self._oauth_host = DEFAULT_OAUTH2_API_HOST
else: else:
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}' self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._device_id = f'ha.{uuid}'
self._session = aiohttp.ClientSession(loop=self._main_loop) self._session = aiohttp.ClientSession(loop=self._main_loop)
async def deinit_async(self) -> None: async def deinit_async(self) -> None:
@ -132,6 +136,7 @@ class MIoTOauthClient:
'redirect_uri': redirect_url or self._redirect_url, 'redirect_uri': redirect_url or self._redirect_url,
'client_id': self._client_id, 'client_id': self._client_id,
'response_type': 'code', 'response_type': 'code',
'device_id': self._device_id
} }
if state: if state:
params['state'] = state params['state'] = state
@ -191,6 +196,7 @@ class MIoTOauthClient:
'client_id': self._client_id, 'client_id': self._client_id,
'redirect_uri': self._redirect_url, 'redirect_uri': self._redirect_url,
'code': code, 'code': code,
'device_id': self._device_id
}) })
async def refresh_access_token_async(self, refresh_token: str) -> dict: async def refresh_access_token_async(self, refresh_token: str) -> dict:
@ -531,9 +537,18 @@ class MIoTHttpClient:
name = device.get('name', None) name = device.get('name', None)
urn = device.get('spec_type', None) urn = device.get('spec_type', None)
model = device.get('model', None) model = device.get('model', None)
if did is None or name is None or urn is None or model is None: if did is None or name is None:
_LOGGER.error( _LOGGER.info(
'get_device_list, cloud, invalid device, %s', device) 'invalid device, cloud, %s', device)
continue
if urn is None or model is None:
_LOGGER.info(
'missing the urn|model field, cloud, %s', device)
continue
if did.startswith('miwifi.'):
# The miwifi.* routers defined SPEC functions, but none of them
# were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue continue
device_infos[did] = { device_infos[did] = {
'did': did, 'did': did,
@ -634,7 +649,7 @@ class MIoTHttpClient:
for did in dids: for did in dids:
if did not in results: if did not in results:
devices.pop(did, None) devices.pop(did, None)
_LOGGER.error('get device info failed, %s', did) _LOGGER.info('get device info failed, %s', did)
continue continue
devices[did].update(results[did]) devices[did].update(results[did])
# Whether sub devices # Whether sub devices
@ -720,7 +735,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__)
@ -511,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:
@ -564,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:
@ -836,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
@ -1006,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: Optional[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]
@ -1022,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
@ -1077,15 +1076,12 @@ class MIoTPropertyEntity(Entity):
def get_vlist_description(self, value: Any) -> Optional[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:

View File

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

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

@ -59,5 +59,10 @@
"1", "1",
"5" "5"
] ]
},
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
"services": [
"*"
]
} }
} }

View File

@ -289,7 +289,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
} }
}, },
'optional': { 'optional': {
'properties': {'mode', 'horizontal-swing'} 'properties': {'mode', 'horizontal-swing', 'wind-reverse'}
}, },
'entity': 'fan' 'entity': 'fan'
}, },

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

@ -90,6 +90,7 @@
"unreachable_mqtt_broker": "Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration." "unreachable_mqtt_broker": "Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration."
}, },
"abort": { "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.", "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.", "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.", "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,6 +90,7 @@
"unreachable_mqtt_broker": "Unable to reach Xiaomi MQTT Broker address, please check network configuration." "unreachable_mqtt_broker": "Unable to reach Xiaomi MQTT Broker address, please check network configuration."
}, },
"abort": { "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.", "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.", "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.", "invalid_auth_info": "Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.",

View File

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

View File

@ -90,6 +90,7 @@
"unreachable_mqtt_broker": "Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie." "unreachable_mqtt_broker": "Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie."
}, },
"abort": { "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.", "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.", "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.", "invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.",

View File

@ -90,6 +90,7 @@
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede." "unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
}, },
"abort": { "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.", "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.", "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.", "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,6 +90,7 @@
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede." "unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
}, },
"abort": { "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.", "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.", "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.", "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,6 +90,7 @@
"unreachable_mqtt_broker": "Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети." "unreachable_mqtt_broker": "Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети."
}, },
"abort": { "abort": {
"ha_uuid_get_failed": "Не удалось получить UUID Home Assistant.",
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.", "network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.", "already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.", "invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",

View File

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

View File

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

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