mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2026-01-16 14:40:43 +08:00
Compare commits
15 Commits
7fd4b8017b
...
d53250366e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d53250366e | ||
|
|
60e57e863d | ||
|
|
792d70f2ba | ||
|
|
152933a223 | ||
|
|
6557b22a52 | ||
|
|
fca97e03f0 | ||
|
|
93f04b1aee | ||
|
|
d25d3f6a93 | ||
|
|
3399e3bb20 | ||
|
|
078adfbd4c | ||
|
|
e4dfdf68ab | ||
|
|
5d4b975f85 | ||
|
|
0566546a99 | ||
|
|
c0d100ce2b | ||
|
|
ce7ce7af4b |
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.1.5b1
|
||||
This version will cause some Xiaomi routers that do not support access (#564) to become unavailable. You can update the device list in the configuration or delete it manually.
|
||||
### Added
|
||||
- Fan entity support direction ctrl [#556](https://github.com/XiaoMi/ha_xiaomi_home/pull/556)
|
||||
### Changed
|
||||
- Filter miwifi.* devices and xiaomi.router.rd03 [#564](https://github.com/XiaoMi/ha_xiaomi_home/pull/564)
|
||||
### Fixed
|
||||
- Fix multi ha instance login [#560](https://github.com/XiaoMi/ha_xiaomi_home/pull/560)
|
||||
- Fix fan speed [#464](https://github.com/XiaoMi/ha_xiaomi_home/pull/464)
|
||||
- The number of profile models updated from 660 to 823. [#583](https://github.com/XiaoMi/ha_xiaomi_home/pull/583)
|
||||
|
||||
## v0.1.5b0
|
||||
### Added
|
||||
- Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101)
|
||||
|
||||
@ -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>: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>:action:<aiid> # action
|
||||
```
|
||||
|
||||
@ -156,64 +156,56 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
_LOGGER.error(
|
||||
'unknown on property, %s', self.entity_id)
|
||||
elif prop.name == 'mode':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'invalid mode value_list, %s', self.entity_id)
|
||||
continue
|
||||
self._hvac_mode_map = {}
|
||||
for item in prop.value_list:
|
||||
if item['name'].lower() in {'off', 'idle'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.OFF
|
||||
elif item['name'].lower() in {'auto'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.AUTO
|
||||
elif item['name'].lower() in {'cool'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.COOL
|
||||
elif item['name'].lower() in {'heat'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.HEAT
|
||||
elif item['name'].lower() in {'dry'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.DRY
|
||||
elif item['name'].lower() in {'fan'}:
|
||||
self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'off', 'idle'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.OFF
|
||||
elif item.name in {'auto'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.AUTO
|
||||
elif item.name in {'cool'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.COOL
|
||||
elif item.name in {'heat'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.HEAT
|
||||
elif item.name in {'dry'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.DRY
|
||||
elif item.name in {'fan'}:
|
||||
self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY
|
||||
self._attr_hvac_modes = list(self._hvac_mode_map.values())
|
||||
self._prop_mode = prop
|
||||
elif prop.name == 'target-temperature':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_temp = prop.value_range['min']
|
||||
self._attr_max_temp = prop.value_range['max']
|
||||
self._attr_target_temperature_step = prop.value_range['step']
|
||||
self._attr_min_temp = prop.value_range.min_
|
||||
self._attr_max_temp = prop.value_range.max_
|
||||
self._attr_target_temperature_step = prop.value_range.step
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE)
|
||||
self._prop_target_temp = prop
|
||||
elif prop.name == 'target-humidity':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-humidity value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_humidity = prop.value_range['min']
|
||||
self._attr_max_humidity = prop.value_range['max']
|
||||
self._attr_min_humidity = prop.value_range.min_
|
||||
self._attr_max_humidity = prop.value_range.max_
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_HUMIDITY)
|
||||
self._prop_target_humi = prop
|
||||
elif prop.name == 'fan-level':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'invalid fan-level value_list, %s', self.entity_id)
|
||||
continue
|
||||
self._fan_mode_map = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._fan_mode_map = prop.value_list.to_map()
|
||||
self._attr_fan_modes = list(self._fan_mode_map.values())
|
||||
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
|
||||
self._prop_fan_level = prop
|
||||
@ -269,8 +261,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
elif self.get_prop_value(prop=self._prop_on) is False:
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
# set mode
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._hvac_mode_map, description=hvac_mode)
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=hvac_mode)
|
||||
if (
|
||||
mode_value is None or
|
||||
not await self.set_property_async(
|
||||
@ -339,8 +331,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._fan_mode_map, description=fan_mode)
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._fan_mode_map, value=fan_mode)
|
||||
if mode_value is None or not await self.set_property_async(
|
||||
prop=self._prop_fan_level, value=mode_value):
|
||||
raise RuntimeError(
|
||||
@ -376,9 +368,9 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
"""Return the hvac mode. e.g., heat, cool mode."""
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
return HVACMode.OFF
|
||||
return self.get_map_description(
|
||||
return self.get_map_key(
|
||||
map_=self._hvac_mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
value=self.get_prop_value(prop=self._prop_mode))
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> Optional[str]:
|
||||
@ -386,7 +378,7 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
|
||||
Requires ClimateEntityFeature.FAN_MODE.
|
||||
"""
|
||||
return self.get_map_description(
|
||||
return self.get_map_value(
|
||||
map_=self._fan_mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||
|
||||
@ -446,8 +438,8 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
}.get(v_ac_state['M'], None)
|
||||
if mode:
|
||||
self.set_prop_value(
|
||||
prop=self._prop_mode, value=self.get_map_value(
|
||||
map_=self._hvac_mode_map, description=mode))
|
||||
prop=self._prop_mode, value=self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=mode))
|
||||
# T: target temperature
|
||||
if 'T' in v_ac_state and self._prop_target_temp:
|
||||
self.set_prop_value(prop=self._prop_target_temp,
|
||||
@ -517,29 +509,24 @@ class Heater(MIoTServiceEntity, ClimateEntity):
|
||||
ClimateEntityFeature.TURN_OFF)
|
||||
self._prop_on = prop
|
||||
elif prop.name == 'target-temperature':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_temp = prop.value_range['min']
|
||||
self._attr_max_temp = prop.value_range['max']
|
||||
self._attr_target_temperature_step = prop.value_range['step']
|
||||
self._attr_min_temp = prop.value_range.min_
|
||||
self._attr_max_temp = prop.value_range.max_
|
||||
self._attr_target_temperature_step = prop.value_range.step
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE)
|
||||
self._prop_target_temp = prop
|
||||
elif prop.name == 'heat-level':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'invalid heat-level value_list, %s', self.entity_id)
|
||||
continue
|
||||
self._heat_level_map = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._heat_level_map = prop.value_list.to_map()
|
||||
self._attr_preset_modes = list(self._heat_level_map.values())
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.PRESET_MODE)
|
||||
@ -582,8 +569,8 @@ class Heater(MIoTServiceEntity, ClimateEntity):
|
||||
"""Set the preset mode."""
|
||||
await self.set_property_async(
|
||||
self._prop_heat_level,
|
||||
value=self.get_map_value(
|
||||
map_=self._heat_level_map, description=preset_mode))
|
||||
value=self.get_map_key(
|
||||
map_=self._heat_level_map, value=preset_mode))
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> Optional[float]:
|
||||
@ -613,7 +600,7 @@ class Heater(MIoTServiceEntity, ClimateEntity):
|
||||
@property
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
return (
|
||||
self.get_map_description(
|
||||
self.get_map_value(
|
||||
map_=self._heat_level_map,
|
||||
key=self.get_prop_value(prop=self._prop_heat_level))
|
||||
if self._prop_heat_level else None)
|
||||
|
||||
@ -68,6 +68,7 @@ from homeassistant.components.webhook import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.instance_id import async_get
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .miot.const import (
|
||||
@ -247,6 +248,13 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
self._cloud_server = user_input.get(
|
||||
'cloud_server', self._cloud_server)
|
||||
# Gen instance uuid
|
||||
ha_uuid = await async_get(self.hass)
|
||||
if not ha_uuid:
|
||||
raise AbortFlow(reason='ha_uuid_get_failed')
|
||||
self._uuid = hashlib.sha256(
|
||||
f'{ha_uuid}.{self._virtual_did}.{self._cloud_server}'.encode(
|
||||
'utf-8')).hexdigest()[:32]
|
||||
self._integration_language = user_input.get(
|
||||
'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
|
||||
self._miot_i18n = MIoTI18n(
|
||||
@ -415,9 +423,11 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
miot_oauth = MIoTOauthClient(
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=self._oauth_redirect_url_full,
|
||||
cloud_server=self._cloud_server
|
||||
)
|
||||
state = str(secrets.randbits(64))
|
||||
cloud_server=self._cloud_server,
|
||||
uuid=self._uuid,
|
||||
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._cc_oauth_auth_url = miot_oauth.gen_auth_url(
|
||||
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,
|
||||
access_token=auth_info['access_token'])
|
||||
self._auth_info = auth_info
|
||||
# Gen uuid
|
||||
self._uuid = hashlib.sha256(
|
||||
f'{self._virtual_did}.{auth_info["access_token"]}'.encode(
|
||||
'utf-8')
|
||||
).hexdigest()[:32]
|
||||
try:
|
||||
self._nick_name = (
|
||||
await self._miot_http.get_user_info_async() or {}
|
||||
@ -1145,7 +1150,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
async def async_step_oauth(self, user_input=None):
|
||||
try:
|
||||
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._miot_oauth.set_redirect_url(
|
||||
redirect_url=self._oauth_redirect_url_full)
|
||||
|
||||
@ -132,53 +132,47 @@ class Cover(MIoTServiceEntity, CoverEntity):
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
if prop.name == 'motor-control':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'motor-control value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
for item in prop.value_list:
|
||||
if item['name'].lower() in ['open']:
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'open'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN)
|
||||
self._prop_motor_value_open = item['value']
|
||||
elif item['name'].lower() in ['close']:
|
||||
self._prop_motor_value_open = item.value
|
||||
elif item.name in {'close'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.CLOSE)
|
||||
self._prop_motor_value_close = item['value']
|
||||
elif item['name'].lower() in ['pause']:
|
||||
self._prop_motor_value_close = item.value
|
||||
elif item.name in {'pause'}:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.STOP)
|
||||
self._prop_motor_value_pause = item['value']
|
||||
self._prop_motor_value_pause = item.value
|
||||
self._prop_motor_control = prop
|
||||
elif prop.name == 'status':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'status value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
for item in prop.value_list:
|
||||
if item['name'].lower() in ['opening', 'open']:
|
||||
self._prop_status_opening = item['value']
|
||||
elif item['name'].lower() in ['closing', 'close']:
|
||||
self._prop_status_closing = item['value']
|
||||
elif item['name'].lower() in ['stop', 'pause']:
|
||||
self._prop_status_stop = item['value']
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'opening', 'open'}:
|
||||
self._prop_status_opening = item.value
|
||||
elif item.name in {'closing', 'close'}:
|
||||
self._prop_status_closing = item.value
|
||||
elif item.name in {'stop', 'pause'}:
|
||||
self._prop_status_stop = item.value
|
||||
self._prop_status = prop
|
||||
elif prop.name == 'current-position':
|
||||
self._prop_current_position = prop
|
||||
elif prop.name == 'target-position':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-position value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._prop_position_value_min = prop.value_range['min']
|
||||
self._prop_position_value_max = prop.value_range['max']
|
||||
self._prop_position_value_min = prop.value_range.min_
|
||||
self._prop_position_value_max = prop.value_range.max_
|
||||
self._prop_position_value_range = (
|
||||
self._prop_position_value_max -
|
||||
self._prop_position_value_min)
|
||||
|
||||
@ -55,7 +55,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.util.percentage import (
|
||||
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
|
||||
@ -85,15 +87,20 @@ async def async_setup_entry(
|
||||
class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Fan entities for Xiaomi Home."""
|
||||
# pylint: disable=unused-argument
|
||||
_prop_on: Optional[MIoTSpecProperty]
|
||||
_prop_on: MIoTSpecProperty
|
||||
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
_prop_horizontal_swing: Optional[MIoTSpecProperty]
|
||||
_prop_wind_reverse: Optional[MIoTSpecProperty]
|
||||
_prop_wind_reverse_forward: Any
|
||||
_prop_wind_reverse_reverse: Any
|
||||
|
||||
_speed_min: Optional[int]
|
||||
_speed_max: Optional[int]
|
||||
_speed_step: Optional[int]
|
||||
_mode_list: Optional[dict[Any, Any]]
|
||||
_speed_min: int
|
||||
_speed_max: int
|
||||
_speed_step: int
|
||||
_speed_names: Optional[list]
|
||||
_speed_name_map: Optional[dict[int, str]]
|
||||
_mode_map: Optional[dict[Any, Any]]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
@ -101,16 +108,23 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Initialize the Fan."""
|
||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||
self._attr_preset_modes = []
|
||||
self._attr_current_direction = None
|
||||
self._attr_supported_features = FanEntityFeature(0)
|
||||
|
||||
self._prop_on = None
|
||||
# _prop_on is required
|
||||
self._prop_fan_level = None
|
||||
self._prop_mode = None
|
||||
self._prop_horizontal_swing = None
|
||||
self._prop_wind_reverse = None
|
||||
self._prop_wind_reverse_forward = None
|
||||
self._prop_wind_reverse_reverse = None
|
||||
self._speed_min = 65535
|
||||
self._speed_max = 0
|
||||
self._speed_step = 1
|
||||
self._mode_list = None
|
||||
self._speed_names = []
|
||||
self._speed_name_map = {}
|
||||
|
||||
self._mode_map = None
|
||||
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
@ -119,59 +133,62 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
self._attr_supported_features |= FanEntityFeature.TURN_OFF
|
||||
self._prop_on = prop
|
||||
elif prop.name == 'fan-level':
|
||||
if isinstance(prop.value_range, dict):
|
||||
if prop.value_range:
|
||||
# Fan level with value-range
|
||||
self._speed_min = prop.value_range['min']
|
||||
self._speed_max = prop.value_range['max']
|
||||
self._speed_step = prop.value_range['step']
|
||||
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||
self._speed_min = prop.value_range.min_
|
||||
self._speed_max = prop.value_range.max_
|
||||
self._speed_step = prop.value_range.step
|
||||
self._attr_speed_count = int((
|
||||
self._speed_max - self._speed_min)/self._speed_step)+1
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
self._prop_fan_level = prop
|
||||
elif (
|
||||
self._prop_fan_level is None
|
||||
and isinstance(prop.value_list, list)
|
||||
and prop.value_list
|
||||
):
|
||||
# Fan level with value-list
|
||||
for item in prop.value_list:
|
||||
self._speed_min = min(self._speed_min, item['value'])
|
||||
self._speed_max = max(self._speed_max, item['value'])
|
||||
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||
# Fan level with value-range is prior to fan level with
|
||||
# value-list when a fan has both fan level properties.
|
||||
self._speed_name_map = prop.value_list.to_map()
|
||||
self._speed_names = list(self._speed_name_map.values())
|
||||
self._attr_speed_count = len(self._speed_names)
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
self._prop_fan_level = prop
|
||||
elif prop.name == 'mode':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'mode value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
self._mode_list = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._attr_preset_modes = list(self._mode_list.values())
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_preset_modes = list(self._mode_map.values())
|
||||
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
|
||||
self._prop_mode = prop
|
||||
elif prop.name == 'horizontal-swing':
|
||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||
self._prop_horizontal_swing = prop
|
||||
|
||||
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
return self._mode_list.get(key, None)
|
||||
|
||||
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
for key, value in self._mode_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
elif prop.name == 'wind-reverse':
|
||||
if prop.format_ == 'bool':
|
||||
self._prop_wind_reverse_forward = False
|
||||
self._prop_wind_reverse_reverse = True
|
||||
elif prop.value_list:
|
||||
for item in prop.value_list.items:
|
||||
if item.name in {'foreward'}:
|
||||
self._prop_wind_reverse_forward = item.value
|
||||
self._prop_wind_reverse_reverse = item.value
|
||||
if (
|
||||
self._prop_wind_reverse_forward is None
|
||||
or self._prop_wind_reverse_reverse is 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(
|
||||
self, percentage: int = None, preset_mode: str = None, **kwargs: Any
|
||||
self, percentage: Optional[int] = None,
|
||||
preset_mode: Optional[str] = None, **kwargs: Any
|
||||
) -> None:
|
||||
"""Turn the fan on.
|
||||
|
||||
@ -182,14 +199,25 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
# percentage
|
||||
if percentage:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=int(percentage*self._attr_speed_count/100))
|
||||
if self._speed_names:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=self.get_map_value(
|
||||
map_=self._speed_name_map,
|
||||
key=percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)))
|
||||
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
|
||||
if preset_mode:
|
||||
await self.set_property_async(
|
||||
self._prop_mode,
|
||||
value=self.__get_mode_value(description=preset_mode))
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map, value=preset_mode))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
@ -202,11 +230,19 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan speed."""
|
||||
if percentage > 0:
|
||||
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 self._speed_names:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=self.get_map_value(
|
||||
map_=self._speed_name_map,
|
||||
key=percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)))
|
||||
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 the fan is off, turn it on.
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
@ -217,10 +253,19 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Set the preset mode."""
|
||||
await self.set_property_async(
|
||||
self._prop_mode,
|
||||
value=self.__get_mode_value(description=preset_mode))
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map, value=preset_mode))
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
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:
|
||||
"""Oscillate the fan."""
|
||||
@ -238,17 +283,33 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Return the current preset mode,
|
||||
e.g., auto, smart, eco, favorite."""
|
||||
return (
|
||||
self.__get_mode_description(
|
||||
self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
if self._prop_mode else None)
|
||||
|
||||
@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
|
||||
def percentage(self) -> Optional[int]:
|
||||
"""Return the current percentage of the fan speed."""
|
||||
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
||||
return ranged_value_to_percentage(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
value=fan_level) if fan_level else None
|
||||
if fan_level is None:
|
||||
return 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
|
||||
def oscillating(self) -> Optional[bool]:
|
||||
@ -257,8 +318,3 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
self.get_prop_value(
|
||||
prop=self._prop_horizontal_swing)
|
||||
if self._prop_horizontal_swing else None)
|
||||
|
||||
@property
|
||||
def percentage_step(self) -> float:
|
||||
"""Return the step of the fan speed."""
|
||||
return self._speed_step
|
||||
|
||||
@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
_prop_target_humidity: Optional[MIoTSpecProperty]
|
||||
_prop_humidity: Optional[MIoTSpecProperty]
|
||||
|
||||
_mode_list: dict[Any, Any]
|
||||
_mode_map: dict[Any, Any]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
@ -110,7 +110,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
self._prop_mode = None
|
||||
self._prop_target_humidity = None
|
||||
self._prop_humidity = None
|
||||
self._mode_list = None
|
||||
self._mode_map = None
|
||||
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
@ -119,28 +119,23 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
self._prop_on = prop
|
||||
# target-humidity
|
||||
elif prop.name == 'target-humidity':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-humidity value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_humidity = prop.value_range['min']
|
||||
self._attr_max_humidity = prop.value_range['max']
|
||||
self._attr_min_humidity = prop.value_range.min_
|
||||
self._attr_max_humidity = prop.value_range.max_
|
||||
self._prop_target_humidity = prop
|
||||
# mode
|
||||
elif prop.name == 'mode':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'mode value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
self._mode_list = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_available_modes = list(
|
||||
self._mode_list.values())
|
||||
self._mode_map.values())
|
||||
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||
self._prop_mode = prop
|
||||
# relative-humidity
|
||||
@ -163,7 +158,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
async def async_set_mode(self, mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
await self.set_property_async(
|
||||
prop=self._prop_mode, value=self.__get_mode_value(description=mode))
|
||||
prop=self._prop_mode,
|
||||
value=self.get_map_key(map_=self._mode_map, value=mode))
|
||||
|
||||
@property
|
||||
def is_on(self) -> Optional[bool]:
|
||||
@ -183,20 +179,6 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
|
||||
@property
|
||||
def mode(self) -> Optional[str]:
|
||||
"""Return the current preset mode."""
|
||||
return self.__get_mode_description(
|
||||
return self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
||||
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||
"""Convert mode value to description."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
return self._mode_list.get(key, None)
|
||||
|
||||
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||
"""Convert mode description to value."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
for key, value in self._mode_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
|
||||
@ -96,14 +96,14 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
"""Light entities for Xiaomi Home."""
|
||||
# pylint: disable=unused-argument
|
||||
_VALUE_RANGE_MODE_COUNT_MAX = 30
|
||||
_prop_on: Optional[MIoTSpecProperty]
|
||||
_prop_on: MIoTSpecProperty
|
||||
_prop_brightness: Optional[MIoTSpecProperty]
|
||||
_prop_color_temp: Optional[MIoTSpecProperty]
|
||||
_prop_color: Optional[MIoTSpecProperty]
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
|
||||
_brightness_scale: Optional[tuple[int, int]]
|
||||
_mode_list: Optional[dict[Any, Any]]
|
||||
_mode_map: Optional[dict[Any, Any]]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
@ -122,7 +122,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
self._prop_color = None
|
||||
self._prop_mode = None
|
||||
self._brightness_scale = None
|
||||
self._mode_list = None
|
||||
self._mode_map = None
|
||||
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
@ -131,20 +131,17 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
self._prop_on = prop
|
||||
# brightness
|
||||
if prop.name == 'brightness':
|
||||
if isinstance(prop.value_range, dict):
|
||||
if prop.value_range:
|
||||
self._brightness_scale = (
|
||||
prop.value_range['min'], prop.value_range['max'])
|
||||
prop.value_range.min_, prop.value_range.max_)
|
||||
self._prop_brightness = prop
|
||||
elif (
|
||||
self._mode_list is None
|
||||
and isinstance(prop.value_list, list)
|
||||
self._mode_map is None
|
||||
and prop.value_list
|
||||
):
|
||||
# For value-list brightness
|
||||
self._mode_list = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._attr_effect_list = list(self._mode_list.values())
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_effect_list = list(self._mode_map.values())
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
self._prop_mode = prop
|
||||
else:
|
||||
@ -153,13 +150,13 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
continue
|
||||
# color-temperature
|
||||
if prop.name == 'color-temperature':
|
||||
if not isinstance(prop.value_range, dict):
|
||||
if not prop.value_range:
|
||||
_LOGGER.info(
|
||||
'invalid color-temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_color_temp_kelvin = prop.value_range['min']
|
||||
self._attr_max_color_temp_kelvin = prop.value_range['max']
|
||||
self._attr_min_color_temp_kelvin = prop.value_range.min_
|
||||
self._attr_max_color_temp_kelvin = prop.value_range.max_
|
||||
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
self._prop_color_temp = prop
|
||||
@ -171,20 +168,15 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
# mode
|
||||
if prop.name == 'mode':
|
||||
mode_list = None
|
||||
if (
|
||||
isinstance(prop.value_list, list)
|
||||
and prop.value_list
|
||||
):
|
||||
mode_list = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
elif isinstance(prop.value_range, dict):
|
||||
if prop.value_list:
|
||||
mode_list = prop.value_list.to_map()
|
||||
elif prop.value_range:
|
||||
mode_list = {}
|
||||
if (
|
||||
int((
|
||||
prop.value_range['max']
|
||||
- prop.value_range['min']
|
||||
) / prop.value_range['step'])
|
||||
prop.value_range.max_
|
||||
- prop.value_range.min_
|
||||
) / prop.value_range.step)
|
||||
> self._VALUE_RANGE_MODE_COUNT_MAX
|
||||
):
|
||||
_LOGGER.info(
|
||||
@ -192,13 +184,13 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
self.entity_id, prop.name, prop.value_range)
|
||||
else:
|
||||
for value in range(
|
||||
prop.value_range['min'],
|
||||
prop.value_range['max'],
|
||||
prop.value_range['step']):
|
||||
prop.value_range.min_,
|
||||
prop.value_range.max_,
|
||||
prop.value_range.step):
|
||||
mode_list[value] = f'mode {value}'
|
||||
if mode_list:
|
||||
self._mode_list = mode_list
|
||||
self._attr_effect_list = list(self._mode_list.values())
|
||||
self._mode_map = mode_list
|
||||
self._attr_effect_list = list(self._mode_map.values())
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
self._prop_mode = prop
|
||||
else:
|
||||
@ -213,21 +205,6 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
self._attr_supported_color_modes.add(ColorMode.ONOFF)
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
|
||||
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||
"""Convert mode value to description."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
return self._mode_list.get(key, None)
|
||||
|
||||
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||
"""Convert mode description to value."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
for key, value in self._mode_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self) -> Optional[bool]:
|
||||
"""Return if the light is on."""
|
||||
@ -264,7 +241,8 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
@property
|
||||
def effect(self) -> Optional[str]:
|
||||
"""Return the current mode."""
|
||||
return self.__get_mode_description(
|
||||
return self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
@ -275,7 +253,7 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
result: bool = False
|
||||
# on
|
||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||
value_on = True if self._prop_on.format_ == 'bool' else 1
|
||||
value_on = True if self._prop_on.format_ == bool else 1
|
||||
result = await self.set_property_async(
|
||||
prop=self._prop_on, value=value_on)
|
||||
# brightness
|
||||
@ -303,11 +281,12 @@ class Light(MIoTServiceEntity, LightEntity):
|
||||
if ATTR_EFFECT in kwargs:
|
||||
result = await self.set_property_async(
|
||||
prop=self._prop_mode,
|
||||
value=self.__get_mode_value(description=kwargs[ATTR_EFFECT]))
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
|
||||
return result
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the light off."""
|
||||
# Dirty logic for lumi.gateway.mgl03 indicator light
|
||||
value_on = False if self._prop_on.format_ == 'bool' else 0
|
||||
value_on = False if self._prop_on.format_ == bool else 0
|
||||
return await self.set_property_async(prop=self._prop_on, value=value_on)
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"cryptography",
|
||||
"psutil"
|
||||
],
|
||||
"version": "v0.1.5b0",
|
||||
"version": "v0.1.5b1",
|
||||
"zeroconf": [
|
||||
"_miot-central._tcp.local."
|
||||
]
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
ts: 1603967572
|
||||
1245.airpurifier.dl01:
|
||||
ts: 1607502661
|
||||
17216.magic_touch.d150:
|
||||
ts: 1575097876
|
||||
17216.magic_touch.d152:
|
||||
ts: 1575097876
|
||||
17216.massage.ec1266a:
|
||||
ts: 1615881124
|
||||
397.light.hallight:
|
||||
@ -56,6 +60,10 @@ bj352.airmonitor.m30:
|
||||
ts: 1686644541
|
||||
bj352.waterpuri.s100cm:
|
||||
ts: 1615795630
|
||||
bymiot.gateway.v1:
|
||||
ts: 1575097876
|
||||
bymiot.gateway.v2:
|
||||
ts: 1575097876
|
||||
cgllc.airmonitor.b1:
|
||||
ts: 1676339912
|
||||
cgllc.airmonitor.s1:
|
||||
@ -64,6 +72,8 @@ cgllc.clock.cgc1:
|
||||
ts: 1686644422
|
||||
cgllc.clock.dove:
|
||||
ts: 1619607474
|
||||
cgllc.gateway.s1:
|
||||
ts: 1575097876
|
||||
cgllc.magnet.hodor:
|
||||
ts: 1724329476
|
||||
cgllc.motion.cgpr1:
|
||||
@ -120,8 +130,14 @@ chuangmi.cateye.ipc018:
|
||||
ts: 1632735241
|
||||
chuangmi.cateye.ipc508:
|
||||
ts: 1633677521
|
||||
chuangmi.door.hmi508:
|
||||
ts: 1611733437
|
||||
chuangmi.door.hmi515:
|
||||
ts: 1640334316
|
||||
chuangmi.gateway.ipc011:
|
||||
ts: 1575097876
|
||||
chuangmi.ir.v2:
|
||||
ts: 1575097876
|
||||
chuangmi.lock.hmi501:
|
||||
ts: 1614742147
|
||||
chuangmi.lock.hmi501b01:
|
||||
@ -142,10 +158,18 @@ chuangmi.plug.v1:
|
||||
ts: 1621925183
|
||||
chuangmi.plug.v3:
|
||||
ts: 1644480255
|
||||
chuangmi.plug.vtl_v1:
|
||||
ts: 1575097876
|
||||
chuangmi.radio.v1:
|
||||
ts: 1531108800
|
||||
chuangmi.radio.v2:
|
||||
ts: 1531108800
|
||||
chuangmi.remote.h102a03:
|
||||
ts: 1575097876
|
||||
chuangmi.remote.h102c01:
|
||||
ts: 1575097876
|
||||
chuangmi.remote.v2:
|
||||
ts: 1575097876
|
||||
chunmi.cooker.eh1:
|
||||
ts: 1607339278
|
||||
chunmi.cooker.eh402:
|
||||
@ -204,6 +228,8 @@ dmaker.airfresh.t2017:
|
||||
ts: 1686731233
|
||||
dmaker.fan.p5:
|
||||
ts: 1655793784
|
||||
doco.fcb.docov001:
|
||||
ts: 1575097876
|
||||
dsm.lock.h3:
|
||||
ts: 1615283790
|
||||
dsm.lock.q3:
|
||||
@ -218,6 +244,30 @@ fawad.airrtc.fwd20011:
|
||||
ts: 1610607149
|
||||
fbs.airmonitor.pth02:
|
||||
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:
|
||||
ts: 1618989537
|
||||
hannto.printer.honey:
|
||||
@ -226,14 +276,26 @@ hannto.printer.honey1s:
|
||||
ts: 1614332725
|
||||
hfjh.fishbowl.v1:
|
||||
ts: 1615278556
|
||||
hhcc.bleflowerpot.v2:
|
||||
ts: 1575097876
|
||||
hhcc.plantmonitor.v1:
|
||||
ts: 1664163526
|
||||
hith.foot_bath.q2:
|
||||
ts: 1531108800
|
||||
hmpace.bracelet.v4:
|
||||
ts: 1575097876
|
||||
hmpace.scales.mibfs:
|
||||
ts: 1575097876
|
||||
hmpace.scales.miscale2:
|
||||
ts: 1575097876
|
||||
huohe.lock.m1:
|
||||
ts: 1635410938
|
||||
huoman.litter_box.co1:
|
||||
ts: 1687165034
|
||||
hutlon.lock.v0001:
|
||||
ts: 1634799698
|
||||
idelan.aircondition.g1:
|
||||
ts: 1575097876
|
||||
idelan.aircondition.v1:
|
||||
ts: 1614666973
|
||||
idelan.aircondition.v2:
|
||||
@ -248,14 +310,22 @@ ikea.light.led1537r6:
|
||||
ts: 1605162872
|
||||
ikea.light.led1545g12:
|
||||
ts: 1605162937
|
||||
ikea.light.led1546g12:
|
||||
ts: 1575097876
|
||||
ikea.light.led1623g12:
|
||||
ts: 1605163009
|
||||
ikea.light.led1649c5:
|
||||
ts: 1605163064
|
||||
ikea.light.led1650r5:
|
||||
ts: 1575097876
|
||||
imibar.cooker.mbihr3:
|
||||
ts: 1624620659
|
||||
imou99.camera.tp2:
|
||||
ts: 1531108800
|
||||
inovel.projector.me2:
|
||||
ts: 1575097876
|
||||
iracc.aircondition.d19:
|
||||
ts: 1609914362
|
||||
isa.camera.df3:
|
||||
ts: 1531108800
|
||||
isa.camera.hl5:
|
||||
@ -266,18 +336,34 @@ isa.camera.isc5:
|
||||
ts: 1531108800
|
||||
isa.camera.isc5c1:
|
||||
ts: 1621238175
|
||||
isa.camera.qf3:
|
||||
ts: 1575097876
|
||||
isa.cateye.hldb6:
|
||||
ts: 1575097876
|
||||
isa.magnet.dw2hl:
|
||||
ts: 1638274655
|
||||
jieman.magic_touch.js78:
|
||||
ts: 1575097876
|
||||
jiqid.mistory.ipen1:
|
||||
ts: 1575097876
|
||||
jiqid.mistory.pro:
|
||||
ts: 1531108800
|
||||
jiqid.mistory.v1:
|
||||
ts: 1531108800
|
||||
jiqid.mistudy.v2:
|
||||
ts: 1610612349
|
||||
jiqid.robot.cube:
|
||||
ts: 1575097876
|
||||
jiwu.lock.jwp01:
|
||||
ts: 1614752632
|
||||
jyaiot.cm.ccj01:
|
||||
ts: 1611824545
|
||||
k0918.toothbrush.kid01:
|
||||
ts: 1575097876
|
||||
kejia.airer.th001:
|
||||
ts: 1575097876
|
||||
ksmb.treadmill.k12:
|
||||
ts: 1575097876
|
||||
ksmb.treadmill.v1:
|
||||
ts: 1611211447
|
||||
ksmb.treadmill.v2:
|
||||
@ -390,6 +476,8 @@ loock.lock.xfvl10:
|
||||
ts: 1632814256
|
||||
loock.safe.v1:
|
||||
ts: 1619607755
|
||||
lumi.acpartner.mcn02:
|
||||
ts: 1655791626
|
||||
lumi.acpartner.v1:
|
||||
ts: 1531108800
|
||||
lumi.acpartner.v2:
|
||||
@ -462,6 +550,8 @@ lumi.lock.acn02:
|
||||
ts: 1623928631
|
||||
lumi.lock.acn03:
|
||||
ts: 1614752574
|
||||
lumi.lock.aq1:
|
||||
ts: 1612518044
|
||||
lumi.lock.bacn01:
|
||||
ts: 1614741699
|
||||
lumi.lock.bmcn02:
|
||||
@ -482,6 +572,8 @@ lumi.lock.mcn007:
|
||||
ts: 1650446757
|
||||
lumi.lock.mcn01:
|
||||
ts: 1679881881
|
||||
lumi.lock.v1:
|
||||
ts: 1575097876
|
||||
lumi.lock.wbmcn1:
|
||||
ts: 1619422072
|
||||
lumi.motion.bmgl01:
|
||||
@ -510,14 +602,20 @@ lumi.sensor_86sw1.v1:
|
||||
ts: 1609311038
|
||||
lumi.sensor_86sw2.v1:
|
||||
ts: 1608795035
|
||||
lumi.sensor_cube.aqgl01:
|
||||
ts: 1575097876
|
||||
lumi.sensor_ht.v1:
|
||||
ts: 1621239877
|
||||
lumi.sensor_magnet.aq2:
|
||||
ts: 1641112867
|
||||
lumi.sensor_magnet.v1:
|
||||
ts: 1606120416
|
||||
lumi.sensor_magnet.v2:
|
||||
ts: 1641113779
|
||||
lumi.sensor_motion.aq2:
|
||||
ts: 1676433994
|
||||
lumi.sensor_motion.v1:
|
||||
ts: 1605093075
|
||||
lumi.sensor_motion.v2:
|
||||
ts: 1672818550
|
||||
lumi.sensor_natgas.v1:
|
||||
@ -530,6 +628,8 @@ lumi.sensor_switch.aq2:
|
||||
ts: 1615256430
|
||||
lumi.sensor_switch.aq3:
|
||||
ts: 1607399487
|
||||
lumi.sensor_switch.v1:
|
||||
ts: 1606874434
|
||||
lumi.sensor_switch.v2:
|
||||
ts: 1609310683
|
||||
lumi.sensor_wleak.aq1:
|
||||
@ -574,6 +674,20 @@ miaomiaoce.sensor_ht.t1:
|
||||
ts: 1616057242
|
||||
miaomiaoce.sensor_ht.t2:
|
||||
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:
|
||||
ts: 1531108800
|
||||
miir.aircondition.ir02:
|
||||
@ -612,6 +726,8 @@ minij.washer.v5:
|
||||
ts: 1622792196
|
||||
minij.washer.v8:
|
||||
ts: 1615777868
|
||||
minuo.tracker.lm001:
|
||||
ts: 1575097876
|
||||
miot.light.plato2:
|
||||
ts: 1685518142
|
||||
miot.light.plato3:
|
||||
@ -624,18 +740,32 @@ mmgg.feeder.snack:
|
||||
ts: 1607503182
|
||||
moyu.washer.s1hm:
|
||||
ts: 1624620888
|
||||
mrbond.airer.m0:
|
||||
ts: 1575097876
|
||||
mrbond.airer.m1pro:
|
||||
ts: 1646393746
|
||||
mrbond.airer.m1s:
|
||||
ts: 1646393874
|
||||
mrbond.airer.m1super:
|
||||
ts: 1575097876
|
||||
msj.f_washer.m1:
|
||||
ts: 1614914340
|
||||
mxiang.cateye.mdb10:
|
||||
ts: 1616140362
|
||||
mxiang.cateye.xmcatt1:
|
||||
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:
|
||||
ts: 1611822375
|
||||
onemore.wifispeaker.sm4:
|
||||
ts: 1575097876
|
||||
opple.light.bydceiling:
|
||||
ts: 1608187619
|
||||
opple.light.fanlight:
|
||||
@ -646,6 +776,8 @@ opple.remote.5pb112:
|
||||
ts: 1627453840
|
||||
opple.remote.5pb113:
|
||||
ts: 1636599905
|
||||
orion.wifispeaker.cm1:
|
||||
ts: 1575097876
|
||||
ows.towel_w.mj1x0:
|
||||
ts: 1610604939
|
||||
philips.light.bceiling1:
|
||||
@ -696,6 +828,8 @@ pwzn.relay.apple:
|
||||
ts: 1611217196
|
||||
pwzn.relay.banana:
|
||||
ts: 1646647255
|
||||
qicyc.bike.tdp02z:
|
||||
ts: 1575097876
|
||||
qike.bhf_light.qk201801:
|
||||
ts: 1608174909
|
||||
qmi.powerstrip.v1:
|
||||
@ -726,8 +860,32 @@ roborock.vacuum.t6:
|
||||
ts: 1619423841
|
||||
rockrobo.vacuum.v1:
|
||||
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:
|
||||
ts: 1531108800
|
||||
rotai.magic_touch.sx300:
|
||||
ts: 1602662578
|
||||
rotai.massage.rt5728:
|
||||
ts: 1610607000
|
||||
rotai.massage.rt5850:
|
||||
@ -738,22 +896,42 @@ rotai.massage.rt5863:
|
||||
ts: 1611827937
|
||||
rotai.massage.rt5870:
|
||||
ts: 1632376570
|
||||
runmi.suitcase.v1:
|
||||
ts: 1575097876
|
||||
scishare.coffee.s1102:
|
||||
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:
|
||||
ts: 1606376290
|
||||
skyrc.feeder.dfeed:
|
||||
ts: 1626082349
|
||||
skyrc.pet_waterer.fre1:
|
||||
ts: 1608186812
|
||||
smith.w_soften.cxs05ta1:
|
||||
ts: 1575097876
|
||||
smith.waterheater.cxea1:
|
||||
ts: 1611826349
|
||||
smith.waterheater.cxeb1:
|
||||
ts: 1611826388
|
||||
smith.waterpuri.jnt600:
|
||||
ts: 1531108800
|
||||
soocare.toothbrush.m1:
|
||||
ts: 1575097876
|
||||
soocare.toothbrush.m1s:
|
||||
ts: 1610611310
|
||||
soocare.toothbrush.mc1:
|
||||
ts: 1575097876
|
||||
soocare.toothbrush.t501:
|
||||
ts: 1672192586
|
||||
soocare.toothbrush.x3:
|
||||
ts: 1575097876
|
||||
sxds.pillow.pillow02:
|
||||
ts: 1611222235
|
||||
syniot.curtain.syc1:
|
||||
@ -778,6 +956,10 @@ tokit.oven.tk32pro1:
|
||||
ts: 1617002408
|
||||
tokit.pre_cooker.tkih1:
|
||||
ts: 1607410832
|
||||
trios1.bleshoes.v02:
|
||||
ts: 1602662599
|
||||
txdd.wifispeaker.x1:
|
||||
ts: 1575097876
|
||||
viomi.aircondition.v10:
|
||||
ts: 1606375041
|
||||
viomi.aircondition.v21:
|
||||
@ -830,12 +1012,16 @@ viomi.fridge.u13:
|
||||
ts: 1614667152
|
||||
viomi.fridge.u15:
|
||||
ts: 1607505693
|
||||
viomi.fridge.u17:
|
||||
ts: 1575097876
|
||||
viomi.fridge.u18:
|
||||
ts: 1614655755
|
||||
viomi.fridge.u2:
|
||||
ts: 1531108800
|
||||
viomi.fridge.u24:
|
||||
ts: 1614667214
|
||||
viomi.fridge.u25:
|
||||
ts: 1575097876
|
||||
viomi.fridge.u4:
|
||||
ts: 1614667295
|
||||
viomi.fridge.u6:
|
||||
@ -992,6 +1178,82 @@ xiaomi.aircondition.ma6:
|
||||
ts: 1721629272
|
||||
xiaomi.aircondition.ma9:
|
||||
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:
|
||||
ts: 1658817956
|
||||
xiaomi.wifispeaker.l06a:
|
||||
@ -1012,6 +1274,10 @@ xiaomi.wifispeaker.lx5a:
|
||||
ts: 1672299577
|
||||
xiaomi.wifispeaker.s12:
|
||||
ts: 1672299594
|
||||
xiaomi.wifispeaker.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.wifispeaker.v3:
|
||||
ts: 1575097876
|
||||
xiaomi.wifispeaker.x08a:
|
||||
ts: 1672818945
|
||||
xiaomi.wifispeaker.x08c:
|
||||
@ -1028,6 +1294,44 @@ xiaovv.camera.xvd5:
|
||||
ts: 1531108800
|
||||
xiaovv.camera.xvsnowman:
|
||||
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:
|
||||
ts: 1615965466
|
||||
xjx.toilet.pure:
|
||||
@ -1054,6 +1358,8 @@ yeelink.bhf_light.v3:
|
||||
ts: 1608790102
|
||||
yeelink.bhf_light.v5:
|
||||
ts: 1601292562
|
||||
yeelink.gateway.v1:
|
||||
ts: 1575097876
|
||||
yeelink.light.bslamp1:
|
||||
ts: 1703120679
|
||||
yeelink.light.bslamp2:
|
||||
@ -1192,6 +1498,10 @@ yunmi.kettle.r2:
|
||||
ts: 1606372087
|
||||
yunmi.kettle.r3:
|
||||
ts: 1637309534
|
||||
yunmi.kettle.v1:
|
||||
ts: 1575097876
|
||||
yunmi.kettle.v9:
|
||||
ts: 1602662686
|
||||
yunmi.plmachine.mg2:
|
||||
ts: 1611833658
|
||||
yunmi.waterpuri.c5:
|
||||
@ -1230,18 +1540,26 @@ yunmi.waterpurifier.v2:
|
||||
ts: 1632377061
|
||||
yunmi.waterpurifier.v3:
|
||||
ts: 1611221428
|
||||
yunyi.camera.v1:
|
||||
ts: 1575097876
|
||||
yyunyi.wopener.yypy24:
|
||||
ts: 1616741966
|
||||
yyzhn.gateway.yn181126:
|
||||
ts: 1610689325
|
||||
zdeer.ajh.a8:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.a9:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.ajb:
|
||||
ts: 1608276454
|
||||
zdeer.ajh.zda10:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.zda9:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.zjy:
|
||||
ts: 1531108800
|
||||
zhij.toothbrush.bv1:
|
||||
ts: 1575097876
|
||||
zhimi.aircondition.ma1:
|
||||
ts: 1615185265
|
||||
zhimi.aircondition.ma3:
|
||||
@ -1250,6 +1568,8 @@ zhimi.aircondition.ma4:
|
||||
ts: 1626334057
|
||||
zhimi.aircondition.v1:
|
||||
ts: 1610610931
|
||||
zhimi.aircondition.v2:
|
||||
ts: 1575097876
|
||||
zhimi.aircondition.va1:
|
||||
ts: 1609924720
|
||||
zhimi.aircondition.za1:
|
||||
@ -1276,8 +1596,12 @@ zhimi.airpurifier.sa2:
|
||||
ts: 1635820002
|
||||
zhimi.airpurifier.v1:
|
||||
ts: 1635855633
|
||||
zhimi.airpurifier.v2:
|
||||
ts: 1575097876
|
||||
zhimi.airpurifier.v3:
|
||||
ts: 1676339933
|
||||
zhimi.airpurifier.v5:
|
||||
ts: 1575097876
|
||||
zhimi.airpurifier.v6:
|
||||
ts: 1636978652
|
||||
zhimi.airpurifier.v7:
|
||||
@ -1318,3 +1642,5 @@ zimi.mosq.v1:
|
||||
ts: 1620728957
|
||||
zimi.powerstrip.v2:
|
||||
ts: 1620812714
|
||||
zimi.projector.v1:
|
||||
ts: 1575097876
|
||||
|
||||
@ -257,6 +257,7 @@ class MIoTClient:
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=self._entry_data['oauth_redirect_url'],
|
||||
cloud_server=self._cloud_server,
|
||||
uuid=self._entry_data["uuid"],
|
||||
loop=self._main_loop)
|
||||
# MIoT http client instance
|
||||
self._http = MIoTHttpClient(
|
||||
|
||||
@ -75,10 +75,11 @@ class MIoTOauthClient:
|
||||
_oauth_host: str
|
||||
_client_id: int
|
||||
_redirect_url: str
|
||||
_device_id: str
|
||||
|
||||
def __init__(
|
||||
self, client_id: str, redirect_url: str, cloud_server: str,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_running_loop()
|
||||
if client_id is None or client_id.strip() == '':
|
||||
@ -87,6 +88,8 @@ class MIoTOauthClient:
|
||||
raise MIoTOauthError('invalid redirect_url')
|
||||
if not cloud_server:
|
||||
raise MIoTOauthError('invalid cloud_server')
|
||||
if not uuid:
|
||||
raise MIoTOauthError('invalid uuid')
|
||||
|
||||
self._client_id = int(client_id)
|
||||
self._redirect_url = redirect_url
|
||||
@ -94,6 +97,7 @@ class MIoTOauthClient:
|
||||
self._oauth_host = DEFAULT_OAUTH2_API_HOST
|
||||
else:
|
||||
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
|
||||
self._device_id = f'ha.{uuid}'
|
||||
self._session = aiohttp.ClientSession(loop=self._main_loop)
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
@ -132,6 +136,7 @@ class MIoTOauthClient:
|
||||
'redirect_uri': redirect_url or self._redirect_url,
|
||||
'client_id': self._client_id,
|
||||
'response_type': 'code',
|
||||
'device_id': self._device_id
|
||||
}
|
||||
if state:
|
||||
params['state'] = state
|
||||
@ -191,6 +196,7 @@ class MIoTOauthClient:
|
||||
'client_id': self._client_id,
|
||||
'redirect_uri': self._redirect_url,
|
||||
'code': code,
|
||||
'device_id': self._device_id
|
||||
})
|
||||
|
||||
async def refresh_access_token_async(self, refresh_token: str) -> dict:
|
||||
@ -531,9 +537,18 @@ class MIoTHttpClient:
|
||||
name = device.get('name', None)
|
||||
urn = device.get('spec_type', None)
|
||||
model = device.get('model', None)
|
||||
if did is None or name is None or urn is None or model is None:
|
||||
_LOGGER.error(
|
||||
'get_device_list, cloud, invalid device, %s', device)
|
||||
if did is None or name is None:
|
||||
_LOGGER.info(
|
||||
'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
|
||||
device_infos[did] = {
|
||||
'did': did,
|
||||
@ -634,7 +649,7 @@ class MIoTHttpClient:
|
||||
for did in dids:
|
||||
if did not in results:
|
||||
devices.pop(did, None)
|
||||
_LOGGER.error('get device info failed, %s', did)
|
||||
_LOGGER.info('get device info failed, %s', did)
|
||||
continue
|
||||
devices[did].update(results[did])
|
||||
# Whether sub devices
|
||||
@ -720,7 +735,7 @@ class MIoTHttpClient:
|
||||
prop_obj['fut'].set_result(None)
|
||||
if props_req:
|
||||
_LOGGER.info(
|
||||
'get prop from cloud failed, %s, %s', len(key), props_req)
|
||||
'get prop from cloud failed, %s', props_req)
|
||||
|
||||
if self._get_prop_list:
|
||||
self._get_prop_timer = self._main_loop.call_later(
|
||||
|
||||
@ -94,7 +94,9 @@ from .miot_spec import (
|
||||
MIoTSpecEvent,
|
||||
MIoTSpecInstance,
|
||||
MIoTSpecProperty,
|
||||
MIoTSpecService
|
||||
MIoTSpecService,
|
||||
MIoTSpecValueList,
|
||||
MIoTSpecValueRange
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -511,7 +513,7 @@ class MIoTDevice:
|
||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['access']):
|
||||
return None
|
||||
if prop.format_ not in SPEC_PROP_TRANS_MAP[
|
||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['format']:
|
||||
return None
|
||||
if prop.unit:
|
||||
@ -564,9 +566,9 @@ class MIoTDevice:
|
||||
# general conversion
|
||||
if not prop.platform:
|
||||
if prop.writable:
|
||||
if prop.format_ == 'str':
|
||||
if prop.format_ == str:
|
||||
prop.platform = 'text'
|
||||
elif prop.format_ == 'bool':
|
||||
elif prop.format_ == bool:
|
||||
prop.platform = 'switch'
|
||||
prop.device_class = SwitchDeviceClass.SWITCH
|
||||
elif prop.value_list:
|
||||
@ -836,18 +838,20 @@ class MIoTServiceEntity(Entity):
|
||||
self.miot_device.unsub_event(
|
||||
siid=event.service.iid, eiid=event.iid)
|
||||
|
||||
def get_map_description(self, map_: dict[int, Any], key: int) -> Any:
|
||||
def get_map_value(
|
||||
self, map_: dict[int, Any], key: int
|
||||
) -> Any:
|
||||
if map_ is None:
|
||||
return None
|
||||
return map_.get(key, None)
|
||||
|
||||
def get_map_value(
|
||||
self, map_: dict[int, Any], description: Any
|
||||
def get_map_key(
|
||||
self, map_: dict[int, Any], value: Any
|
||||
) -> Optional[int]:
|
||||
if map_ is None:
|
||||
return None
|
||||
for key, value in map_.items():
|
||||
if value == description:
|
||||
for key, value_ in map_.items():
|
||||
if value_ == value:
|
||||
return key
|
||||
return None
|
||||
|
||||
@ -1006,10 +1010,9 @@ class MIoTPropertyEntity(Entity):
|
||||
service: MIoTSpecService
|
||||
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
# {'min':int, 'max':int, 'step': int}
|
||||
_value_range: dict[str, int]
|
||||
_value_range: Optional[MIoTSpecValueRange]
|
||||
# {Any: Any}
|
||||
_value_list: Optional[dict[Any, Any]]
|
||||
_value_list: Optional[MIoTSpecValueList]
|
||||
_value: Any
|
||||
|
||||
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
|
||||
@ -1022,11 +1025,7 @@ class MIoTPropertyEntity(Entity):
|
||||
self.service = spec.service
|
||||
self._main_loop = miot_device.miot_client.main_loop
|
||||
self._value_range = spec.value_range
|
||||
if spec.value_list:
|
||||
self._value_list = {
|
||||
item['value']: item['description'] for item in spec.value_list}
|
||||
else:
|
||||
self._value_list = None
|
||||
self._value_list = spec.value_list
|
||||
self._value = None
|
||||
self._pending_write_ha_state_timer = None
|
||||
# Gen entity_id
|
||||
@ -1077,15 +1076,12 @@ class MIoTPropertyEntity(Entity):
|
||||
def get_vlist_description(self, value: Any) -> Optional[str]:
|
||||
if not self._value_list:
|
||||
return None
|
||||
return self._value_list.get(value, None)
|
||||
return self._value_list.get_description_by_value(value)
|
||||
|
||||
def get_vlist_value(self, description: str) -> Any:
|
||||
if not self._value_list:
|
||||
return None
|
||||
for key, value in self._value_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
return self._value_list.get_value_by_description(description)
|
||||
|
||||
async def set_property_async(self, value: Any) -> bool:
|
||||
if not self.spec.writable:
|
||||
|
||||
@ -46,13 +46,11 @@ off Xiaomi or its affiliates' products.
|
||||
MIoT-Spec-V2 parser.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import platform
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
from typing import Any, Optional, Type, Union
|
||||
import logging
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
@ -62,53 +60,162 @@ from .miot_error import MIoTSpecError
|
||||
from .miot_storage import (
|
||||
MIoTStorage,
|
||||
SpecBoolTranslation,
|
||||
SpecFilter,
|
||||
SpecMultiLang)
|
||||
SpecFilter)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _MIoTSpecValueRange:
|
||||
class MIoTSpecValueRange:
|
||||
"""MIoT SPEC value range class."""
|
||||
min_: int
|
||||
max_: 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.max_ = value_range[1]
|
||||
self.step = value_range[2]
|
||||
|
||||
def to_list(self) -> list:
|
||||
return [self.min_, self.max_, self.step]
|
||||
def dump(self) -> dict:
|
||||
return {
|
||||
'min': self.min_,
|
||||
'max': self.max_,
|
||||
'step': self.step
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'[{self.min_}, {self.max_}, {self.step}'
|
||||
|
||||
|
||||
class _MIoTSpecValueListItem:
|
||||
class MIoTSpecValueListItem:
|
||||
"""MIoT SPEC value list item class."""
|
||||
# All lower-case SPEC description.
|
||||
# NOTICE: bool type without name
|
||||
name: str
|
||||
# Value
|
||||
value: Any
|
||||
# Descriptions after multilingual conversion.
|
||||
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 {
|
||||
'name': self.name,
|
||||
'value': self.value,
|
||||
'description': self.description
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name}: {self.value} - {self.description}'
|
||||
|
||||
class _MIoTSpecValueList:
|
||||
|
||||
class MIoTSpecValueList:
|
||||
"""MIoT SPEC value list class."""
|
||||
items: list[_MIoTSpecValueListItem]
|
||||
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:
|
||||
return {item.value: item.description for item in self.items}
|
||||
|
||||
def to_list(self) -> list:
|
||||
return [item.to_dict() for item in self.items]
|
||||
def get_value_by_description(self, description: str) -> Any:
|
||||
for item in self.items:
|
||||
if item.description == description:
|
||||
return item.value
|
||||
return None
|
||||
|
||||
def get_description_by_value(self, value: Any) -> Optional[str]:
|
||||
for item in self.items:
|
||||
if item.value == value:
|
||||
return item.description
|
||||
return None
|
||||
|
||||
def dump(self) -> list:
|
||||
return [item.dump() for item in self.items]
|
||||
|
||||
|
||||
class _SpecStdLib:
|
||||
@ -132,7 +239,7 @@ class _SpecStdLib:
|
||||
|
||||
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 (
|
||||
not isinstance(std_lib, dict)
|
||||
or 'devices' not in std_lib
|
||||
@ -211,7 +318,7 @@ class _SpecStdLib:
|
||||
async def refresh_async(self) -> bool:
|
||||
std_lib_new = await self.__request_from_cloud_async()
|
||||
if std_lib_new:
|
||||
self.from_dict(std_lib_new)
|
||||
self.load(std_lib_new)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -353,7 +460,7 @@ class _SpecStdLib:
|
||||
return result
|
||||
|
||||
|
||||
class MIoTSpecBase:
|
||||
class _MIoTSpecBase:
|
||||
"""MIoT SPEC base class."""
|
||||
iid: int
|
||||
type_: str
|
||||
@ -397,26 +504,32 @@ class MIoTSpecBase:
|
||||
return self.spec_id == value.spec_id
|
||||
|
||||
|
||||
class MIoTSpecProperty(MIoTSpecBase):
|
||||
class MIoTSpecProperty(_MIoTSpecBase):
|
||||
"""MIoT SPEC property class."""
|
||||
format_: str
|
||||
precision: int
|
||||
unit: Optional[str]
|
||||
precision: int
|
||||
|
||||
value_range: Optional[list]
|
||||
value_list: Optional[list[dict]]
|
||||
_format_: Type
|
||||
_value_range: Optional[MIoTSpecValueRange]
|
||||
_value_list: Optional[MIoTSpecValueList]
|
||||
|
||||
_access: list
|
||||
_writable: bool
|
||||
_readable: bool
|
||||
_notifiable: bool
|
||||
|
||||
service: MIoTSpecBase
|
||||
service: 'MIoTSpecService'
|
||||
|
||||
def __init__(
|
||||
self, spec: dict, service: MIoTSpecBase, format_: str, access: list,
|
||||
unit: Optional[str] = None, value_range: Optional[list] = None,
|
||||
value_list: Optional[list[dict]] = None, precision: int = 0
|
||||
self,
|
||||
spec: dict,
|
||||
service: 'MIoTSpecService',
|
||||
format_: str,
|
||||
access: list,
|
||||
unit: Optional[str] = None,
|
||||
value_range: Optional[dict] = None,
|
||||
value_list: Optional[list[dict]] = None,
|
||||
precision: Optional[int] = None
|
||||
) -> None:
|
||||
super().__init__(spec=spec)
|
||||
self.service = service
|
||||
@ -425,11 +538,24 @@ class MIoTSpecProperty(MIoTSpecBase):
|
||||
self.unit = unit
|
||||
self.value_range = value_range
|
||||
self.value_list = value_list
|
||||
self.precision = precision
|
||||
self.precision = precision or 1
|
||||
|
||||
self.spec_id = hash(
|
||||
f'p.{self.name}.{self.service.iid}.{self.iid}')
|
||||
|
||||
@property
|
||||
def format_(self) -> Type:
|
||||
return self._format_
|
||||
|
||||
@format_.setter
|
||||
def format_(self, value: str) -> None:
|
||||
self._format_ = {
|
||||
'string': str,
|
||||
'str': str,
|
||||
'bool': bool,
|
||||
'float': float}.get(
|
||||
value, int)
|
||||
|
||||
@property
|
||||
def access(self) -> list:
|
||||
return self._access
|
||||
@ -454,15 +580,46 @@ class MIoTSpecProperty(MIoTSpecBase):
|
||||
def notifiable(self):
|
||||
return self._notifiable
|
||||
|
||||
@property
|
||||
def value_range(self) -> Optional[MIoTSpecValueRange]:
|
||||
return self._value_range
|
||||
|
||||
@value_range.setter
|
||||
def value_range(self, value: Union[dict, list, None]) -> None:
|
||||
"""Set value-range, precision."""
|
||||
if not value:
|
||||
self._value_range = None
|
||||
return
|
||||
self._value_range = MIoTSpecValueRange(value_range=value)
|
||||
if isinstance(value, list):
|
||||
self.precision = len(str(value[2]).split(
|
||||
'.')[1].rstrip('0')) if '.' in str(value[2]) else 0
|
||||
|
||||
@property
|
||||
def value_list(self) -> Optional[MIoTSpecValueList]:
|
||||
return self._value_list
|
||||
|
||||
@value_list.setter
|
||||
def value_list(
|
||||
self, value: Union[list[dict], MIoTSpecValueList, None]
|
||||
) -> None:
|
||||
if not value:
|
||||
self._value_list = None
|
||||
return
|
||||
if isinstance(value, list):
|
||||
self._value_list = MIoTSpecValueList(value_list=value)
|
||||
elif isinstance(value, MIoTSpecValueList):
|
||||
self._value_list = value
|
||||
|
||||
def value_format(self, value: Any) -> Any:
|
||||
if value is None:
|
||||
return None
|
||||
if self.format_ == 'int':
|
||||
if self.format_ == int:
|
||||
return int(value)
|
||||
if self.format_ == 'float':
|
||||
if self.format_ == float:
|
||||
return round(value, self.precision)
|
||||
if self.format_ == 'bool':
|
||||
return bool(value in [True, 1, 'true', '1'])
|
||||
if self.format_ == bool:
|
||||
return bool(value in [True, 1, 'True', 'true', '1'])
|
||||
return value
|
||||
|
||||
def dump(self) -> dict:
|
||||
@ -474,26 +631,27 @@ class MIoTSpecProperty(MIoTSpecBase):
|
||||
'description_trans': self.description_trans,
|
||||
'proprietary': self.proprietary,
|
||||
'need_filter': self.need_filter,
|
||||
'format': self.format_,
|
||||
'format': self.format_.__name__,
|
||||
'access': self._access,
|
||||
'unit': self.unit,
|
||||
'value_range': self.value_range,
|
||||
'value_list': self.value_list,
|
||||
'value_range': (
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
class MIoTSpecEvent(MIoTSpecBase):
|
||||
class MIoTSpecEvent(_MIoTSpecBase):
|
||||
"""MIoT SPEC event class."""
|
||||
argument: list[MIoTSpecProperty]
|
||||
service: MIoTSpecBase
|
||||
service: 'MIoTSpecService'
|
||||
|
||||
def __init__(
|
||||
self, spec: dict, service: MIoTSpecBase,
|
||||
argument: list[MIoTSpecProperty] = None
|
||||
self, spec: dict, service: 'MIoTSpecService',
|
||||
argument: Optional[list[MIoTSpecProperty]] = None
|
||||
) -> None:
|
||||
super().__init__(spec=spec)
|
||||
self.argument = argument
|
||||
self.argument = argument or []
|
||||
self.service = service
|
||||
|
||||
self.spec_id = hash(
|
||||
@ -507,25 +665,25 @@ class MIoTSpecEvent(MIoTSpecBase):
|
||||
'description': self.description,
|
||||
'description_trans': self.description_trans,
|
||||
'proprietary': self.proprietary,
|
||||
'need_filter': self.need_filter,
|
||||
'argument': [prop.iid for prop in self.argument],
|
||||
'need_filter': self.need_filter
|
||||
}
|
||||
|
||||
|
||||
class MIoTSpecAction(MIoTSpecBase):
|
||||
class MIoTSpecAction(_MIoTSpecBase):
|
||||
"""MIoT SPEC action class."""
|
||||
in_: list[MIoTSpecProperty]
|
||||
out: list[MIoTSpecProperty]
|
||||
service: MIoTSpecBase
|
||||
service: 'MIoTSpecService'
|
||||
|
||||
def __init__(
|
||||
self, spec: dict, service: MIoTSpecBase = None,
|
||||
in_: list[MIoTSpecProperty] = None,
|
||||
out: list[MIoTSpecProperty] = None
|
||||
self, spec: dict, service: 'MIoTSpecService',
|
||||
in_: Optional[list[MIoTSpecProperty]] = None,
|
||||
out: Optional[list[MIoTSpecProperty]] = None
|
||||
) -> None:
|
||||
super().__init__(spec=spec)
|
||||
self.in_ = in_
|
||||
self.out = out
|
||||
self.in_ = in_ or []
|
||||
self.out = out or []
|
||||
self.service = service
|
||||
|
||||
self.spec_id = hash(
|
||||
@ -538,14 +696,14 @@ class MIoTSpecAction(MIoTSpecBase):
|
||||
'iid': self.iid,
|
||||
'description': self.description,
|
||||
'description_trans': self.description_trans,
|
||||
'proprietary': self.proprietary,
|
||||
'need_filter': self.need_filter,
|
||||
'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."""
|
||||
properties: list[MIoTSpecProperty]
|
||||
events: list[MIoTSpecEvent]
|
||||
@ -566,9 +724,9 @@ class MIoTSpecService(MIoTSpecBase):
|
||||
'description_trans': self.description_trans,
|
||||
'proprietary': self.proprietary,
|
||||
'properties': [prop.dump() for prop in self.properties],
|
||||
'need_filter': self.need_filter,
|
||||
'events': [event.dump() for event in self.events],
|
||||
'actions': [action.dump() for action in self.actions],
|
||||
'need_filter': self.need_filter
|
||||
}
|
||||
|
||||
|
||||
@ -588,8 +746,7 @@ class MIoTSpecInstance:
|
||||
icon: str
|
||||
|
||||
def __init__(
|
||||
self, urn: str = None, name: str = None,
|
||||
description: str = None, description_trans: str = None
|
||||
self, urn: str, name: str, description: str, description_trans: str
|
||||
) -> None:
|
||||
self.urn = urn
|
||||
self.name = name
|
||||
@ -597,12 +754,13 @@ class MIoTSpecInstance:
|
||||
self.description_trans = description_trans
|
||||
self.services = []
|
||||
|
||||
def load(self, specs: dict) -> 'MIoTSpecInstance':
|
||||
self.urn = specs['urn']
|
||||
self.name = specs['name']
|
||||
self.description = specs['description']
|
||||
self.description_trans = specs['description_trans']
|
||||
self.services = []
|
||||
@staticmethod
|
||||
def load(specs: dict) -> 'MIoTSpecInstance':
|
||||
instance = MIoTSpecInstance(
|
||||
urn=specs['urn'],
|
||||
name=specs['name'],
|
||||
description=specs['description'],
|
||||
description_trans=specs['description_trans'])
|
||||
for service in specs['services']:
|
||||
spec_service = MIoTSpecService(spec=service)
|
||||
for prop in service['properties']:
|
||||
@ -614,7 +772,7 @@ class MIoTSpecInstance:
|
||||
unit=prop['unit'],
|
||||
value_range=prop['value_range'],
|
||||
value_list=prop['value_list'],
|
||||
precision=prop.get('precision', 0))
|
||||
precision=prop.get('precision', None))
|
||||
spec_service.properties.append(spec_prop)
|
||||
for event in service['events']:
|
||||
spec_event = MIoTSpecEvent(
|
||||
@ -645,8 +803,8 @@ class MIoTSpecInstance:
|
||||
break
|
||||
spec_action.out = out_list
|
||||
spec_service.actions.append(spec_action)
|
||||
self.services.append(spec_service)
|
||||
return self
|
||||
instance.services.append(spec_service)
|
||||
return instance
|
||||
|
||||
def dump(self) -> dict:
|
||||
return {
|
||||
@ -658,22 +816,16 @@ class MIoTSpecInstance:
|
||||
}
|
||||
|
||||
|
||||
class MIoTSpecParser:
|
||||
"""MIoT SPEC parser."""
|
||||
# pylint: disable=inconsistent-quotes
|
||||
VERSION: int = 1
|
||||
DOMAIN: str = 'miot_specs'
|
||||
class _MIoTSpecMultiLang:
|
||||
"""MIoT SPEC multi lang class."""
|
||||
# pylint: disable=broad-exception-caught
|
||||
_DOMAIN: str = 'miot_specs_multi_lang'
|
||||
_lang: str
|
||||
_storage: MIoTStorage
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
|
||||
_init_done: bool
|
||||
_ram_cache: dict
|
||||
|
||||
_std_lib: _SpecStdLib
|
||||
_bool_trans: SpecBoolTranslation
|
||||
_multi_lang: SpecMultiLang
|
||||
_spec_filter: SpecFilter
|
||||
_custom_cache: dict[str, dict]
|
||||
_current_data: Optional[dict[str, str]]
|
||||
|
||||
def __init__(
|
||||
self, lang: Optional[str],
|
||||
@ -684,23 +836,137 @@ class MIoTSpecParser:
|
||||
self._storage = storage
|
||||
self._main_loop = loop or asyncio.get_running_loop()
|
||||
|
||||
self._init_done = False
|
||||
self._ram_cache = {}
|
||||
self._custom_cache = {}
|
||||
self._current_data = None
|
||||
|
||||
async def set_spec_async(self, urn: str) -> None:
|
||||
if urn in self._custom_cache:
|
||||
self._current_data = self._custom_cache[urn]
|
||||
return
|
||||
|
||||
trans_cache: dict[str, str] = {}
|
||||
trans_cloud: dict = {}
|
||||
trans_local: dict = {}
|
||||
# Get multi lang from cloud
|
||||
try:
|
||||
trans_cloud = await self.__get_multi_lang_async(urn)
|
||||
if self._lang == 'zh-Hans':
|
||||
# Simplified Chinese
|
||||
trans_cache = trans_cloud.get('zh_cn', {})
|
||||
elif self._lang == 'zh-Hant':
|
||||
# Traditional Chinese, zh_hk or zh_tw
|
||||
trans_cache = trans_cloud.get('zh_hk', {})
|
||||
if not trans_cache:
|
||||
trans_cache = trans_cloud.get('zh_tw', {})
|
||||
else:
|
||||
trans_cache = trans_cloud.get(self._lang, {})
|
||||
except Exception as err:
|
||||
trans_cloud = {}
|
||||
_LOGGER.info('get multi lang from cloud failed, %s, %s', urn, err)
|
||||
# Get multi lang from local
|
||||
try:
|
||||
trans_local = await self._storage.load_async(
|
||||
domain=self._DOMAIN, name=urn, type_=dict) # type: ignore
|
||||
if (
|
||||
isinstance(trans_local, dict)
|
||||
and self._lang in trans_local
|
||||
):
|
||||
trans_cache.update(trans_local[self._lang])
|
||||
except Exception as err:
|
||||
trans_local = {}
|
||||
_LOGGER.info('get multi lang from local failed, %s, %s', urn, err)
|
||||
# Default language
|
||||
if not trans_cache:
|
||||
if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:
|
||||
trans_cache = trans_cloud[DEFAULT_INTEGRATION_LANGUAGE]
|
||||
if trans_local and DEFAULT_INTEGRATION_LANGUAGE in trans_local:
|
||||
trans_cache.update(
|
||||
trans_local[DEFAULT_INTEGRATION_LANGUAGE])
|
||||
trans_data: dict[str, str] = {}
|
||||
for tag, value in trans_cache.items():
|
||||
if value is None or value.strip() == '':
|
||||
continue
|
||||
# The dict key is like:
|
||||
# 'service:002:property:001:valuelist:000' or
|
||||
# 'service:002:property:001' or 'service:002'
|
||||
strs: list = tag.split(':')
|
||||
strs_len = len(strs)
|
||||
if strs_len == 2:
|
||||
trans_data[f's:{int(strs[1])}'] = value
|
||||
elif strs_len == 4:
|
||||
type_ = 'p' if strs[2] == 'property' else (
|
||||
'a' if strs[2] == 'action' else 'e')
|
||||
trans_data[
|
||||
f'{type_}:{int(strs[1])}:{int(strs[3])}'
|
||||
] = value
|
||||
elif strs_len == 6:
|
||||
trans_data[
|
||||
f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}'
|
||||
] = value
|
||||
|
||||
self._custom_cache[urn] = trans_data
|
||||
self._current_data = trans_data
|
||||
|
||||
def translate(self, key: str) -> Optional[str]:
|
||||
if not self._current_data:
|
||||
return None
|
||||
return self._current_data.get(key, None)
|
||||
|
||||
async def __get_multi_lang_async(self, urn: str) -> dict:
|
||||
res_trans = await MIoTHttp.get_json_async(
|
||||
url='https://miot-spec.org/instance/v2/multiLanguage',
|
||||
params={'urn': urn})
|
||||
if (
|
||||
not isinstance(res_trans, dict)
|
||||
or 'data' not in res_trans
|
||||
or not isinstance(res_trans['data'], dict)
|
||||
):
|
||||
raise MIoTSpecError('invalid translation data')
|
||||
return res_trans['data']
|
||||
|
||||
|
||||
class MIoTSpecParser:
|
||||
"""MIoT SPEC parser."""
|
||||
# pylint: disable=inconsistent-quotes
|
||||
VERSION: int = 1
|
||||
_DOMAIN: str = 'miot_specs'
|
||||
_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._multi_lang = _MIoTSpecMultiLang(
|
||||
lang=self._lang, storage=self._storage, loop=self._main_loop)
|
||||
|
||||
self._init_done = False
|
||||
|
||||
self._bool_trans = SpecBoolTranslation(
|
||||
lang=self._lang, loop=self._main_loop)
|
||||
self._multi_lang = SpecMultiLang(lang=self._lang, loop=self._main_loop)
|
||||
self._spec_filter = SpecFilter(loop=self._main_loop)
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if self._init_done is True:
|
||||
return
|
||||
await self._bool_trans.init_async()
|
||||
await self._multi_lang.init_async()
|
||||
await self._spec_filter.init_async()
|
||||
std_lib_cache = await self._storage.load_async(
|
||||
domain=self.DOMAIN, name='spec_std_lib', type_=dict)
|
||||
domain=self._DOMAIN, name='spec_std_lib', type_=dict)
|
||||
if (
|
||||
isinstance(std_lib_cache, dict)
|
||||
and 'data' in std_lib_cache
|
||||
@ -712,13 +978,13 @@ class MIoTSpecParser:
|
||||
# Use the cache if the update time is less than 14 day
|
||||
_LOGGER.debug(
|
||||
'use local spec std cache, ts->%s', std_lib_cache['ts'])
|
||||
self._std_lib.from_dict(std_lib_cache['data'])
|
||||
self._std_lib.load(std_lib_cache['data'])
|
||||
self._init_done = True
|
||||
return
|
||||
# Update spec std lib
|
||||
if await self._std_lib.refresh_async():
|
||||
if not await self._storage.save_async(
|
||||
domain=self.DOMAIN, name='spec_std_lib',
|
||||
domain=self._DOMAIN, name='spec_std_lib',
|
||||
data={
|
||||
'data': self._std_lib.dump(),
|
||||
'ts': int(time.time())
|
||||
@ -727,7 +993,7 @@ class MIoTSpecParser:
|
||||
_LOGGER.error('save spec std lib failed')
|
||||
else:
|
||||
if isinstance(std_lib_cache, dict) and 'data' in std_lib_cache:
|
||||
self._std_lib.from_dict(std_lib_cache['data'])
|
||||
self._std_lib.load(std_lib_cache['data'])
|
||||
_LOGGER.info('get spec std lib failed, use local cache')
|
||||
else:
|
||||
_LOGGER.error('load spec std lib failed')
|
||||
@ -737,19 +1003,17 @@ class MIoTSpecParser:
|
||||
self._init_done = False
|
||||
# self._std_lib.deinit()
|
||||
await self._bool_trans.deinit_async()
|
||||
await self._multi_lang.deinit_async()
|
||||
await self._spec_filter.deinit_async()
|
||||
self._ram_cache.clear()
|
||||
|
||||
async def parse(
|
||||
self, urn: str, skip_cache: bool = False,
|
||||
) -> MIoTSpecInstance:
|
||||
) -> Optional[MIoTSpecInstance]:
|
||||
"""MUST await init first !!!"""
|
||||
if not skip_cache:
|
||||
cache_result = await self.__cache_get(urn=urn)
|
||||
if isinstance(cache_result, dict):
|
||||
_LOGGER.debug('get from cache, %s', urn)
|
||||
return MIoTSpecInstance().load(specs=cache_result)
|
||||
return MIoTSpecInstance.load(specs=cache_result)
|
||||
# Retry three times
|
||||
for index in range(3):
|
||||
try:
|
||||
@ -765,7 +1029,7 @@ class MIoTSpecParser:
|
||||
return False
|
||||
if await self._std_lib.refresh_async():
|
||||
if not await self._storage.save_async(
|
||||
domain=self.DOMAIN, name='spec_std_lib',
|
||||
domain=self._DOMAIN, name='spec_std_lib',
|
||||
data={
|
||||
'data': self._std_lib.dump(),
|
||||
'ts': int(time.time())
|
||||
@ -784,38 +1048,24 @@ class MIoTSpecParser:
|
||||
return success_count
|
||||
|
||||
async def __cache_get(self, urn: str) -> Optional[dict]:
|
||||
if self._storage is not None:
|
||||
if platform.system() == 'Windows':
|
||||
urn = urn.replace(':', '_')
|
||||
return await self._storage.load_async(
|
||||
domain=self.DOMAIN, name=f'{urn}_{self._lang}', type_=dict)
|
||||
return self._ram_cache.get(urn, None)
|
||||
if platform.system() == 'Windows':
|
||||
urn = urn.replace(':', '_')
|
||||
return await self._storage.load_async(
|
||||
domain=self._DOMAIN,
|
||||
name=f'{urn}_{self._lang}',
|
||||
type_=dict) # type: ignore
|
||||
|
||||
async def __cache_set(self, urn: str, data: dict) -> bool:
|
||||
if self._storage is not None:
|
||||
if platform.system() == 'Windows':
|
||||
urn = urn.replace(':', '_')
|
||||
return await self._storage.save_async(
|
||||
domain=self.DOMAIN, name=f'{urn}_{self._lang}', data=data)
|
||||
self._ram_cache[urn] = data
|
||||
return True
|
||||
|
||||
def __spec_format2dtype(self, format_: str) -> str:
|
||||
# 'string'|'bool'|'uint8'|'uint16'|'uint32'|
|
||||
# 'int8'|'int16'|'int32'|'int64'|'float'
|
||||
return {'string': 'str', 'bool': 'bool', 'float': 'float'}.get(
|
||||
format_, 'int')
|
||||
if platform.system() == 'Windows':
|
||||
urn = urn.replace(':', '_')
|
||||
return await self._storage.save_async(
|
||||
domain=self._DOMAIN, name=f'{urn}_{self._lang}', data=data)
|
||||
|
||||
async def __get_instance(self, urn: str) -> Optional[dict]:
|
||||
return await MIoTHttp.get_json_async(
|
||||
url='https://miot-spec.org/miot-spec-v2/instance',
|
||||
params={'type': urn})
|
||||
|
||||
async def __get_translation(self, urn: str) -> Optional[dict]:
|
||||
return await MIoTHttp.get_json_async(
|
||||
url='https://miot-spec.org/instance/v2/multiLanguage',
|
||||
params={'urn': urn})
|
||||
|
||||
async def __parse(self, urn: str) -> MIoTSpecInstance:
|
||||
_LOGGER.debug('parse urn, %s', urn)
|
||||
# Load spec instance
|
||||
@ -827,68 +1077,11 @@ class MIoTSpecParser:
|
||||
or 'services' not in instance
|
||||
):
|
||||
raise MIoTSpecError(f'invalid urn instance, {urn}')
|
||||
translation: dict = {}
|
||||
urn_strs: list[str] = urn.split(':')
|
||||
urn_key: str = ':'.join(urn_strs[:6])
|
||||
try:
|
||||
# Load multiple language configuration.
|
||||
res_trans = await self.__get_translation(urn=urn)
|
||||
if (
|
||||
not isinstance(res_trans, dict)
|
||||
or 'data' not in res_trans
|
||||
or not isinstance(res_trans['data'], dict)
|
||||
):
|
||||
raise MIoTSpecError('invalid translation data')
|
||||
trans_data: dict[str, str] = {}
|
||||
if self._lang == 'zh-Hans':
|
||||
# Simplified Chinese
|
||||
trans_data = res_trans['data'].get('zh_cn', {})
|
||||
elif self._lang == 'zh-Hant':
|
||||
# Traditional Chinese, zh_hk or zh_tw
|
||||
trans_data = res_trans['data'].get('zh_hk', {})
|
||||
if not trans_data:
|
||||
trans_data = res_trans['data'].get('zh_tw', {})
|
||||
else:
|
||||
trans_data = res_trans['data'].get(self._lang, {})
|
||||
# Load local multiple language configuration.
|
||||
multi_lang: dict = await self._multi_lang.translate_async(
|
||||
urn_key=urn_key)
|
||||
if multi_lang:
|
||||
trans_data.update(multi_lang)
|
||||
if not trans_data:
|
||||
trans_data = res_trans['data'].get(
|
||||
DEFAULT_INTEGRATION_LANGUAGE, {})
|
||||
if not trans_data:
|
||||
raise MIoTSpecError(
|
||||
f'the language is not supported, {self._lang}')
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'the language is not supported, %s, try using the '
|
||||
'default language, %s, %s',
|
||||
self._lang, DEFAULT_INTEGRATION_LANGUAGE, urn)
|
||||
for tag, value in trans_data.items():
|
||||
if value is None or value.strip() == '':
|
||||
continue
|
||||
# The dict key is like:
|
||||
# 'service:002:property:001:valuelist:000' or
|
||||
# 'service:002:property:001' or 'service:002'
|
||||
strs: list = tag.split(':')
|
||||
strs_len = len(strs)
|
||||
if strs_len == 2:
|
||||
translation[f's:{int(strs[1])}'] = value
|
||||
elif strs_len == 4:
|
||||
type_ = 'p' if strs[2] == 'property' else (
|
||||
'a' if strs[2] == 'action' else 'e')
|
||||
translation[
|
||||
f'{type_}:{int(strs[1])}:{int(strs[3])}'
|
||||
] = value
|
||||
elif strs_len == 6:
|
||||
translation[
|
||||
f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}'
|
||||
] = value
|
||||
except MIoTSpecError as e:
|
||||
_LOGGER.error('get translation error, %s, %s', urn, e)
|
||||
# Spec filter
|
||||
# Set translation cache
|
||||
await self._multi_lang.set_spec_async(urn=urn_key)
|
||||
# Set spec filter
|
||||
self._spec_filter.filter_spec(urn_key=urn_key)
|
||||
# Parse device type
|
||||
spec_instance: MIoTSpecInstance = MIoTSpecInstance(
|
||||
@ -919,7 +1112,7 @@ class MIoTSpecParser:
|
||||
if type_strs[1] != 'miot-spec-v2':
|
||||
spec_service.proprietary = True
|
||||
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 service['description']
|
||||
or spec_service.name
|
||||
@ -938,7 +1131,7 @@ class MIoTSpecParser:
|
||||
spec_prop: MIoTSpecProperty = MIoTSpecProperty(
|
||||
spec=property_,
|
||||
service=spec_service,
|
||||
format_=self.__spec_format2dtype(property_['format']),
|
||||
format_=property_['format'],
|
||||
access=property_['access'],
|
||||
unit=property_.get('unit', None))
|
||||
spec_prop.name = p_type_strs[3]
|
||||
@ -950,41 +1143,33 @@ class MIoTSpecParser:
|
||||
if p_type_strs[1] != 'miot-spec-v2':
|
||||
spec_prop.proprietary = spec_service.proprietary or True
|
||||
spec_prop.description_trans = (
|
||||
translation.get(
|
||||
f'p:{service["iid"]}:{property_["iid"]}', None)
|
||||
self._multi_lang.translate(
|
||||
f'p:{service["iid"]}:{property_["iid"]}')
|
||||
or self._std_lib.property_translate(
|
||||
key=':'.join(p_type_strs[:5]))
|
||||
or property_['description']
|
||||
or spec_prop.name)
|
||||
if 'value-range' in property_:
|
||||
spec_prop.value_range = {
|
||||
'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
|
||||
spec_prop.value_range = property_['value-range']
|
||||
elif 'value-list' in property_:
|
||||
v_list: list[dict] = property_['value-list']
|
||||
for index, v in enumerate(v_list):
|
||||
v['name'] = v['description']
|
||||
v['description'] = (
|
||||
translation.get(
|
||||
self._multi_lang.translate(
|
||||
f'v:{service["iid"]}:{property_["iid"]}:'
|
||||
f'{index}', None)
|
||||
f'{index}')
|
||||
or self._std_lib.value_translate(
|
||||
key=f'{type_strs[:5]}|{p_type_strs[3]}|'
|
||||
f'{v["description"]}')
|
||||
or v['name']
|
||||
)
|
||||
spec_prop.value_list = v_list
|
||||
or v['name'])
|
||||
spec_prop.value_list = MIoTSpecValueList.from_spec(v_list)
|
||||
elif property_['format'] == 'bool':
|
||||
v_tag = ':'.join(p_type_strs[:5])
|
||||
v_descriptions: dict = (
|
||||
v_descriptions = (
|
||||
await self._bool_trans.translate_async(urn=v_tag))
|
||||
if v_descriptions:
|
||||
# bool without value-list.name
|
||||
spec_prop.value_list = v_descriptions
|
||||
spec_service.properties.append(spec_prop)
|
||||
# Parse service event
|
||||
@ -1008,8 +1193,8 @@ class MIoTSpecParser:
|
||||
if e_type_strs[1] != 'miot-spec-v2':
|
||||
spec_event.proprietary = spec_service.proprietary or True
|
||||
spec_event.description_trans = (
|
||||
translation.get(
|
||||
f'e:{service["iid"]}:{event["iid"]}', None)
|
||||
self._multi_lang.translate(
|
||||
f'e:{service["iid"]}:{event["iid"]}')
|
||||
or self._std_lib.event_translate(
|
||||
key=':'.join(e_type_strs[:5]))
|
||||
or event['description']
|
||||
@ -1044,8 +1229,8 @@ class MIoTSpecParser:
|
||||
if a_type_strs[1] != 'miot-spec-v2':
|
||||
spec_action.proprietary = spec_service.proprietary or True
|
||||
spec_action.description_trans = (
|
||||
translation.get(
|
||||
f'a:{service["iid"]}:{action["iid"]}', None)
|
||||
self._multi_lang.translate(
|
||||
f'a:{service["iid"]}:{action["iid"]}')
|
||||
or self._std_lib.action_translate(
|
||||
key=':'.join(a_type_strs[:5]))
|
||||
or action['description']
|
||||
|
||||
@ -719,60 +719,6 @@ class MIoTCert:
|
||||
return binascii.hexlify(sha1_hash.finalize()).decode('utf-8')
|
||||
|
||||
|
||||
class SpecMultiLang:
|
||||
"""
|
||||
MIoT-Spec-V2 multi-language for entities.
|
||||
"""
|
||||
MULTI_LANG_FILE = 'specs/multi_lang.json'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_lang: str
|
||||
_data: Optional[dict[str, dict]]
|
||||
|
||||
def __init__(
|
||||
self, lang: str, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_event_loop()
|
||||
self._lang = lang
|
||||
self._data = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
multi_lang_data = None
|
||||
self._data = {}
|
||||
try:
|
||||
multi_lang_data = await self._main_loop.run_in_executor(
|
||||
None, load_json_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self.MULTI_LANG_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('multi lang, load file error, %s', err)
|
||||
return
|
||||
# Check if the file is a valid JSON file
|
||||
if not isinstance(multi_lang_data, dict):
|
||||
_LOGGER.error('multi lang, invalid file data')
|
||||
return
|
||||
for lang_data in multi_lang_data.values():
|
||||
if not isinstance(lang_data, dict):
|
||||
_LOGGER.error('multi lang, invalid lang data')
|
||||
return
|
||||
for data in lang_data.values():
|
||||
if not isinstance(data, dict):
|
||||
_LOGGER.error('multi lang, invalid lang data item')
|
||||
return
|
||||
self._data = multi_lang_data
|
||||
|
||||
async def deinit_async(self) -> str:
|
||||
self._data = None
|
||||
|
||||
async def translate_async(self, urn_key: str) -> dict[str, str]:
|
||||
"""MUST call init_async() first."""
|
||||
if urn_key in self._data:
|
||||
return self._data[urn_key].get(self._lang, {})
|
||||
return {}
|
||||
|
||||
|
||||
class SpecBoolTranslation:
|
||||
"""
|
||||
Boolean value translation.
|
||||
|
||||
@ -59,5 +59,10 @@
|
||||
"1",
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
|
||||
"services": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -289,7 +289,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'properties': {'mode', 'horizontal-swing'}
|
||||
'properties': {'mode', 'horizontal-swing', 'wind-reverse'}
|
||||
},
|
||||
'entity': 'fan'
|
||||
},
|
||||
|
||||
@ -90,7 +90,7 @@ class Notify(MIoTActionEntity, NotifyEntity):
|
||||
super().__init__(miot_device=miot_device, spec=spec)
|
||||
self._attr_extra_state_attributes = {}
|
||||
action_in: str = ', '.join([
|
||||
f'{prop.description_trans}({prop.format_})'
|
||||
f'{prop.description_trans}({prop.format_.__name__})'
|
||||
for prop in self.spec.in_])
|
||||
self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
|
||||
|
||||
@ -122,24 +122,24 @@ class Notify(MIoTActionEntity, NotifyEntity):
|
||||
return
|
||||
in_value: list[dict] = []
|
||||
for index, prop in enumerate(self.spec.in_):
|
||||
if prop.format_ == 'str':
|
||||
if prop.format_ == str:
|
||||
if isinstance(in_list[index], (bool, int, float, str)):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': str(in_list[index])})
|
||||
continue
|
||||
elif prop.format_ == 'bool':
|
||||
elif prop.format_ == bool:
|
||||
if isinstance(in_list[index], (bool, int)):
|
||||
# yes, no, on, off, true, false and other bool types
|
||||
# will also be parsed as 0 and 1 of int.
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': bool(in_list[index])})
|
||||
continue
|
||||
elif prop.format_ == 'float':
|
||||
elif prop.format_ == float:
|
||||
if isinstance(in_list[index], (int, float)):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': in_list[index]})
|
||||
continue
|
||||
elif prop.format_ == 'int':
|
||||
elif prop.format_ == int:
|
||||
if isinstance(in_list[index], int):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': in_list[index]})
|
||||
|
||||
@ -92,9 +92,9 @@ class Number(MIoTPropertyEntity, NumberEntity):
|
||||
self._attr_icon = self.spec.icon
|
||||
# Set value range
|
||||
if self._value_range:
|
||||
self._attr_native_min_value = self._value_range['min']
|
||||
self._attr_native_max_value = self._value_range['max']
|
||||
self._attr_native_step = self._value_range['step']
|
||||
self._attr_native_min_value = self._value_range.min_
|
||||
self._attr_native_max_value = self._value_range.max_
|
||||
self._attr_native_step = self._value_range.step
|
||||
|
||||
@property
|
||||
def native_value(self) -> Optional[float]:
|
||||
|
||||
@ -82,7 +82,8 @@ class Select(MIoTPropertyEntity, SelectEntity):
|
||||
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
|
||||
"""Initialize the Select."""
|
||||
super().__init__(miot_device=miot_device, spec=spec)
|
||||
self._attr_options = list(self._value_list.values())
|
||||
if self._value_list:
|
||||
self._attr_options = self._value_list.descriptions
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
|
||||
@ -91,7 +91,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_icon = 'mdi:message-text'
|
||||
self._attr_native_unit_of_measurement = None
|
||||
self._attr_options = list(self._value_list.values())
|
||||
self._attr_options = self._value_list.descriptions
|
||||
else:
|
||||
self._attr_device_class = spec.device_class
|
||||
if spec.external_unit:
|
||||
@ -115,14 +115,14 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
"""Return the current value of the sensor."""
|
||||
if self._value_range and isinstance(self._value, (int, float)):
|
||||
if (
|
||||
self._value < self._value_range['min']
|
||||
or self._value > self._value_range['max']
|
||||
self._value < self._value_range.min_
|
||||
or self._value > self._value_range.max_
|
||||
):
|
||||
_LOGGER.info(
|
||||
'%s, data exception, out of range, %s, %s',
|
||||
self.entity_id, self._value, self._value_range)
|
||||
if self._value_list:
|
||||
return self._value_list.get(self._value, None)
|
||||
return self.get_vlist_description(self._value)
|
||||
if isinstance(self._value, str):
|
||||
return self._value[:255]
|
||||
return self._value
|
||||
|
||||
@ -111,7 +111,7 @@ class ActionText(MIoTActionEntity, TextEntity):
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._attr_native_value = ''
|
||||
action_in: str = ', '.join([
|
||||
f'{prop.description_trans}({prop.format_})'
|
||||
f'{prop.description_trans}({prop.format_.__name__})'
|
||||
for prop in self.spec.in_])
|
||||
self._attr_extra_state_attributes['action params'] = f'[{action_in}]'
|
||||
# For action debug
|
||||
@ -141,24 +141,24 @@ class ActionText(MIoTActionEntity, TextEntity):
|
||||
f'invalid action params, {value}')
|
||||
in_value: list[dict] = []
|
||||
for index, prop in enumerate(self.spec.in_):
|
||||
if prop.format_ == 'str':
|
||||
if prop.format_ == str:
|
||||
if isinstance(in_list[index], (bool, int, float, str)):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': str(in_list[index])})
|
||||
continue
|
||||
elif prop.format_ == 'bool':
|
||||
elif prop.format_ == bool:
|
||||
if isinstance(in_list[index], (bool, int)):
|
||||
# yes, no, on, off, true, false and other bool types
|
||||
# will also be parsed as 0 and 1 of int.
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': bool(in_list[index])})
|
||||
continue
|
||||
elif prop.format_ == 'float':
|
||||
elif prop.format_ == float:
|
||||
if isinstance(in_list[index], (int, float)):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': in_list[index]})
|
||||
continue
|
||||
elif prop.format_ == 'int':
|
||||
elif prop.format_ == int:
|
||||
if isinstance(in_list[index], int):
|
||||
in_value.append(
|
||||
{'piid': prop.iid, 'value': in_list[index]})
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Fehler beim Abrufen der Home Assistant-UUID.",
|
||||
"network_connect_error": "Konfiguration fehlgeschlagen. Netzwerkverbindung fehlgeschlagen. Überprüfen Sie die Netzwerkkonfiguration des Geräts.",
|
||||
"already_configured": "Dieser Benutzer hat die Konfiguration bereits abgeschlossen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Konfiguration zu ändern.",
|
||||
"invalid_auth_info": "Authentifizierungsinformationen sind abgelaufen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Authentifizierung erneut durchzuführen.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Unable to reach Xiaomi MQTT Broker address, please check network configuration."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Failed to get Home Assistant UUID.",
|
||||
"network_connect_error": "Configuration failed. The network connection is abnormal. Please check the equipment network configuration.",
|
||||
"already_configured": "Configuration for this user is already completed. Please go to the integration page and click the CONFIGURE button for modifications.",
|
||||
"invalid_auth_info": "Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.",
|
||||
|
||||
@ -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."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Error al obtener el UUID de Home Assistant.",
|
||||
"network_connect_error": "La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.",
|
||||
"already_configured": "Esta cuenta ya ha finalizado la configuración. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para modificar la configuración.",
|
||||
"invalid_auth_info": "La información de autorización ha caducado. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para volver a autenticarse.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Impossible d'atteindre l'adresse du Broker MQTT de Xiaomi, veuillez vérifier la configuration réseau."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Échec de l'obtention de l'UUID de Home Assistant.",
|
||||
"network_connect_error": "La configuration a échoué. Erreur de connexion réseau. Veuillez vérifier la configuration du réseau de l'appareil.",
|
||||
"already_configured": "Cet utilisateur a déjà terminé la configuration. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour modifier la configuration.",
|
||||
"invalid_auth_info": "Les informations d'authentification ont expiré. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour vous authentifier à nouveau.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Xiaomi MQTT ブローカーアドレスにアクセスできません。ネットワーク設定を確認してください。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Home Assistant インスタンスIDを取得できませんでした。",
|
||||
"network_connect_error": "設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。",
|
||||
"already_configured": "このユーザーはすでに設定が完了しています。統合ページにアクセスして、「設定」ボタンをクリックして設定を変更してください。",
|
||||
"invalid_auth_info": "認証情報が期限切れになりました。統合ページにアクセスして、「設定」ボタンをクリックして再度認証してください。",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Mislukt bij het ophalen van Home Assistant UUID.",
|
||||
"network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.",
|
||||
"already_configured": "Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.",
|
||||
"invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.",
|
||||
|
||||
@ -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."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Falha ao obter o UUID do Home Assistant.",
|
||||
"network_connect_error": "Configuração falhou. A conexão de rede está anormal. Verifique a configuração de rede do equipamento.",
|
||||
"already_configured": "A configuração para este usuário já foi concluída. Vá para a página de integrações e clique no botão CONFIGURAR para modificações.",
|
||||
"invalid_auth_info": "As informações de autenticação expiraram. Vá para a página de integrações e clique em CONFIGURAR para reautenticar.",
|
||||
|
||||
@ -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."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Não foi possível obter o UUID do Home Assistant.",
|
||||
"network_connect_error": "A configuração falhou. A ligação de rede é anormal. Verifique a configuração de rede do equipamento.",
|
||||
"already_configured": "A configuração para este utilizador já foi concluída. Vá à página de integrações e clique em CONFIGURAR para efetuar alterações.",
|
||||
"invalid_auth_info": "A informação de autenticação expirou. Vá à página de integrações e clique em CONFIGURAR para reautenticar.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Не удалось получить UUID Home Assistant.",
|
||||
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
|
||||
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
|
||||
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "无法访问小米 MQTT Broker 地址,请检查网络配置。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "获取 Home Assistant UUID 失败。",
|
||||
"network_connect_error": "配置失败。网络连接异常,请检查设备网络配置。",
|
||||
"already_configured": "该用户已配置完成。请进入集成页面,点击“配置”按钮修改配置。",
|
||||
"invalid_auth_info": "认证信息已过期。请进入集成页面,点击“配置”按钮重新认证。",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "無法訪問小米 MQTT Broker 地址,請檢查網絡配置。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "獲取 Home Assistant UUID 失敗。",
|
||||
"network_connect_error": "配置失敗。網絡連接異常,請檢查設備網絡配置。",
|
||||
"already_configured": "該用戶已配置完成。請進入集成頁面,點擊“配置”按鈕修改配置。",
|
||||
"invalid_auth_info": "認證信息已過期。請進入集成頁面,點擊“配置”按鈕重新認證。",
|
||||
|
||||
@ -120,28 +120,18 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
if prop.name == 'status':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'invalid status value_list, %s', self.entity_id)
|
||||
continue
|
||||
self._status_map = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._status_map = prop.value_list.to_map()
|
||||
self._prop_status = prop
|
||||
elif prop.name == 'fan-level':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'invalid fan-level value_list, %s', self.entity_id)
|
||||
continue
|
||||
self._fan_level_map = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._fan_level_map = prop.value_list.to_map()
|
||||
self._attr_fan_speed_list = list(self._fan_level_map.values())
|
||||
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
||||
self._prop_fan_level = prop
|
||||
@ -202,7 +192,7 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
||||
@property
|
||||
def state(self) -> Optional[str]:
|
||||
"""Return the current state of the vacuum cleaner."""
|
||||
return self.get_map_description(
|
||||
return self.get_map_value(
|
||||
map_=self._status_map,
|
||||
key=self.get_prop_value(prop=self._prop_status))
|
||||
|
||||
@ -214,6 +204,6 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
|
||||
@property
|
||||
def fan_speed(self) -> Optional[str]:
|
||||
"""Return the current fan speed of the vacuum cleaner."""
|
||||
return self.get_map_description(
|
||||
return self.get_map_value(
|
||||
map_=self._fan_level_map,
|
||||
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||
|
||||
@ -93,7 +93,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
_prop_target_temp: Optional[MIoTSpecProperty]
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
|
||||
_mode_list: Optional[dict[Any, Any]]
|
||||
_mode_map: Optional[dict[Any, Any]]
|
||||
|
||||
def __init__(
|
||||
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
|
||||
@ -106,7 +106,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
self._prop_temp = None
|
||||
self._prop_target_temp = None
|
||||
self._prop_mode = None
|
||||
self._mode_list = None
|
||||
self._mode_map = None
|
||||
|
||||
# properties
|
||||
for prop in entity_data.props:
|
||||
@ -115,7 +115,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
self._prop_on = prop
|
||||
# temperature
|
||||
if prop.name == 'temperature':
|
||||
if isinstance(prop.value_range, dict):
|
||||
if prop.value_range:
|
||||
if (
|
||||
self._attr_temperature_unit is None
|
||||
and prop.external_unit
|
||||
@ -128,9 +128,14 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
self.entity_id)
|
||||
# target-temperature
|
||||
if prop.name == 'target-temperature':
|
||||
self._attr_min_temp = prop.value_range['min']
|
||||
self._attr_max_temp = prop.value_range['max']
|
||||
self._attr_precision = prop.value_range['step']
|
||||
if not prop.value_range:
|
||||
_LOGGER.error(
|
||||
'invalid target-temperature value_range format, %s',
|
||||
self.entity_id)
|
||||
continue
|
||||
self._attr_min_temp = prop.value_range.min_
|
||||
self._attr_max_temp = prop.value_range.max_
|
||||
self._attr_precision = prop.value_range.step
|
||||
if self._attr_temperature_unit is None and prop.external_unit:
|
||||
self._attr_temperature_unit = prop.external_unit
|
||||
self._attr_supported_features |= (
|
||||
@ -138,17 +143,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
self._prop_target_temp = prop
|
||||
# mode
|
||||
if prop.name == 'mode':
|
||||
if (
|
||||
not isinstance(prop.value_list, list)
|
||||
or not prop.value_list
|
||||
):
|
||||
if not prop.value_list:
|
||||
_LOGGER.error(
|
||||
'mode value_list is None, %s', self.entity_id)
|
||||
continue
|
||||
self._mode_list = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._attr_operation_list = list(self._mode_list.values())
|
||||
self._mode_map = prop.value_list.to_map()
|
||||
self._attr_operation_list = list(self._mode_map.values())
|
||||
self._attr_supported_features |= (
|
||||
WaterHeaterEntityFeature.OPERATION_MODE)
|
||||
self._prop_mode = prop
|
||||
@ -184,7 +184,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
prop=self._prop_on, value=True, update=False)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_mode,
|
||||
value=self.__get_mode_value(description=operation_mode))
|
||||
value=self.get_map_key(
|
||||
map_=self._mode_map,
|
||||
value=operation_mode))
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Set the water heater to away mode."""
|
||||
@ -207,20 +209,6 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
|
||||
return STATE_OFF
|
||||
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
|
||||
return STATE_ON
|
||||
return self.__get_mode_description(
|
||||
return self.get_map_value(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
||||
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||
"""Convert mode value to description."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
return self._mode_list.get(key, None)
|
||||
|
||||
def __get_mode_value(self, description: str) -> Optional[int]:
|
||||
"""Convert mode description to value."""
|
||||
if self._mode_list is None:
|
||||
return None
|
||||
for key, value in self._mode_list.items():
|
||||
if value == description:
|
||||
return key
|
||||
return None
|
||||
|
||||
Loading…
Reference in New Issue
Block a user