refactor: refactor miot device and spec (#592)

* fix: fix miot_device type error

* fix: fix type error

* feat: remove spec cache storage

* feat: update std_lib and multi_lang logic

* feat: update entity value-range

* feat: update value-list logic

* feat: update prop.format_ logic

* fix: fix miot cloud log error

* fix: fix fan entity

* style: ignore type error

* style: rename spec_filter func name

* feat: move bool_trans from storage to spec

* feat: move sepc_filter from storage to spec, use the YAML format file

* feat: same prop supports multiple sub

* feat: same event supports multiple sub

* fix: fix device remove error

* feat: add func slugify_did

* fix: fix multi lang error

* feat: update action debug logic

* feat: ignore normal disconnect log

* feat: support binary mode

* feat: change miot spec name type define

* style: ignore i18n tranlate type error

* fix: fix pylint warning

* fix: miot storage type error

* feat: support binary display mode configure

* feat: set default sensor state_class

* fix: fix sensor entity type error

* fix: fix __init__ type error

* feat: update test case logic

* fix: github actions add dependencies lib

* fix: fix some type error

* feat: update device list changed notify logic
This commit is contained in:
Paul Shawn 2025-01-17 18:14:31 +08:00 committed by GitHub
parent bf116e13a4
commit ef56448dbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 2050 additions and 1978 deletions

View File

@ -19,7 +19,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography
pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify
- name: Check rule format with pytest
run: |

View File

@ -54,6 +54,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.components import persistent_notification
from homeassistant.helpers import device_registry, entity_registry
from .miot.common import slugify_did
from .miot.miot_storage import (
DeviceManufacturer, MIoTStorage, MIoTCert)
from .miot.miot_spec import (
@ -92,7 +93,7 @@ async def async_setup_entry(
"""Send messages in Notifications dialog box."""
if title:
persistent_notification.async_create(
hass=hass, message=message,
hass=hass, message=message or '',
title=title, notification_id=notify_id)
else:
persistent_notification.async_dismiss(
@ -125,9 +126,8 @@ async def async_setup_entry(
miot_devices: list[MIoTDevice] = []
er = entity_registry.async_get(hass=hass)
for did, info in miot_client.device_list.items():
spec_instance: MIoTSpecInstance = await spec_parser.parse(
urn=info['urn'])
if spec_instance is None:
spec_instance = await spec_parser.parse(urn=info['urn'])
if not isinstance(spec_instance, MIoTSpecInstance):
_LOGGER.error('spec content is None, %s, %s', did, info)
continue
device: MIoTDevice = MIoTDevice(
@ -155,7 +155,8 @@ async def async_setup_entry(
for entity in filter_entities:
device.entity_list[platform].remove(entity)
entity_id = device.gen_service_entity_id(
ha_domain=platform, siid=entity.spec.iid)
ha_domain=platform,
siid=entity.spec.iid) # type: ignore
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
if platform in device.prop_list:
@ -208,12 +209,7 @@ async def async_setup_entry(
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
# Action debug
if miot_client.action_debug:
if 'notify' in device.action_list:
# Add text entity for debug action
device.action_list['action_text'] = (
device.action_list['notify'])
else:
if not miot_client.action_debug:
# Remove text entity for debug action
for action in device.action_list.get('notify', []):
entity_id = device.gen_action_entity_id(
@ -221,6 +217,21 @@ async def async_setup_entry(
siid=action.service.iid, aiid=action.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
# Binary sensor display
if not miot_client.display_binary_bool:
for prop in device.prop_list.get('binary_sensor', []):
entity_id = device.gen_prop_entity_id(
ha_domain='binary_sensor', spec_name=prop.name,
siid=prop.service.iid, piid=prop.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
if not miot_client.display_binary_text:
for prop in device.prop_list.get('binary_sensor', []):
entity_id = device.gen_prop_entity_id(
ha_domain='sensor', spec_name=prop.name,
siid=prop.service.iid, piid=prop.iid)
if er.async_get(entity_id_or_uuid=entity_id):
er.async_remove(entity_id=entity_id)
hass.data[DOMAIN]['devices'][config_entry.entry_id] = miot_devices
await hass.config_entries.async_forward_entry_setups(
@ -237,7 +248,7 @@ async def async_setup_entry(
device_entry = dr.async_get_device(
identifiers={(
DOMAIN,
MIoTDevice.gen_did_tag(
slugify_did(
cloud_server=config_entry.data['cloud_server'],
did=did))},
connections=None)
@ -330,21 +341,10 @@ async def async_remove_config_entry_device(
'remove device failed, invalid domain, %s, %s',
device_entry.id, device_entry.identifiers)
return False
device_info = identifiers[1].split('_')
if len(device_info) != 2:
_LOGGER.error(
'remove device failed, invalid device info, %s, %s',
device_entry.id, device_entry.identifiers)
return False
did = device_info[1]
if did not in miot_client.device_list:
_LOGGER.error(
'remove device failed, device not found, %s, %s',
device_entry.id, device_entry.identifiers)
return False
# Remove device
await miot_client.remove_device_async(did)
await miot_client.remove_device2_async(did_tag=identifiers[1])
device_registry.async_get(hass).async_remove_device(device_entry.id)
_LOGGER.info(
'remove device, %s, %s, %s', device_info[0], did, device_entry.id)
'remove device, %s, %s', identifiers[1], device_entry.id)
return True

View File

@ -68,9 +68,10 @@ async def async_setup_entry(
new_entities = []
for miot_device in device_list:
for prop in miot_device.prop_list.get('binary_sensor', []):
new_entities.append(BinarySensor(
miot_device=miot_device, spec=prop))
if miot_device.miot_client.display_binary_bool:
for prop in miot_device.prop_list.get('binary_sensor', []):
new_entities.append(BinarySensor(
miot_device=miot_device, spec=prop))
if new_entities:
async_add_entities(new_entities)

View File

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

View File

@ -124,6 +124,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_area_name_rule: str
_action_debug: bool
_hide_non_standard_entities: bool
_display_binary_mode: list[str]
_display_devices_changed_notify: list[str]
_cloud_server: str
@ -158,6 +159,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._area_name_rule = self.DEFAULT_AREA_NAME_RULE
self._action_debug = False
self._hide_non_standard_entities = False
self._display_binary_mode = ['bool']
self._display_devices_changed_notify = ['add', 'del', 'offline']
self._auth_info = {}
self._nick_name = DEFAULT_NICK_NAME
@ -473,6 +475,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self._miot_oauth.deinit_async()
self._miot_oauth = None
return self.async_show_progress_done(next_step_id='homes_select')
# pylint: disable=unexpected-keyword-arg
return self.async_show_progress(
step_id='oauth',
progress_action='oauth',
@ -481,7 +484,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
f'<a href="{self._cc_oauth_auth_url}" target="_blank">',
'link_right': '</a>'
},
progress_task=self._cc_task_oauth,
progress_task=self._cc_task_oauth, # type: ignore
)
async def __check_oauth_async(self) -> None:
@ -727,6 +730,8 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
'action_debug', self._action_debug)
self._hide_non_standard_entities = user_input.get(
'hide_non_standard_entities', self._hide_non_standard_entities)
self._display_binary_mode = user_input.get(
'display_binary_mode', self._display_binary_mode)
self._display_devices_changed_notify = user_input.get(
'display_devices_changed_notify',
self._display_devices_changed_notify)
@ -749,6 +754,12 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
'hide_non_standard_entities',
default=self._hide_non_standard_entities # type: ignore
): bool,
vol.Required(
'display_binary_mode',
default=self._display_binary_mode # type: ignore
): cv.multi_select(
self._miot_i18n.translate(
key='config.binary_mode')), # type: ignore
vol.Required(
'display_devices_changed_notify',
default=self._display_devices_changed_notify # type: ignore
@ -931,6 +942,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
'action_debug': self._action_debug,
'hide_non_standard_entities':
self._hide_non_standard_entities,
'display_binary_mode': self._display_binary_mode,
'display_devices_changed_notify':
self._display_devices_changed_notify
})
@ -972,6 +984,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_devices_filter: dict
_action_debug: bool
_hide_non_standard_entities: bool
_display_binary_mode: list[str]
_display_devs_notify: list[str]
_oauth_redirect_url_full: str
@ -986,6 +999,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
_nick_name_new: Optional[str]
_action_debug_new: bool
_hide_non_standard_entities_new: bool
_display_binary_mode_new: list[str]
_update_user_info: bool
_update_devices: bool
_update_trans_rules: bool
@ -1024,6 +1038,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._action_debug = self._entry_data.get('action_debug', False)
self._hide_non_standard_entities = self._entry_data.get(
'hide_non_standard_entities', False)
self._display_binary_mode = self._entry_data.get(
'display_binary_mode', ['text'])
self._display_devs_notify = self._entry_data.get(
'display_devices_changed_notify', ['add', 'del', 'offline'])
self._home_selected_list = list(
@ -1042,6 +1058,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._nick_name_new = None
self._action_debug_new = False
self._hide_non_standard_entities_new = False
self._display_binary_mode_new = []
self._update_user_info = False
self._update_devices = False
self._update_trans_rules = False
@ -1196,7 +1213,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
err, traceback.format_exc())
self._cc_config_rc = str(err)
return self.async_show_progress_done(next_step_id='oauth_error')
# pylint: disable=unexpected-keyword-arg
return self.async_show_progress(
step_id='oauth',
progress_action='oauth',
@ -1205,7 +1222,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
f'<a href="{self._cc_oauth_auth_url}" target="_blank">',
'link_right': '</a>'
},
progress_task=self._cc_task_oauth,
progress_task=self._cc_task_oauth, # type: ignore
)
async def __check_oauth_async(self) -> None:
@ -1308,6 +1325,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'hide_non_standard_entities',
default=self._hide_non_standard_entities # type: ignore
): bool,
vol.Required(
'display_binary_mode',
default=self._display_binary_mode # type: ignore
): cv.multi_select(
self._miot_i18n.translate(
'config.binary_mode')), # type: ignore
vol.Required(
'update_trans_rules',
default=self._update_trans_rules # type: ignore
@ -1336,6 +1359,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'action_debug', self._action_debug)
self._hide_non_standard_entities_new = user_input.get(
'hide_non_standard_entities', self._hide_non_standard_entities)
self._display_binary_mode_new = user_input.get(
'display_binary_mode', self._display_binary_mode)
self._display_devs_notify = user_input.get(
'display_devices_changed_notify', self._display_devs_notify)
self._update_trans_rules = user_input.get(
@ -1939,6 +1964,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self._entry_data['hide_non_standard_entities'] = (
self._hide_non_standard_entities_new)
self._need_reload = True
if set(self._display_binary_mode) != set(self._display_binary_mode_new):
self._entry_data['display_binary_mode'] = (
self._display_binary_mode_new)
self._need_reload = True
# Update display_devices_changed_notify
self._entry_data['display_devices_changed_notify'] = (
self._display_devs_notify)

View File

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

View File

@ -85,6 +85,8 @@ class Event(MIoTEventEntity, EventEntity):
# Set device_class
self._attr_device_class = spec.device_class
def on_event_occurred(self, name: str, arguments: list[dict[int, Any]]):
def on_event_occurred(
self, name: str, arguments: dict[str, Any] | None = None
) -> None:
"""An event is occurred."""
self._trigger_event(event_type=name, event_attributes=arguments)

View File

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

View File

@ -97,7 +97,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
_prop_target_humidity: Optional[MIoTSpecProperty]
_prop_humidity: Optional[MIoTSpecProperty]
_mode_list: dict[Any, Any]
_mode_map: dict[Any, Any]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -110,7 +110,7 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_mode = None
self._prop_target_humidity = None
self._prop_humidity = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -119,28 +119,23 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
self._prop_on = prop
# target-humidity
elif prop.name == 'target-humidity':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.error(
'invalid target-humidity value_range format, %s',
self.entity_id)
continue
self._attr_min_humidity = prop.value_range['min']
self._attr_max_humidity = prop.value_range['max']
self._attr_min_humidity = prop.value_range.min_
self._attr_max_humidity = prop.value_range.max_
self._prop_target_humidity = prop
# mode
elif prop.name == 'mode':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._mode_map = prop.value_list.to_map()
self._attr_available_modes = list(
self._mode_list.values())
self._mode_map.values())
self._attr_supported_features |= HumidifierEntityFeature.MODES
self._prop_mode = prop
# relative-humidity
@ -163,7 +158,8 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
async def async_set_mode(self, mode: str) -> None:
"""Set new target preset mode."""
await self.set_property_async(
prop=self._prop_mode, value=self.__get_mode_value(description=mode))
prop=self._prop_mode,
value=self.get_map_key(map_=self._mode_map, value=mode))
@property
def is_on(self) -> Optional[bool]:
@ -183,20 +179,6 @@ class Humidifier(MIoTServiceEntity, HumidifierEntity):
@property
def mode(self) -> Optional[str]:
"""Return the current preset mode."""
return self.__get_mode_description(
return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode))
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None

View File

@ -96,14 +96,14 @@ class Light(MIoTServiceEntity, LightEntity):
"""Light entities for Xiaomi Home."""
# pylint: disable=unused-argument
_VALUE_RANGE_MODE_COUNT_MAX = 30
_prop_on: Optional[MIoTSpecProperty]
_prop_on: MIoTSpecProperty
_prop_brightness: Optional[MIoTSpecProperty]
_prop_color_temp: Optional[MIoTSpecProperty]
_prop_color: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty]
_brightness_scale: Optional[tuple[int, int]]
_mode_list: Optional[dict[Any, Any]]
_mode_map: Optional[dict[Any, Any]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -122,7 +122,7 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_color = None
self._prop_mode = None
self._brightness_scale = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -131,20 +131,17 @@ class Light(MIoTServiceEntity, LightEntity):
self._prop_on = prop
# brightness
if prop.name == 'brightness':
if isinstance(prop.value_range, dict):
if prop.value_range:
self._brightness_scale = (
prop.value_range['min'], prop.value_range['max'])
prop.value_range.min_, prop.value_range.max_)
self._prop_brightness = prop
elif (
self._mode_list is None
and isinstance(prop.value_list, list)
self._mode_map is None
and prop.value_list
):
# For value-list brightness
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_effect_list = list(self._mode_list.values())
self._mode_map = prop.value_list.to_map()
self._attr_effect_list = list(self._mode_map.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -153,13 +150,13 @@ class Light(MIoTServiceEntity, LightEntity):
continue
# color-temperature
if prop.name == 'color-temperature':
if not isinstance(prop.value_range, dict):
if not prop.value_range:
_LOGGER.info(
'invalid color-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_color_temp_kelvin = prop.value_range['min']
self._attr_max_color_temp_kelvin = prop.value_range['max']
self._attr_min_color_temp_kelvin = prop.value_range.min_
self._attr_max_color_temp_kelvin = prop.value_range.max_
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
self._attr_color_mode = ColorMode.COLOR_TEMP
self._prop_color_temp = prop
@ -171,20 +168,15 @@ class Light(MIoTServiceEntity, LightEntity):
# mode
if prop.name == 'mode':
mode_list = None
if (
isinstance(prop.value_list, list)
and prop.value_list
):
mode_list = {
item['value']: item['description']
for item in prop.value_list}
elif isinstance(prop.value_range, dict):
if prop.value_list:
mode_list = prop.value_list.to_map()
elif prop.value_range:
mode_list = {}
if (
int((
prop.value_range['max']
- prop.value_range['min']
) / prop.value_range['step'])
prop.value_range.max_
- prop.value_range.min_
) / prop.value_range.step)
> self._VALUE_RANGE_MODE_COUNT_MAX
):
_LOGGER.info(
@ -192,13 +184,13 @@ class Light(MIoTServiceEntity, LightEntity):
self.entity_id, prop.name, prop.value_range)
else:
for value in range(
prop.value_range['min'],
prop.value_range['max'],
prop.value_range['step']):
prop.value_range.min_,
prop.value_range.max_,
prop.value_range.step):
mode_list[value] = f'mode {value}'
if mode_list:
self._mode_list = mode_list
self._attr_effect_list = list(self._mode_list.values())
self._mode_map = mode_list
self._attr_effect_list = list(self._mode_map.values())
self._attr_supported_features |= LightEntityFeature.EFFECT
self._prop_mode = prop
else:
@ -213,21 +205,6 @@ class Light(MIoTServiceEntity, LightEntity):
self._attr_supported_color_modes.add(ColorMode.ONOFF)
self._attr_color_mode = ColorMode.ONOFF
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None
@property
def is_on(self) -> Optional[bool]:
"""Return if the light is on."""
@ -264,7 +241,8 @@ class Light(MIoTServiceEntity, LightEntity):
@property
def effect(self) -> Optional[str]:
"""Return the current mode."""
return self.__get_mode_description(
return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode))
async def async_turn_on(self, **kwargs) -> None:
@ -275,7 +253,7 @@ class Light(MIoTServiceEntity, LightEntity):
result: bool = False
# on
# Dirty logic for lumi.gateway.mgl03 indicator light
value_on = True if self._prop_on.format_ == 'bool' else 1
value_on = True if self._prop_on.format_ == bool else 1
result = await self.set_property_async(
prop=self._prop_on, value=value_on)
# brightness
@ -303,11 +281,12 @@ class Light(MIoTServiceEntity, LightEntity):
if ATTR_EFFECT in kwargs:
result = await self.set_property_async(
prop=self._prop_mode,
value=self.__get_mode_value(description=kwargs[ATTR_EFFECT]))
value=self.get_map_key(
map_=self._mode_map, value=kwargs[ATTR_EFFECT]))
return result
async def async_turn_off(self, **kwargs) -> None:
"""Turn the light off."""
# Dirty logic for lumi.gateway.mgl03 indicator light
value_on = False if self._prop_on.format_ == 'bool' else 0
value_on = False if self._prop_on.format_ == bool else 0
return await self.set_property_async(prop=self._prop_on, value=value_on)

View File

@ -45,13 +45,17 @@ off Xiaomi or its affiliates' products.
Common utilities.
"""
import asyncio
import json
from os import path
import random
from typing import Any, Optional
import hashlib
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from paho.mqtt.matcher import MQTTMatcher
import yaml
from slugify import slugify
MIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__))
@ -83,10 +87,22 @@ def randomize_int(value: int, ratio: float) -> int:
"""Randomize an integer value."""
return int(value * (1 - ratio + random.random()*2*ratio))
def randomize_float(value: float, ratio: float) -> float:
"""Randomize a float value."""
return value * (1 - ratio + random.random()*2*ratio)
def slugify_name(name: str, separator: str = '_') -> str:
"""Slugify a name."""
return slugify(name, separator=separator)
def slugify_did(cloud_server: str, did: str) -> str:
"""Slugify a device id."""
return slugify(f'{cloud_server}_{did}', separator='_')
class MIoTMatcher(MQTTMatcher):
"""MIoT Pub/Sub topic matcher."""
@ -105,3 +121,68 @@ class MIoTMatcher(MQTTMatcher):
return self[topic]
except KeyError:
return None
class MIoTHttp:
"""MIoT Common HTTP API."""
@staticmethod
def get(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
full_url = url
if params:
encoded_params = urlencode(params)
full_url = f'{url}?{encoded_params}'
request = Request(full_url, method='GET', headers=headers or {})
content: Optional[bytes] = None
with urlopen(request) as response:
content = response.read()
return str(content, 'utf-8') if content else None
@staticmethod
def get_json(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.get(url, params, headers)
return json.loads(response) if response else None
@staticmethod
def post(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
pass
@staticmethod
def post_json(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.post(url, data, headers)
return json.loads(response) if response else None
@staticmethod
async def get_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
# TODO: Use aiohttp
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get, url, params, headers)
@staticmethod
async def get_json_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[dict]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get_json, url, params, headers)
@ staticmethod
async def post_async(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.post, url, data, headers)

View File

@ -54,6 +54,10 @@
"enable": "aktivieren",
"disable": "deaktivieren"
},
"binary_mode": {
"text": "Textsensor-Entität",
"bool": "Binärsensor-Entität"
},
"device_state": {
"add": "hinzufügen",
"del": "nicht verfügbar",

View File

@ -54,6 +54,10 @@
"enable": "Enable",
"disable": "Disable"
},
"binary_mode": {
"text": "Text Sensor Entity",
"bool": "Binary Sensor Entity"
},
"device_state": {
"add": "Add",
"del": "Unavailable",

View File

@ -54,6 +54,10 @@
"enable": "habilitar",
"disable": "deshabilitar"
},
"binary_mode": {
"text": "Entidad del sensor de texto",
"bool": "Entidad del sensor binario"
},
"device_state": {
"add": "agregar",
"del": "no disponible",

View File

@ -54,6 +54,10 @@
"enable": "activer",
"disable": "désactiver"
},
"binary_mode": {
"text": "Entité du capteur de texte",
"bool": "Entité du capteur binaire"
},
"device_state": {
"add": "Ajouter",
"del": "Supprimer",

View File

@ -54,6 +54,10 @@
"enable": "Abilita",
"disable": "Disabilita"
},
"binary_mode": {
"text": "Entità Sensore Testo",
"bool": "Entità Sensore Binario"
},
"device_state": {
"add": "Aggiungere",
"del": "Non disponibile",
@ -149,4 +153,4 @@
"-706014006": "Descrizione del dispositivo non trovata"
}
}
}
}

View File

@ -54,6 +54,10 @@
"enable": "有効",
"disable": "無効"
},
"binary_mode": {
"text": "テキストセンサーエンティティ",
"bool": "バイナリセンサーエンティティ"
},
"device_state": {
"add": "追加",
"del": "利用不可",

View File

@ -54,6 +54,10 @@
"enable": "Inschakelen",
"disable": "Uitschakelen"
},
"binary_mode": {
"text": "Tekstsensor-entiteit",
"bool": "Binairesensor-entiteit"
},
"device_state": {
"add": "Toevoegen",
"del": "Niet beschikbaar",

View File

@ -54,6 +54,10 @@
"enable": "habilitado",
"disable": "desabilitado"
},
"binary_mode": {
"text": "Entidade do sensor de texto",
"bool": "Entidade do sensor binário"
},
"device_state": {
"add": "adicionar",
"del": "indisponível",

View File

@ -54,6 +54,10 @@
"enable": "Habilitar",
"disable": "Desabilitar"
},
"binary_mode": {
"text": "Entidade do sensor de texto",
"bool": "Entidade do sensor binário"
},
"device_state": {
"add": "Adicionar",
"del": "Indisponível",

View File

@ -54,6 +54,10 @@
"enable": "Включить",
"disable": "Отключить"
},
"binary_mode": {
"text": "Сущность текстового датчика",
"bool": "Сущность бинарного датчика"
},
"device_state": {
"add": "Добавить",
"del": "Недоступно",

View File

@ -54,6 +54,10 @@
"enable": "启用",
"disable": "禁用"
},
"binary_mode": {
"text": "文本传感器实体",
"bool": "二进制传感器实体"
},
"device_state": {
"add": "新增",
"del": "不可用",

View File

@ -54,6 +54,10 @@
"enable": "啟用",
"disable": "禁用"
},
"binary_mode": {
"text": "文本傳感器實體",
"bool": "二進制傳感器實體"
},
"device_state": {
"add": "新增",
"del": "不可用",

View File

@ -59,7 +59,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.components import zeroconf
# pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher
from .common import MIoTMatcher, slugify_did
from .const import (
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
@ -150,7 +150,7 @@ class MIoTClient:
# Device list update timestamp
_device_list_update_ts: int
_sub_source_list: dict[str]
_sub_source_list: dict[str, str]
_sub_tree: MIoTMatcher
_sub_device_state: dict[str, MipsDeviceState]
@ -169,6 +169,10 @@ class MIoTClient:
_show_devices_changed_notify_timer: Optional[asyncio.TimerHandle]
# Display devices changed notify
_display_devs_notify: list[str]
_display_notify_content_hash: Optional[int]
# Display binary mode
_display_binary_text: bool
_display_binary_bool: bool
def __init__(
self,
@ -235,6 +239,11 @@ class MIoTClient:
self._display_devs_notify = entry_data.get(
'display_devices_changed_notify', ['add', 'del', 'offline'])
self._display_notify_content_hash = None
self._display_binary_text = 'text' in entry_data.get(
'display_binary_mode', ['text'])
self._display_binary_bool = 'bool' in entry_data.get(
'display_binary_mode', ['text'])
async def init_async(self) -> None:
# Load user config and check
@ -469,6 +478,14 @@ class MIoTClient:
def display_devices_changed_notify(self) -> list[str]:
return self._display_devs_notify
@property
def display_binary_text(self) -> bool:
return self._display_binary_text
@property
def display_binary_bool(self) -> bool:
return self._display_binary_bool
@display_devices_changed_notify.setter
def display_devices_changed_notify(self, value: list[str]) -> None:
if set(value) == set(self._display_devs_notify):
@ -543,7 +560,8 @@ class MIoTClient:
return True
except Exception as err:
self.__show_client_error_notify(
message=self._i18n.translate('miot.client.invalid_oauth_info'),
message=self._i18n.translate(
'miot.client.invalid_oauth_info'), # type: ignore
notify_key='oauth_info')
_LOGGER.error(
'refresh oauth info error (%s, %s), %s, %s',
@ -586,7 +604,8 @@ class MIoTClient:
return True
except MIoTClientError as error:
self.__show_client_error_notify(
message=self._i18n.translate('miot.client.invalid_cert_info'),
message=self._i18n.translate(
'miot.client.invalid_cert_info'), # type: ignore
notify_key='user_cert')
_LOGGER.error(
'refresh user cert error, %s, %s',
@ -872,8 +891,16 @@ class MIoTClient:
# Update notify
self.__request_show_devices_changed_notify()
async def remove_device2_async(self, did_tag: str) -> None:
for did in self._device_list_cache:
d_tag = slugify_did(cloud_server=self._cloud_server, did=did)
if did_tag == d_tag:
await self.remove_device_async(did)
break
def __get_exec_error_with_rc(self, rc: int) -> str:
err_msg: str = self._i18n.translate(key=f'error.common.{rc}')
err_msg: str = self._i18n.translate(
key=f'error.common.{rc}') # type: ignore
if not err_msg:
err_msg = f'{self._i18n.translate(key="error.common.-10000")}, '
err_msg += f'code={rc}'
@ -1280,7 +1307,7 @@ class MIoTClient:
if not cache_list:
self.__show_client_error_notify(
message=self._i18n.translate(
'miot.client.invalid_device_cache'),
'miot.client.invalid_device_cache'), # type: ignore
notify_key='device_cache')
raise MIoTClientError('load device list from cache error')
else:
@ -1368,7 +1395,8 @@ class MIoTClient:
home_ids=list(self._entry_data.get('home_selected', {}).keys()))
if not result and 'devices' not in result:
self.__show_client_error_notify(
message=self._i18n.translate('miot.client.device_cloud_error'),
message=self._i18n.translate(
'miot.client.device_cloud_error'), # type: ignore
notify_key='device_cloud')
return
else:
@ -1725,13 +1753,14 @@ class MIoTClient:
@final
def __show_client_error_notify(
self, message: str, notify_key: str = ''
self, message: Optional[str], notify_key: str = ''
) -> None:
if message:
self._persistence_notify(
f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',
self._i18n.translate(
key='miot.client.xiaomi_home_error_title'),
key='miot.client.xiaomi_home_error_title'), # type: ignore
self._i18n.translate(
key='miot.client.xiaomi_home_error',
replace={
@ -1739,8 +1768,7 @@ class MIoTClient:
'nick_name', DEFAULT_NICK_NAME),
'uid': self._uid,
'cloud_server': self._cloud_server,
'message': message
}))
'message': message})) # type: ignore
else:
self._persistence_notify(
f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',
@ -1806,27 +1834,34 @@ class MIoTClient:
key='miot.client.device_list_add',
replace={
'count': count_add,
'message': message_add})
'message': message_add}) # type: ignore
if 'del' in self._display_devs_notify and count_del:
message += self._i18n.translate(
key='miot.client.device_list_del',
replace={
'count': count_del,
'message': message_del})
'message': message_del}) # type: ignore
if 'offline' in self._display_devs_notify and count_offline:
message += self._i18n.translate(
key='miot.client.device_list_offline',
replace={
'count': count_offline,
'message': message_offline})
'message': message_offline}) # type: ignore
if message != '':
msg_hash = hash(message)
if msg_hash == self._display_notify_content_hash:
# Notify content no change, return
_LOGGER.debug(
'device list changed notify content no change, return')
return
network_status = self._i18n.translate(
key='miot.client.network_status_online'
if self._network.network_status
else 'miot.client.network_status_offline')
self._persistence_notify(
self.__gen_notify_key('dev_list_changed'),
self._i18n.translate('miot.client.device_list_changed_title'),
self._i18n.translate(
'miot.client.device_list_changed_title'), # type: ignore
self._i18n.translate(
key='miot.client.device_list_changed',
replace={
@ -1835,8 +1870,8 @@ class MIoTClient:
'uid': self._uid,
'cloud_server': self._cloud_server,
'network_status': network_status,
'message': message
}))
'message': message})) # type: ignore
self._display_notify_content_hash = msg_hash
_LOGGER.debug(
'show device list changed notify, add %s, del %s, offline %s',
count_add, count_del, count_offline)

View File

@ -744,7 +744,7 @@ class MIoTHttpClient:
prop_obj['fut'].set_result(None)
if props_req:
_LOGGER.info(
'get prop from cloud failed, %s, %s', len(key), props_req)
'get prop from cloud failed, %s', props_req)
if self._get_prop_list:
self._get_prop_timer = self._main_loop.call_later(

View File

@ -75,7 +75,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.util import slugify
# pylint: disable=relative-beyond-top-level
from .specs.specv2entity import (
@ -85,6 +85,7 @@ from .specs.specv2entity import (
SPEC_PROP_TRANS_MAP,
SPEC_SERVICE_TRANS_MAP
)
from .common import slugify_name, slugify_did
from .const import DOMAIN
from .miot_client import MIoTClient
from .miot_error import MIoTClientError, MIoTDeviceError
@ -94,7 +95,9 @@ from .miot_spec import (
MIoTSpecEvent,
MIoTSpecInstance,
MIoTSpecProperty,
MIoTSpecService
MIoTSpecService,
MIoTSpecValueList,
MIoTSpecValueRange
)
_LOGGER = logging.getLogger(__name__)
@ -142,9 +145,12 @@ class MIoTDevice:
_room_id: str
_room_name: str
_suggested_area: str
_suggested_area: Optional[str]
_device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]]
_sub_id: int
_device_state_sub_list: dict[str, dict[
str, Callable[[str, MIoTDeviceState], None]]]
_value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]]
_entity_list: dict[str, list[MIoTEntityData]]
_prop_list: dict[str, list[MIoTSpecProperty]]
@ -153,7 +159,7 @@ class MIoTDevice:
def __init__(
self, miot_client: MIoTClient,
device_info: dict[str, str],
device_info: dict[str, Any],
spec_instance: MIoTSpecInstance
) -> None:
self.miot_client = miot_client
@ -183,7 +189,9 @@ class MIoTDevice:
case _:
self._suggested_area = None
self._sub_id = 0
self._device_state_sub_list = {}
self._value_sub_list = {}
self._entity_list = {}
self._prop_list = {}
self._event_list = {}
@ -234,36 +242,76 @@ class MIoTDevice:
def sub_device_state(
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
) -> bool:
self._device_state_sub_list[key] = handler
return True
) -> int:
self._sub_id += 1
if key in self._device_state_sub_list:
self._device_state_sub_list[key][str(self._sub_id)] = handler
else:
self._device_state_sub_list[key] = {str(self._sub_id): handler}
return self._sub_id
def unsub_device_state(self, key: str) -> bool:
self._device_state_sub_list.pop(key, None)
return True
def unsub_device_state(self, key: str, sub_id: int) -> None:
sub_list = self._device_state_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self._device_state_sub_list.pop(key, None)
def sub_property(
self, handler: Callable[[dict, Any], None], siid: int = None,
piid: int = None, handler_ctx: Any = None
) -> bool:
return self.miot_client.sub_prop(
did=self._did, handler=handler, siid=siid, piid=piid,
handler_ctx=handler_ctx)
self, handler: Callable[[dict, Any], None], siid: int, piid: int
) -> int:
key: str = f'p.{siid}.{piid}'
def unsub_property(self, siid: int = None, piid: int = None) -> bool:
return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
def _on_prop_changed(params: dict, ctx: Any) -> None:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self.miot_client.sub_prop(
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
return self._sub_id
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
key: str = f'p.{siid}.{piid}'
sub_list = self._value_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
self._value_sub_list.pop(key, None)
def sub_event(
self, handler: Callable[[dict, Any], None], siid: int = None,
eiid: int = None, handler_ctx: Any = None
) -> bool:
return self.miot_client.sub_event(
did=self._did, handler=handler, siid=siid, eiid=eiid,
handler_ctx=handler_ctx)
self, handler: Callable[[dict, Any], None], siid: int, eiid: int
) -> int:
key: str = f'e.{siid}.{eiid}'
def unsub_event(self, siid: int = None, eiid: int = None) -> bool:
return self.miot_client.unsub_event(
did=self._did, siid=siid, eiid=eiid)
def _on_event_occurred(params: dict, ctx: Any) -> None:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self.miot_client.sub_event(
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
return self._sub_id
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
key: str = f'e.{siid}.{eiid}'
sub_list = self._value_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self.miot_client.unsub_event(did=self._did, siid=siid, eiid=eiid)
self._value_sub_list.pop(key, None)
@property
def device_info(self) -> DeviceInfo:
@ -287,11 +335,8 @@ class MIoTDevice:
@property
def did_tag(self) -> str:
return slugify(f'{self.miot_client.cloud_server}_{self._did}')
@staticmethod
def gen_did_tag(cloud_server: str, did: str) -> str:
return slugify(f'{cloud_server}_{did}')
return slugify_did(
cloud_server=self.miot_client.cloud_server, did=self._did)
def gen_device_entity_id(self, ha_domain: str) -> str:
return (
@ -308,21 +353,24 @@ class MIoTDevice:
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_p_{siid}_{piid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_p_{siid}_{piid}')
def gen_event_entity_id(
self, ha_domain: str, spec_name: str, siid: int, eiid: int
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_e_{siid}_{eiid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_e_{siid}_{eiid}')
def gen_action_entity_id(
self, ha_domain: str, spec_name: str, siid: int, aiid: int
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_a_{siid}_{aiid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_a_{siid}_{aiid}')
@property
def name(self) -> str:
@ -341,14 +389,20 @@ class MIoTDevice:
self._entity_list[entity_data.platform].append(entity_data)
def append_prop(self, prop: MIoTSpecProperty) -> None:
if not prop.platform:
return
self._prop_list.setdefault(prop.platform, [])
self._prop_list[prop.platform].append(prop)
def append_event(self, event: MIoTSpecEvent) -> None:
if not event.platform:
return
self._event_list.setdefault(event.platform, [])
self._event_list[event.platform].append(event)
def append_action(self, action: MIoTSpecAction) -> None:
if not action.platform:
return
self._action_list.setdefault(action.platform, [])
self._action_list[action.platform].append(action)
@ -507,7 +561,7 @@ class MIoTDevice:
if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']):
return None
if prop.format_ not in SPEC_PROP_TRANS_MAP[
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']:
return None
if prop.unit:
@ -560,9 +614,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:
@ -573,9 +627,11 @@ class MIoTDevice:
# Irregular property will not be transformed.
pass
elif prop.readable or prop.notifiable:
prop.platform = 'sensor'
if prop.platform:
self.append_prop(prop=prop)
if prop.format_ == bool:
prop.platform = 'binary_sensor'
else:
prop.platform = 'sensor'
self.append_prop(prop=prop)
# STEP 3.2: event conversion
for event in service.events:
if event.platform:
@ -703,10 +759,11 @@ class MIoTDevice:
def __on_device_state_changed(
self, did: str, state: MIoTDeviceState, ctx: Any
) -> None:
self._online = state
for key, handler in self._device_state_sub_list.items():
self.miot_client.main_loop.call_soon_threadsafe(
handler, key, state)
self._online = state == MIoTDeviceState.ONLINE
for key, sub_list in self._device_state_sub_list.items():
for handler in sub_list.values():
self.miot_client.main_loop.call_soon_threadsafe(
handler, key, state)
class MIoTServiceEntity(Entity):
@ -718,8 +775,11 @@ class MIoTServiceEntity(Entity):
_main_loop: asyncio.AbstractEventLoop
_prop_value_map: dict[MIoTSpecProperty, Any]
_state_sub_id: int
_value_sub_ids: dict[str, int]
_event_occurred_handler: Callable[[MIoTSpecEvent, dict], None]
_event_occurred_handler: Optional[
Callable[[MIoTSpecEvent, dict], None]]
_prop_changed_subs: dict[
MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]
@ -738,13 +798,15 @@ class MIoTServiceEntity(Entity):
self.entity_data = entity_data
self._main_loop = miot_device.miot_client.main_loop
self._prop_value_map = {}
self._state_sub_id = 0
self._value_sub_ids = {}
# Gen entity id
if isinstance(entity_data.spec, MIoTSpecInstance):
if isinstance(self.entity_data.spec, MIoTSpecInstance):
self.entity_id = miot_device.gen_device_entity_id(DOMAIN)
self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(entity_data.spec, MIoTSpecService):
elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id(
DOMAIN, siid=entity_data.spec.iid)
DOMAIN, siid=self.entity_data.spec.iid)
self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}')
@ -763,7 +825,9 @@ class MIoTServiceEntity(Entity):
self.entity_id)
@property
def event_occurred_handler(self) -> Callable[[MIoTSpecEvent, dict], None]:
def event_occurred_handler(
self
) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
return self._event_occurred_handler
@event_occurred_handler.setter
@ -784,25 +848,27 @@ class MIoTServiceEntity(Entity):
self._prop_changed_subs.pop(prop, None)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
state_id = 's.0'
if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f's.{self.entity_data.spec.iid}'
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=state_id, handler=self.__on_device_state_changed)
# Sub prop
for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable:
continue
self.miot_device.sub_property(
key = f'p.{prop.service.iid}.{prop.iid}'
self._value_sub_ids[key] = self.miot_device.sub_property(
handler=self.__on_properties_changed,
siid=prop.service.iid, piid=prop.iid)
# Sub event
for event in self.entity_data.events:
self.miot_device.sub_event(
key = f'e.{event.service.iid}.{event.iid}'
self._value_sub_ids[key] = self.miot_device.sub_event(
handler=self.__on_event_occurred,
siid=event.service.iid, eiid=event.iid)
@ -817,30 +883,39 @@ class MIoTServiceEntity(Entity):
state_id = 's.0'
if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f's.{self.entity_data.spec.iid}'
self.miot_device.unsub_device_state(key=state_id)
self.miot_device.unsub_device_state(
key=state_id, sub_id=self._state_sub_id)
# Unsub prop
for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable:
continue
self.miot_device.unsub_property(
siid=prop.service.iid, piid=prop.iid)
sub_id = self._value_sub_ids.pop(
f'p.{prop.service.iid}.{prop.iid}', None)
if sub_id:
self.miot_device.unsub_property(
siid=prop.service.iid, piid=prop.iid, sub_id=sub_id)
# Unsub event
for event in self.entity_data.events:
self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid)
sub_id = self._value_sub_ids.pop(
f'e.{event.service.iid}.{event.iid}', None)
if sub_id:
self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
def get_map_description(self, map_: dict[int, Any], key: int) -> Any:
def get_map_value(
self, map_: dict[int, Any], key: int
) -> Any:
if map_ is None:
return None
return map_.get(key, None)
def get_map_value(
self, map_: dict[int, Any], description: Any
def get_map_key(
self, map_: dict[int, Any], value: Any
) -> Optional[int]:
if map_ is None:
return None
for key, value in map_.items():
if value == description:
for key, value_ in map_.items():
if value_ == value:
return key
return None
@ -999,11 +1074,12 @@ class MIoTPropertyEntity(Entity):
service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop
# {'min':int, 'max':int, 'step': int}
_value_range: dict[str, int]
_value_range: Optional[MIoTSpecValueRange]
# {Any: Any}
_value_list: dict[Any, Any]
_value_list: Optional[MIoTSpecValueList]
_value: Any
_state_sub_id: int
_value_sub_id: int
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
@ -1015,12 +1091,10 @@ 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._state_sub_id = 0
self._value_sub_id = 0
self._pending_write_ha_state_timer = None
# Gen entity_id
self.entity_id = self.miot_device.gen_prop_entity_id(
@ -1042,16 +1116,16 @@ class MIoTPropertyEntity(Entity):
self._value_list)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
# Sub device state changed
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=f'{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
# Sub value changed
self.miot_device.sub_property(
self._value_sub_id = self.miot_device.sub_property(
handler=self.__on_value_changed,
siid=self.service.iid, piid=self.spec.iid)
# Refresh value
@ -1063,22 +1137,21 @@ class MIoTPropertyEntity(Entity):
self._pending_write_ha_state_timer.cancel()
self._pending_write_ha_state_timer = None
self.miot_device.unsub_device_state(
key=f'{ self.service.iid}.{self.spec.iid}')
key=f'{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
self.miot_device.unsub_property(
siid=self.service.iid, piid=self.spec.iid)
siid=self.service.iid, piid=self.spec.iid,
sub_id=self._value_sub_id)
def get_vlist_description(self, value: Any) -> str:
def get_vlist_description(self, value: Any) -> Optional[str]:
if not self._value_list:
return None
return self._value_list.get(value, None)
return self._value_list.get_description_by_value(value)
def get_vlist_value(self, description: str) -> Any:
if not self._value_list:
return None
for key, value in self._value_list.items():
if value == description:
return key
return None
return self._value_list.get_value_by_description(description)
async def set_property_async(self, value: Any) -> bool:
if not self.spec.writable:
@ -1148,9 +1221,10 @@ class MIoTEventEntity(Entity):
service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop
_value: Any
_attr_event_types: list[str]
_arguments_map: dict[int, str]
_state_sub_id: int
_value_sub_id: int
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:
if miot_device is None or spec is None or spec.service is None:
@ -1159,7 +1233,6 @@ class MIoTEventEntity(Entity):
self.spec = spec
self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop
self._value = None
# Gen entity_id
self.entity_id = self.miot_device.gen_event_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
@ -1177,6 +1250,8 @@ class MIoTEventEntity(Entity):
self._arguments_map = {}
for prop in spec.argument:
self._arguments_map[prop.iid] = prop.description_trans
self._state_sub_id = 0
self._value_sub_id = 0
_LOGGER.info(
'new miot event entity, %s, %s, %s, %s, %s',
@ -1184,29 +1259,31 @@ class MIoTEventEntity(Entity):
spec.device_class, self.entity_id)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
# Sub device state changed
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=f'event.{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
# Sub value changed
self.miot_device.sub_event(
self._value_sub_id = self.miot_device.sub_event(
handler=self.__on_event_occurred,
siid=self.service.iid, eiid=self.spec.iid)
async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state(
key=f'event.{ self.service.iid}.{self.spec.iid}')
key=f'event.{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
self.miot_device.unsub_event(
siid=self.service.iid, eiid=self.spec.iid)
siid=self.service.iid, eiid=self.spec.iid,
sub_id=self._value_sub_id)
@abstractmethod
def on_event_occurred(
self, name: str, arguments: list[dict[int, Any]]
): ...
self, name: str, arguments: dict[str, Any] | None = None
) -> None: ...
def __on_event_occurred(self, params: dict, ctx: Any) -> None:
_LOGGER.debug('event occurred, %s', params)
@ -1253,11 +1330,11 @@ class MIoTActionEntity(Entity):
miot_device: MIoTDevice
spec: MIoTSpecAction
service: MIoTSpecService
action_platform: str
_main_loop: asyncio.AbstractEventLoop
_in_map: dict[int, MIoTSpecProperty]
_out_map: dict[int, MIoTSpecProperty]
_state_sub_id: int
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
if miot_device is None or spec is None or spec.service is None:
@ -1265,8 +1342,8 @@ class MIoTActionEntity(Entity):
self.miot_device = miot_device
self.spec = spec
self.service = spec.service
self.action_platform = 'action'
self._main_loop = miot_device.miot_client.main_loop
self._state_sub_id = 0
# Gen entity_id
self.entity_id = self.miot_device.gen_action_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
@ -1286,19 +1363,22 @@ class MIoTActionEntity(Entity):
spec.device_class, self.entity_id)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
self.miot_device.sub_device_state(
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}',
self._state_sub_id = self.miot_device.sub_device_state(
key=f'a.{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state(
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}')
key=f'a.{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
async def action_async(self, in_list: list = None) -> Optional[list]:
async def action_async(
self, in_list: Optional[list] = None
) -> Optional[list]:
try:
return await self.miot_device.miot_client.action_async(
did=self.miot_device.did,

View File

@ -663,7 +663,8 @@ class _MipsClient(ABC):
def __on_disconnect(self, client, user_data, rc, props) -> None:
if self._mqtt_state:
self.log_error(f'mips disconnect, {rc}, {props}')
(self.log_info if rc == 0 else self.log_error)(
f'mips disconnect, {rc}, {props}')
self._mqtt_state = False
if self._mqtt_timer:
self._mqtt_timer.cancel()

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,6 @@ from enum import Enum, auto
from pathlib import Path
from typing import Any, Optional, Union
import logging
from urllib.request import Request, urlopen
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
@ -66,13 +65,13 @@ from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ed25519
# pylint: disable=relative-beyond-top-level
from .common import load_json_file
from .const import (
DEFAULT_INTEGRATION_LANGUAGE,
MANUFACTURER_EFFECTIVE_TIME,
MIHOME_CA_CERT_STR,
MIHOME_CA_CERT_SHA256)
from .common import MIoTHttp
from .miot_error import MIoTCertError, MIoTError, MIoTStorageError
_LOGGER = logging.getLogger(__name__)
@ -93,10 +92,10 @@ class MIoTStorage:
User data will be stored in the `.storage` directory of Home Assistant.
"""
_main_loop: asyncio.AbstractEventLoop = None
_main_loop: asyncio.AbstractEventLoop
_file_future: dict[str, tuple[MIoTStorageType, asyncio.Future]]
_root_path: str = None
_root_path: str
def __init__(
self, root_path: str,
@ -140,7 +139,7 @@ class MIoTStorage:
if r_data is None:
_LOGGER.error('load error, empty file, %s', full_path)
return None
data_bytes: bytes = None
data_bytes: bytes
# Hash check
if with_hash_check:
if len(r_data) <= 32:
@ -209,17 +208,17 @@ class MIoTStorage:
else:
os.makedirs(os.path.dirname(full_path), exist_ok=True)
try:
type_: type = type(data)
w_bytes: bytes = None
if type_ == bytes:
w_bytes: bytes
if isinstance(data, bytes):
w_bytes = data
elif type_ == str:
elif isinstance(data, str):
w_bytes = data.encode('utf-8')
elif type_ in [dict, list]:
elif isinstance(data, (dict, list)):
w_bytes = json.dumps(data).encode('utf-8')
else:
_LOGGER.error(
'save error, unsupported data type, %s', type_.__name__)
'save error, unsupported data type, %s',
type(data).__name__)
return False
with open(full_path, 'wb') as w_file:
w_file.write(w_bytes)
@ -353,7 +352,8 @@ class MIoTStorage:
def load_file(self, domain: str, name_with_suffix: str) -> Optional[bytes]:
full_path = os.path.join(self._root_path, domain, name_with_suffix)
return self.__load(
full_path=full_path, type_=bytes, with_hash_check=False)
full_path=full_path, type_=bytes,
with_hash_check=False) # type: ignore
async def load_file_async(
self, domain: str, name_with_suffix: str
@ -371,7 +371,7 @@ class MIoTStorage:
None, self.__load, full_path, bytes, False)
if not fut.done():
self.__add_file_future(full_path, MIoTStorageType.LOAD_FILE, fut)
return await fut
return await fut # type: ignore
def remove_file(self, domain: str, name_with_suffix: str) -> bool:
full_path = os.path.join(self._root_path, domain, name_with_suffix)
@ -438,7 +438,7 @@ class MIoTStorage:
domain=config_domain, name=config_name, data=config)
local_config = (self.load(domain=config_domain,
name=config_name, type_=dict)) or {}
local_config.update(config)
local_config.update(config) # type: ignore
return self.save(
domain=config_domain, name=config_name, data=local_config)
@ -474,27 +474,31 @@ class MIoTStorage:
domain=config_domain, name=config_name, data=config)
local_config = (await self.load_async(
domain=config_domain, name=config_name, type_=dict)) or {}
local_config.update(config)
local_config.update(config) # type: ignore
return await self.save_async(
domain=config_domain, name=config_name, data=local_config)
def load_user_config(
self, uid: str, cloud_server: str, keys: Optional[list[str]] = None
) -> dict[str, Any]:
if keys is not None and len(keys) == 0:
if isinstance(keys, list) and len(keys) == 0:
# Do nothing
return {}
config_domain = 'miot_config'
config_name = f'{uid}_{cloud_server}'
local_config = (self.load(domain=config_domain,
name=config_name, type_=dict)) or {}
name=config_name, type_=dict))
if not isinstance(local_config, dict):
return {}
if keys is None:
return local_config
return {key: local_config.get(key, None) for key in keys}
return {
key: local_config[key] for key in keys
if key in local_config}
async def load_user_config_async(
self, uid: str, cloud_server: str, keys: Optional[list[str]] = None
) -> dict[str, Any]:
) -> dict:
"""Load user configuration.
Args:
@ -505,13 +509,15 @@ class MIoTStorage:
Returns:
dict[str, Any]: query result
"""
if keys is not None and len(keys) == 0:
if isinstance(keys, list) and len(keys) == 0:
# Do nothing
return {}
config_domain = 'miot_config'
config_name = f'{uid}_{cloud_server}'
local_config = (await self.load_async(
domain=config_domain, name=config_name, type_=dict)) or {}
domain=config_domain, name=config_name, type_=dict))
if not isinstance(local_config, dict):
return {}
if keys is None:
return local_config
return {
@ -519,7 +525,8 @@ class MIoTStorage:
if key in local_config}
def gen_storage_path(
self, domain: str = None, name_with_suffix: str = None
self, domain: Optional[str] = None,
name_with_suffix: Optional[str] = None
) -> str:
"""Generate file path."""
result = self._root_path
@ -609,9 +616,8 @@ class MIoTCert:
if cert_data is None:
return 0
# Check user cert
user_cert: x509.Certificate = None
try:
user_cert = x509.load_pem_x509_certificate(
user_cert: x509.Certificate = x509.load_pem_x509_certificate(
cert_data, default_backend())
cert_info = {}
for attribute in user_cert.subject:
@ -669,7 +675,8 @@ class MIoTCert:
NameOID.COMMON_NAME, f'mips.{self._uid}.{did_hash}.2'),
]))
csr = builder.sign(
private_key, algorithm=None, backend=default_backend())
private_key, algorithm=None, # type: ignore
backend=default_backend())
return csr.public_bytes(serialization.Encoding.PEM).decode('utf-8')
async def load_user_key_async(self) -> Optional[str]:
@ -719,250 +726,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.
"""
BOOL_TRANS_FILE = 'specs/bool_trans.json'
_main_loop: asyncio.AbstractEventLoop
_lang: str
_data: Optional[dict[str, dict]]
_data_default: 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
data = None
self._data = {}
try:
data = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self.BOOL_TRANS_FILE))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('bool trans, load file error, %s', err)
return
# Check if the file is a valid JSON file
if (
not isinstance(data, dict)
or 'data' not in data
or not isinstance(data['data'], dict)
or 'translate' not in data
or not isinstance(data['translate'], dict)
):
_LOGGER.error('bool trans, valid file')
return
if 'default' in data['translate']:
data_default = (
data['translate']['default'].get(self._lang, None)
or data['translate']['default'].get(
DEFAULT_INTEGRATION_LANGUAGE, None))
if data_default:
self._data_default = [
{'value': True, 'description': data_default['true']},
{'value': False, 'description': data_default['false']}
]
for urn, key in data['data'].items():
if key not in data['translate']:
_LOGGER.error('bool trans, unknown key, %s, %s', urn, key)
continue
trans_data = (
data['translate'][key].get(self._lang, None)
or data['translate'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None))
if trans_data:
self._data[urn] = [
{'value': True, 'description': trans_data['true']},
{'value': False, 'description': trans_data['false']}
]
async def deinit_async(self) -> None:
self._data = None
self._data_default = None
async def translate_async(self, urn: str) -> list[dict[bool, str]]:
"""
MUST call init_async() before calling this method.
[
{'value': True, 'description': 'True'},
{'value': False, 'description': 'False'}
]
"""
return self._data.get(urn, self._data_default)
class SpecFilter:
"""
MIoT-Spec-V2 filter for entity conversion.
"""
SPEC_FILTER_FILE = 'specs/spec_filter.json'
_main_loop: asyncio.AbstractEventLoop
_data: dict[str, dict[str, set]]
_cache: Optional[dict]
def __init__(self, loop: Optional[asyncio.AbstractEventLoop]) -> None:
self._main_loop = loop or asyncio.get_event_loop()
self._data = None
self._cache = None
async def init_async(self) -> None:
if isinstance(self._data, dict):
return
filter_data = None
self._data = {}
try:
filter_data = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
self.SPEC_FILTER_FILE))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('spec filter, load file error, %s', err)
return
if not isinstance(filter_data, dict):
_LOGGER.error('spec filter, invalid spec filter content')
return
for values in list(filter_data.values()):
if not isinstance(values, dict):
_LOGGER.error('spec filter, invalid spec filter data')
return
for value in values.values():
if not isinstance(value, list):
_LOGGER.error('spec filter, invalid spec filter rules')
return
self._data = filter_data
async def deinit_async(self) -> None:
self._cache = None
self._data = None
def filter_spec(self, urn_key: str) -> None:
"""MUST call init_async() first."""
if not self._data:
return
self._cache = self._data.get(urn_key, None)
def filter_service(self, siid: int) -> bool:
"""Filter service by siid.
MUST call init_async() and filter_spec() first."""
if (
self._cache
and 'services' in self._cache
and (
str(siid) in self._cache['services']
or '*' in self._cache['services'])
):
return True
return False
def filter_property(self, siid: int, piid: int) -> bool:
"""Filter property by piid.
MUST call init_async() and filter_spec() first."""
if (
self._cache
and 'properties' in self._cache
and (
f'{siid}.{piid}' in self._cache['properties']
or f'{siid}.*' in self._cache['properties'])
):
return True
return False
def filter_event(self, siid: int, eiid: int) -> bool:
"""Filter event by eiid.
MUST call init_async() and filter_spec() first."""
if (
self._cache
and 'events' in self._cache
and (
f'{siid}.{eiid}' in self._cache['events']
or f'{siid}.*' in self._cache['events']
)
):
return True
return False
def filter_action(self, siid: int, aiid: int) -> bool:
""""Filter action by aiid.
MUST call init_async() and filter_spec() first."""
if (
self._cache
and 'actions' in self._cache
and (
f'{siid}.{aiid}' in self._cache['actions']
or f'{siid}.*' in self._cache['actions'])
):
return True
return False
class DeviceManufacturer:
"""Device manufacturer."""
DOMAIN: str = 'miot_specs'
@ -976,12 +739,11 @@ class DeviceManufacturer:
) -> None:
self._main_loop = loop or asyncio.get_event_loop()
self._storage = storage
self._data = None
self._data = {}
async def init_async(self) -> None:
if self._data:
return
data_cache: dict = None
data_cache = await self._storage.load_async(
domain=self.DOMAIN, name='manufacturer', type_=dict)
if (
@ -995,8 +757,15 @@ class DeviceManufacturer:
_LOGGER.debug('load manufacturer data success')
return
data_cloud = await self._main_loop.run_in_executor(
None, self.__get_manufacturer_data)
data_cloud = None
try:
data_cloud = await MIoTHttp.get_json_async(
url='https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/'
'manufacturer.json',
loop=self._main_loop)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('get manufacturer info failed, %s', err)
if data_cloud:
await self._storage.save_async(
domain=self.DOMAIN, name='manufacturer',
@ -1004,32 +773,16 @@ class DeviceManufacturer:
self._data = data_cloud
_LOGGER.debug('update manufacturer data success')
else:
if data_cache:
self._data = data_cache.get('data', None)
if isinstance(data_cache, dict):
self._data = data_cache.get('data', {})
_LOGGER.error('load manufacturer data failed, use local data')
else:
_LOGGER.error('load manufacturer data failed')
async def deinit_async(self) -> None:
self._data = None
self._data.clear()
def get_name(self, short_name: str) -> str:
if not self._data or not short_name or short_name not in self._data:
return short_name
return self._data[short_name].get('name', None) or short_name
def __get_manufacturer_data(self) -> dict:
try:
request = Request(
'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/'
'manufacturer.json',
method='GET')
content: bytes = None
with urlopen(request) as response:
content = response.read()
return (
json.loads(str(content, 'utf-8'))
if content else None)
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('get manufacturer info failed, %s', err)
return None

View File

@ -1,315 +0,0 @@
{
"data": {
"urn:miot-spec-v2:property:air-cooler:000000EB": "open_close",
"urn:miot-spec-v2:property:alarm:00000012": "open_close",
"urn:miot-spec-v2:property:anion:00000025": "open_close",
"urn:miot-spec-v2:property:anti-fake:00000130": "yes_no",
"urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no",
"urn:miot-spec-v2:property:auto-cleanup:00000124": "open_close",
"urn:miot-spec-v2:property:auto-deodorization:00000125": "open_close",
"urn:miot-spec-v2:property:auto-keep-warm:0000002B": "open_close",
"urn:miot-spec-v2:property:automatic-feeding:000000F0": "open_close",
"urn:miot-spec-v2:property:blow:000000CD": "open_close",
"urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no",
"urn:miot-spec-v2:property:contact-state:0000007C": "contact_state",
"urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close",
"urn:miot-spec-v2:property:delay:0000014F": "yes_no",
"urn:miot-spec-v2:property:deodorization:000000C6": "open_close",
"urn:miot-spec-v2:property:dns-auto-mode:000000DC": "open_close",
"urn:miot-spec-v2:property:driving-status:000000B9": "yes_no",
"urn:miot-spec-v2:property:dryer:00000027": "open_close",
"urn:miot-spec-v2:property:eco:00000024": "open_close",
"urn:miot-spec-v2:property:glimmer-full-color:00000089": "open_close",
"urn:miot-spec-v2:property:guard-mode:000000B6": "open_close",
"urn:miot-spec-v2:property:heater:00000026": "open_close",
"urn:miot-spec-v2:property:heating:000000C7": "open_close",
"urn:miot-spec-v2:property:horizontal-swing:00000017": "open_close",
"urn:miot-spec-v2:property:hot-water-recirculation:0000011C": "open_close",
"urn:miot-spec-v2:property:image-distortion-correction:0000010F": "open_close",
"urn:miot-spec-v2:property:local-storage:0000011E": "yes_no",
"urn:miot-spec-v2:property:motion-detection:00000056": "open_close",
"urn:miot-spec-v2:property:motion-state:0000007D": "motion_state",
"urn:miot-spec-v2:property:motion-tracking:0000008A": "open_close",
"urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no",
"urn:miot-spec-v2:property:mute:00000040": "open_close",
"urn:miot-spec-v2:property:off-delay:00000053": "open_close",
"urn:miot-spec-v2:property:on:00000006": "open_close",
"urn:miot-spec-v2:property:physical-controls-locked:0000001D": "open_close",
"urn:miot-spec-v2:property:plasma:00000132": "yes_no",
"urn:miot-spec-v2:property:preheat:00000103": "open_close",
"urn:miot-spec-v2:property:seating-state:000000B8": "yes_no",
"urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no",
"urn:miot-spec-v2:property:sleep-aid-mode:0000010B": "open_close",
"urn:miot-spec-v2:property:sleep-mode:00000028": "open_close",
"urn:miot-spec-v2:property:snore-state:0000012A": "yes_no",
"urn:miot-spec-v2:property:soft-wind:000000CF": "open_close",
"urn:miot-spec-v2:property:speed-control:000000E8": "open_close",
"urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no",
"urn:miot-spec-v2:property:time-watermark:00000087": "open_close",
"urn:miot-spec-v2:property:un-straight-blowing:00000100": "open_close",
"urn:miot-spec-v2:property:uv:00000029": "open_close",
"urn:miot-spec-v2:property:valve-switch:000000FE": "open_close",
"urn:miot-spec-v2:property:ventilation:000000CE": "open_close",
"urn:miot-spec-v2:property:vertical-swing:00000018": "open_close",
"urn:miot-spec-v2:property:wake-up-mode:00000107": "open_close",
"urn:miot-spec-v2:property:water-pump:000000F2": "open_close",
"urn:miot-spec-v2:property:watering:000000CC": "open_close",
"urn:miot-spec-v2:property:wdr-mode:00000088": "open_close",
"urn:miot-spec-v2:property:wet:0000002A": "open_close",
"urn:miot-spec-v2:property:wifi-band-combine:000000E0": "open_close",
"urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3": "yes_no",
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no"
},
"translate": {
"default": {
"de": {
"true": "Wahr",
"false": "Falsch"
},
"en": {
"true": "True",
"false": "False"
},
"es": {
"true": "Verdadero",
"false": "Falso"
},
"fr": {
"true": "Vrai",
"false": "Faux"
},
"it": {
"true": "Vero",
"false": "Falso"
},
"ja": {
"true": "真",
"false": "偽"
},
"nl": {
"true": "True",
"false": "False"
},
"pt": {
"true": "True",
"false": "False"
},
"pt-BR": {
"true": "True",
"false": "False"
},
"ru": {
"true": "Истина",
"false": "Ложь"
},
"zh-Hans": {
"true": "真",
"false": "假"
},
"zh-Hant": {
"true": "真",
"false": "假"
}
},
"open_close": {
"de": {
"true": "Öffnen",
"false": "Schließen"
},
"en": {
"true": "Open",
"false": "Close"
},
"es": {
"true": "Abierto",
"false": "Cerrado"
},
"fr": {
"true": "Ouvert",
"false": "Fermer"
},
"it": {
"true": "Aperto",
"false": "Chiuso"
},
"ja": {
"true": "開く",
"false": "閉じる"
},
"nl": {
"true": "Open",
"false": "Dicht"
},
"pt": {
"true": "Aberto",
"false": "Fechado"
},
"pt-BR": {
"true": "Aberto",
"false": "Fechado"
},
"ru": {
"true": "Открыть",
"false": "Закрыть"
},
"zh-Hans": {
"true": "开启",
"false": "关闭"
},
"zh-Hant": {
"true": "開啟",
"false": "關閉"
}
},
"yes_no": {
"de": {
"true": "Ja",
"false": "Nein"
},
"en": {
"true": "Yes",
"false": "No"
},
"es": {
"true": "Sí",
"false": "No"
},
"fr": {
"true": "Oui",
"false": "Non"
},
"it": {
"true": "Si",
"false": "No"
},
"ja": {
"true": "はい",
"false": "いいえ"
},
"nl": {
"true": "Ja",
"false": "Nee"
},
"pt": {
"true": "Sim",
"false": "Não"
},
"pt-BR": {
"true": "Sim",
"false": "Não"
},
"ru": {
"true": "Да",
"false": "Нет"
},
"zh-Hans": {
"true": "是",
"false": "否"
},
"zh-Hant": {
"true": "是",
"false": "否"
}
},
"motion_state": {
"de": {
"true": "Bewegung erkannt",
"false": "Keine Bewegung erkannt"
},
"en": {
"true": "Motion Detected",
"false": "No Motion Detected"
},
"es": {
"true": "Movimiento detectado",
"false": "No se detecta movimiento"
},
"fr": {
"true": "Mouvement détecté",
"false": "Aucun mouvement détecté"
},
"it": {
"true": "Movimento Rilevato",
"false": "Nessun Movimento Rilevato"
},
"ja": {
"true": "動きを検知",
"false": "動きが検出されません"
},
"nl": {
"true": "Contact",
"false": "Geen contact"
},
"pt": {
"true": "Contato",
"false": "Sem contato"
},
"pt-BR": {
"true": "Contato",
"false": "Sem contato"
},
"ru": {
"true": "Обнаружено движение",
"false": "Движение не обнаружено"
},
"zh-Hans": {
"true": "有人",
"false": "无人"
},
"zh-Hant": {
"true": "有人",
"false": "無人"
}
},
"contact_state": {
"de": {
"true": "Kontakt",
"false": "Kein Kontakt"
},
"en": {
"true": "Contact",
"false": "No Contact"
},
"es": {
"true": "Contacto",
"false": "Sin contacto"
},
"fr": {
"true": "Contact",
"false": "Pas de contact"
},
"it": {
"true": "Contatto",
"false": "Nessun Contatto"
},
"ja": {
"true": "接触",
"false": "非接触"
},
"nl": {
"true": "Contact",
"false": "Geen contact"
},
"pt": {
"true": "Contato",
"false": "Sem contato"
},
"pt-BR": {
"true": "Contato",
"false": "Sem contato"
},
"ru": {
"true": "Контакт",
"false": "Нет контакта"
},
"zh-Hans": {
"true": "接触",
"false": "分离"
},
"zh-Hant": {
"true": "接觸",
"false": "分離"
}
}
}
}

View File

@ -0,0 +1,246 @@
data:
urn:miot-spec-v2:property:air-cooler:000000EB: open_close
urn:miot-spec-v2:property:alarm:00000012: open_close
urn:miot-spec-v2:property:anion:00000025: open_close
urn:miot-spec-v2:property:anti-fake:00000130: yes_no
urn:miot-spec-v2:property:arrhythmia:000000B4: yes_no
urn:miot-spec-v2:property:auto-cleanup:00000124: open_close
urn:miot-spec-v2:property:auto-deodorization:00000125: open_close
urn:miot-spec-v2:property:auto-keep-warm:0000002B: open_close
urn:miot-spec-v2:property:automatic-feeding:000000F0: open_close
urn:miot-spec-v2:property:blow:000000CD: open_close
urn:miot-spec-v2:property:card-insertion-state:00000106: yes_no
urn:miot-spec-v2:property:contact-state:0000007C: contact_state
urn:miot-spec-v2:property:current-physical-control-lock:00000099: open_close
urn:miot-spec-v2:property:delay:0000014F: yes_no
urn:miot-spec-v2:property:deodorization:000000C6: open_close
urn:miot-spec-v2:property:dns-auto-mode:000000DC: open_close
urn:miot-spec-v2:property:driving-status:000000B9: yes_no
urn:miot-spec-v2:property:dryer:00000027: open_close
urn:miot-spec-v2:property:eco:00000024: open_close
urn:miot-spec-v2:property:glimmer-full-color:00000089: open_close
urn:miot-spec-v2:property:guard-mode:000000B6: open_close
urn:miot-spec-v2:property:heater:00000026: open_close
urn:miot-spec-v2:property:heating:000000C7: open_close
urn:miot-spec-v2:property:horizontal-swing:00000017: open_close
urn:miot-spec-v2:property:hot-water-recirculation:0000011C: open_close
urn:miot-spec-v2:property:image-distortion-correction:0000010F: open_close
urn:miot-spec-v2:property:local-storage:0000011E: yes_no
urn:miot-spec-v2:property:motion-detection:00000056: open_close
urn:miot-spec-v2:property:motion-state:0000007D: motion_state
urn:miot-spec-v2:property:motion-tracking:0000008A: open_close
urn:miot-spec-v2:property:motor-reverse:00000072: yes_no
urn:miot-spec-v2:property:mute:00000040: open_close
urn:miot-spec-v2:property:off-delay:00000053: open_close
urn:miot-spec-v2:property:on:00000006: open_close
urn:miot-spec-v2:property:physical-controls-locked:0000001D: open_close
urn:miot-spec-v2:property:plasma:00000132: yes_no
urn:miot-spec-v2:property:preheat:00000103: open_close
urn:miot-spec-v2:property:seating-state:000000B8: yes_no
urn:miot-spec-v2:property:silent-execution:000000FB: yes_no
urn:miot-spec-v2:property:sleep-aid-mode:0000010B: open_close
urn:miot-spec-v2:property:sleep-mode:00000028: open_close
urn:miot-spec-v2:property:snore-state:0000012A: yes_no
urn:miot-spec-v2:property:soft-wind:000000CF: open_close
urn:miot-spec-v2:property:speed-control:000000E8: open_close
urn:miot-spec-v2:property:submersion-state:0000007E: yes_no
urn:miot-spec-v2:property:time-watermark:00000087: open_close
urn:miot-spec-v2:property:un-straight-blowing:00000100: open_close
urn:miot-spec-v2:property:uv:00000029: open_close
urn:miot-spec-v2:property:valve-switch:000000FE: open_close
urn:miot-spec-v2:property:ventilation:000000CE: open_close
urn:miot-spec-v2:property:vertical-swing:00000018: open_close
urn:miot-spec-v2:property:wake-up-mode:00000107: open_close
urn:miot-spec-v2:property:water-pump:000000F2: open_close
urn:miot-spec-v2:property:watering:000000CC: open_close
urn:miot-spec-v2:property:wdr-mode:00000088: open_close
urn:miot-spec-v2:property:wet:0000002A: open_close
urn:miot-spec-v2:property:wifi-band-combine:000000E0: open_close
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
translate:
contact_state:
de:
'false': Kein Kontakt
'true': Kontakt
en:
'false': No Contact
'true': Contact
es:
'false': Sin contacto
'true': Contacto
fr:
'false': Pas de contact
'true': Contact
it:
'false': Nessun contatto
'true': Contatto
ja:
'false': 非接触
'true': 接触
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Нет контакта
'true': Контакт
zh-Hans:
'false': 分离
'true': 接触
zh-Hant:
'false': 分離
'true': 接觸
default:
de:
'false': Falsch
'true': Wahr
en:
'false': 'False'
'true': 'True'
es:
'false': Falso
'true': Verdadero
fr:
'false': Faux
'true': Vrai
it:
'false': Falso
'true': Vero
ja:
'false':
'true':
nl:
'false': 'False'
'true': 'True'
pt:
'false': 'False'
'true': 'True'
pt-BR:
'false': 'False'
'true': 'True'
ru:
'false': Ложь
'true': Истина
zh-Hans:
'false':
'true':
zh-Hant:
'false':
'true':
motion_state:
de:
'false': Keine Bewegung erkannt
'true': Bewegung erkannt
en:
'false': No Motion Detected
'true': Motion Detected
es:
'false': No se detecta movimiento
'true': Movimiento detectado
fr:
'false': Aucun mouvement détecté
'true': Mouvement détecté
it:
'false': Nessun Movimento Rilevato
'true': Movimento Rilevato
ja:
'false': 動きが検出されません
'true': 動きを検知
nl:
'false': Geen contact
'true': Contact
pt:
'false': Sem contato
'true': Contato
pt-BR:
'false': Sem contato
'true': Contato
ru:
'false': Движение не обнаружено
'true': Обнаружено движение
zh-Hans:
'false': 无人
'true': 有人
zh-Hant:
'false': 無人
'true': 有人
open_close:
de:
'false': Schließen
'true': Öffnen
en:
'false': Close
'true': Open
es:
'false': Cerrado
'true': Abierto
fr:
'false': Fermer
'true': Ouvert
it:
'false': Chiuso
'true': Aperto
ja:
'false': 閉じる
'true': 開く
nl:
'false': Dicht
'true': Open
pt:
'false': Fechado
'true': Aberto
pt-BR:
'false': Fechado
'true': Aberto
ru:
'false': Закрыть
'true': Открыть
zh-Hans:
'false': 关闭
'true': 开启
zh-Hant:
'false': 關閉
'true': 開啟
yes_no:
de:
'false': Nein
'true': Ja
en:
'false': 'No'
'true': 'Yes'
es:
'false': 'No'
'true':
fr:
'false': Non
'true': Oui
it:
'false': 'No'
'true': Si
ja:
'false': いいえ
'true': はい
nl:
'false': Nee
'true': Ja
pt:
'false': Não
'true': Sim
pt-BR:
'false': Não
'true': Sim
ru:
'false': Нет
'true': Да
zh-Hans:
'false':
'true':
zh-Hant:
'false':
'true':

View File

@ -1,194 +0,0 @@
{
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"de": {
"service:001": "Geräteinformationen",
"service:001:property:003": "Geräte-ID",
"service:001:property:005": "Seriennummer (SN)",
"service:002": "Gateway",
"service:002:event:001": "Netzwerk geändert",
"service:002:event:002": "Netzwerk geändert",
"service:002:property:001": "Zugriffsmethode",
"service:002:property:001:valuelist:000": "Kabelgebunden",
"service:002:property:001:valuelist:001": "5G Drahtlos",
"service:002:property:001:valuelist:002": "2.4G Drahtlos",
"service:002:property:002": "IP-Adresse",
"service:002:property:003": "WiFi-Netzwerkname",
"service:002:property:004": "Aktuelle Zeit",
"service:002:property:005": "DHCP-Server-MAC-Adresse",
"service:003": "Anzeigelampe",
"service:003:property:001": "Schalter",
"service:004": "Virtueller Dienst",
"service:004:action:001": "Virtuelles Ereignis erzeugen",
"service:004:event:001": "Virtuelles Ereignis aufgetreten",
"service:004:property:001": "Ereignisname"
},
"en": {
"service:001": "Device Information",
"service:001:property:003": "Device ID",
"service:001:property:005": "Serial Number (SN)",
"service:002": "Gateway",
"service:002:event:001": "Network Changed",
"service:002:event:002": "Network Changed",
"service:002:property:001": "Access Method",
"service:002:property:001:valuelist:000": "Wired",
"service:002:property:001:valuelist:001": "5G Wireless",
"service:002:property:001:valuelist:002": "2.4G Wireless",
"service:002:property:002": "IP Address",
"service:002:property:003": "WiFi Network Name",
"service:002:property:004": "Current Time",
"service:002:property:005": "DHCP Server MAC Address",
"service:003": "Indicator Light",
"service:003:property:001": "Switch",
"service:004": "Virtual Service",
"service:004:action:001": "Generate Virtual Event",
"service:004:event:001": "Virtual Event Occurred",
"service:004:property:001": "Event Name"
},
"es": {
"service:001": "Información del dispositivo",
"service:001:property:003": "ID del dispositivo",
"service:001:property:005": "Número de serie (SN)",
"service:002": "Puerta de enlace",
"service:002:event:001": "Cambio de red",
"service:002:event:002": "Cambio de red",
"service:002:property:001": "Método de acceso",
"service:002:property:001:valuelist:000": "Cableado",
"service:002:property:001:valuelist:001": "5G inalámbrico",
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
"service:002:property:002": "Dirección IP",
"service:002:property:003": "Nombre de red WiFi",
"service:002:property:004": "Hora actual",
"service:002:property:005": "Dirección MAC del servidor DHCP",
"service:003": "Luz indicadora",
"service:003:property:001": "Interruptor",
"service:004": "Servicio virtual",
"service:004:action:001": "Generar evento virtual",
"service:004:event:001": "Ocurrió un evento virtual",
"service:004:property:001": "Nombre del evento"
},
"fr": {
"service:001": "Informations sur l'appareil",
"service:001:property:003": "ID de l'appareil",
"service:001:property:005": "Numéro de série (SN)",
"service:002": "Passerelle",
"service:002:event:001": "Changement de réseau",
"service:002:event:002": "Changement de réseau",
"service:002:property:001": "Méthode d'accès",
"service:002:property:001:valuelist:000": "Câblé",
"service:002:property:001:valuelist:001": "Sans fil 5G",
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
"service:002:property:002": "Adresse IP",
"service:002:property:003": "Nom du réseau WiFi",
"service:002:property:004": "Heure actuelle",
"service:002:property:005": "Adresse MAC du serveur DHCP",
"service:003": "Voyant lumineux",
"service:003:property:001": "Interrupteur",
"service:004": "Service virtuel",
"service:004:action:001": "Générer un événement virtuel",
"service:004:event:001": "Événement virtuel survenu",
"service:004:property:001": "Nom de l'événement"
},
"it": {
"service:001": "Informazioni sul Dispositivo",
"service:001:property:003": "ID Dispositivo",
"service:001:property:005": "Numero di Serie (SN)",
"service:002": "Gateway",
"service:002:event:001": "Rete Modificata",
"service:002:event:002": "Rete Modificata",
"service:002:property:001": "Metodo di Accesso",
"service:002:property:001:valuelist:000": "Cablato",
"service:002:property:001:valuelist:001": "Wireless 5G",
"service:002:property:001:valuelist:002": "Wireless 2.4G",
"service:002:property:002": "Indirizzo IP",
"service:002:property:003": "Nome Rete WiFi",
"service:002:property:004": "Ora Attuale",
"service:002:property:005": "Indirizzo MAC del Server DHCP",
"service:003": "Luce Indicatore",
"service:003:property:001": "Interruttore",
"service:004": "Servizio Virtuale",
"service:004:action:001": "Genera Evento Virtuale",
"service:004:event:001": "Evento Virtuale Avvenuto",
"service:004:property:001": "Nome Evento"
},
"ja": {
"service:001": "デバイス情報",
"service:001:property:003": "デバイスID",
"service:001:property:005": "シリアル番号 (SN)",
"service:002": "ゲートウェイ",
"service:002:event:001": "ネットワークが変更されました",
"service:002:event:002": "ネットワークが変更されました",
"service:002:property:001": "アクセス方法",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G ワイヤレス",
"service:002:property:001:valuelist:002": "2.4G ワイヤレス",
"service:002:property:002": "IPアドレス",
"service:002:property:003": "WiFiネットワーク名",
"service:002:property:004": "現在の時間",
"service:002:property:005": "DHCPサーバーMACアドレス",
"service:003": "インジケータライト",
"service:003:property:001": "スイッチ",
"service:004": "バーチャルサービス",
"service:004:action:001": "バーチャルイベントを生成",
"service:004:event:001": "バーチャルイベントが発生しました",
"service:004:property:001": "イベント名"
},
"ru": {
"service:001": "Информация об устройстве",
"service:001:property:003": "ID устройства",
"service:001:property:005": "Серийный номер (SN)",
"service:002": "Шлюз",
"service:002:event:001": "Сеть изменена",
"service:002:event:002": "Сеть изменена",
"service:002:property:001": "Метод доступа",
"service:002:property:001:valuelist:000": "Проводной",
"service:002:property:001:valuelist:001": "5G Беспроводной",
"service:002:property:001:valuelist:002": "2.4G Беспроводной",
"service:002:property:002": "IP Адрес",
"service:002:property:003": "Название WiFi сети",
"service:002:property:004": "Текущее время",
"service:002:property:005": "MAC адрес DHCP сервера",
"service:003": "Световой индикатор",
"service:003:property:001": "Переключатель",
"service:004": "Виртуальная служба",
"service:004:action:001": "Создать виртуальное событие",
"service:004:event:001": "Произошло виртуальное событие",
"service:004:property:001": "Название события"
},
"zh-Hant": {
"service:001": "設備信息",
"service:001:property:003": "設備ID",
"service:001:property:005": "序號 (SN)",
"service:002": "網關",
"service:002:event:001": "網路發生變化",
"service:002:event:002": "網路發生變化",
"service:002:property:001": "接入方式",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G 無線",
"service:002:property:001:valuelist:002": "2.4G 無線",
"service:002:property:002": "IP地址",
"service:002:property:003": "WiFi網路名稱",
"service:002:property:004": "當前時間",
"service:002:property:005": "DHCP伺服器MAC地址",
"service:003": "指示燈",
"service:003:property:001": "開關",
"service:004": "虛擬服務",
"service:004:action:001": "產生虛擬事件",
"service:004:event:001": "虛擬事件發生",
"service:004:property:001": "事件名稱"
}
},
"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040:1": {
"en": {
"service:011": "Right Button On and Off",
"service:011:property:001": "Right Button On and Off",
"service:015:action:001": "Left Button Identify",
"service:016:action:001": "Middle Button Identify",
"service:017:action:001": "Right Button Identify"
},
"zh-Hans": {
"service:015:action:001": "左键确认",
"service:016:action:001": "中键确认",
"service:017:action:001": "右键确认"
}
}
}

View File

@ -1,68 +0,0 @@
{
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
"properties": [
"9.*",
"13.*",
"15.*"
],
"services": [
"10"
]
},
"urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": {
"properties": [
"5.1"
],
"services": [
"4",
"7",
"8"
]
},
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"events": [
"2.1"
]
},
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
"services": [
"5"
]
},
"urn:miot-spec-v2:device:light:0000A001:philips-strip3": {
"properties": [
"2.2"
],
"services": [
"1",
"3"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-color2": {
"properties": [
"3.*",
"2.5"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2": {
"services": [
"3"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3": {
"services": [
"3"
]
},
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
"services": [
"1",
"5"
]
},
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
"services": [
"*"
]
}
}

View File

@ -0,0 +1,43 @@
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
properties:
- 9.*
- 13.*
- 15.*
services:
- '10'
urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:
properties:
- '5.1'
services:
- '4'
- '7'
- '8'
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:
events:
- '2.1'
urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1:
services:
- '5'
urn:miot-spec-v2:device:light:0000A001:philips-strip3:
properties:
- '2.2'
services:
- '1'
- '3'
urn:miot-spec-v2:device:light:0000A001:yeelink-color2:
properties:
- 3.*
- '2.5'
urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2:
services:
- '3'
urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3:
services:
- '3'
urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
services:
- '1'
- '5'
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
services:
- '*'

View File

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

View File

@ -92,9 +92,9 @@ class Number(MIoTPropertyEntity, NumberEntity):
self._attr_icon = self.spec.icon
# Set value range
if self._value_range:
self._attr_native_min_value = self._value_range['min']
self._attr_native_max_value = self._value_range['max']
self._attr_native_step = self._value_range['step']
self._attr_native_min_value = self._value_range.min_
self._attr_native_max_value = self._value_range.max_
self._attr_native_step = self._value_range.step
@property
def native_value(self) -> Optional[float]:

View File

@ -82,7 +82,8 @@ class Select(MIoTPropertyEntity, SelectEntity):
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:
"""Initialize the Select."""
super().__init__(miot_device=miot_device, spec=spec)
self._attr_options = list(self._value_list.values())
if self._value_list:
self._attr_options = self._value_list.descriptions
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""

View File

@ -76,6 +76,12 @@ async def async_setup_entry(
for prop in miot_device.prop_list.get('sensor', []):
new_entities.append(Sensor(miot_device=miot_device, spec=prop))
if miot_device.miot_client.display_binary_text:
for prop in miot_device.prop_list.get('binary_sensor', []):
if not prop.value_list:
continue
new_entities.append(Sensor(miot_device=miot_device, spec=prop))
if new_entities:
async_add_entities(new_entities)
@ -91,7 +97,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:
@ -100,29 +106,29 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
# device_class is not empty but unit is empty.
# Set the default unit according to device_class.
unit_sets = DEVICE_CLASS_UNITS.get(
self._attr_device_class, None)
self._attr_device_class, None) # type: ignore
self._attr_native_unit_of_measurement = list(
unit_sets)[0] if unit_sets else None
# Set state_class
if spec.state_class:
self._attr_state_class = spec.state_class
# Set icon
if spec.icon:
self._attr_icon = spec.icon
# Set state_class
if spec.state_class:
self._attr_state_class = spec.state_class
@property
def native_value(self) -> Any:
"""Return the current value of the sensor."""
if self._value_range and isinstance(self._value, (int, float)):
if (
self._value < self._value_range['min']
or self._value > self._value_range['max']
self._value < self._value_range.min_
or self._value > self._value_range.max_
):
_LOGGER.info(
'%s, data exception, out of range, %s, %s',
self.entity_id, self._value, self._value_range)
if self._value_list:
return self._value_list.get(self._value, None)
return self.get_vlist_description(self._value)
if isinstance(self._value, str):
return self._value[:255]
return self._value

View File

@ -76,9 +76,10 @@ async def async_setup_entry(
for prop in miot_device.prop_list.get('text', []):
new_entities.append(Text(miot_device=miot_device, spec=prop))
for action in miot_device.action_list.get('action_text', []):
new_entities.append(ActionText(
miot_device=miot_device, spec=action))
if miot_device.miot_client.action_debug:
for action in miot_device.action_list.get('notify', []):
new_entities.append(ActionText(
miot_device=miot_device, spec=action))
if new_entities:
async_add_entities(new_entities)
@ -111,11 +112,9 @@ 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
self.action_platform = 'action_text'
async def async_set_value(self, value: str) -> None:
if not value:
@ -141,24 +140,24 @@ class ActionText(MIoTActionEntity, TextEntity):
f'invalid action params, {value}')
in_value: list[dict] = []
for index, prop in enumerate(self.spec.in_):
if prop.format_ == 'str':
if prop.format_ == str:
if isinstance(in_list[index], (bool, int, float, str)):
in_value.append(
{'piid': prop.iid, 'value': str(in_list[index])})
continue
elif prop.format_ == 'bool':
elif prop.format_ == bool:
if isinstance(in_list[index], (bool, int)):
# yes, no, on, off, true, false and other bool types
# will also be parsed as 0 and 1 of int.
in_value.append(
{'piid': prop.iid, 'value': bool(in_list[index])})
continue
elif prop.format_ == 'float':
elif prop.format_ == float:
if isinstance(in_list[index], (int, float)):
in_value.append(
{'piid': prop.iid, 'value': in_list[index]})
continue
elif prop.format_ == 'int':
elif prop.format_ == int:
if isinstance(in_list[index], int):
in_value.append(
{'piid': prop.iid, 'value': in_list[index]})

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Erweiterte Einstellungen",
"description": "## Gebrauchsanweisung\r\n### Wenn Sie die Bedeutung der folgenden Optionen nicht genau kennen, belassen Sie sie bitte auf den Standardeinstellungen.\r\n### Geräte filtern\r\nUnterstützt das Filtern von Geräten nach Raumnamen und Gerätetypen sowie das Filtern nach Gerätedimensionen.\r\n\r\n### Steuerungsmodus\r\n- Automatisch: Wenn ein verfügbarer Xiaomi-Hub im lokalen Netzwerk vorhanden ist, priorisiert Home Assistant das Senden von Steuerbefehlen über den Hub, um eine lokale Steuerung zu ermöglichen. Wenn kein Hub vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden. Nur wenn diese Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Befehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden ausschließlich über die Cloud gesendet.\r\n### Action-Debug-Modus\r\nFür Methoden, die von MIoT-Spec-V2-Geräten definiert werden, wird neben der Benachrichtigungsentität auch eine Texteingabe-Entität erstellt, mit der Sie während des Debuggens Steuerbefehle an das Gerät senden können.\r\n### Nicht standardmäßige Entitäten ausblenden\r\nBlendet Entitäten aus, die von nicht-standardmäßigen MIoT-Spec-V2-Instanzen generiert werden und deren Name mit „*“ beginnt.\r\n### Gerätestatusänderungen anzeigen\r\nDetaillierte Anzeige von Gerätestatusänderungen, es werden nur die ausgewählten Benachrichtigungen angezeigt.",
"description": "## Gebrauchsanweisung\r\n### Wenn Sie die Bedeutung der folgenden Optionen nicht genau kennen, belassen Sie sie bitte auf den Standardeinstellungen.\r\n### Geräte filtern\r\nUnterstützt das Filtern von Geräten nach Raumnamen und Gerätetypen sowie das Filtern nach Gerätedimensionen.\r\n\r\n### Steuerungsmodus\r\n- Automatisch: Wenn ein verfügbarer Xiaomi-Hub im lokalen Netzwerk vorhanden ist, priorisiert Home Assistant das Senden von Steuerbefehlen über den Hub, um eine lokale Steuerung zu ermöglichen. Wenn kein Hub vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden. Nur wenn diese Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Befehle über die Cloud gesendet.\r\n- Cloud: Steuerbefehle werden ausschließlich über die Cloud gesendet.\r\n### Action-Debug-Modus\r\nFür Methoden, die von MIoT-Spec-V2-Geräten definiert werden, wird neben der Benachrichtigungsentität auch eine Texteingabe-Entität erstellt, mit der Sie während des Debuggens Steuerbefehle an das Gerät senden können.\r\n### Nicht standardmäßige Entitäten ausblenden\r\nBlendet Entitäten aus, die von nicht-standardmäßigen MIoT-Spec-V2-Instanzen generiert werden und deren Name mit „*“ beginnt.\r\n### Binärsensor-Anzeigemodus\r\nZeigt Binärsensoren in Xiaomi Home als Textsensor-Entität oder Binärsensor-Entität an。\r\n### Gerätestatusänderungen anzeigen\r\nDetaillierte Anzeige von Gerätestatusänderungen, es werden nur die ausgewählten Benachrichtigungen angezeigt.",
"data": {
"devices_filter": "Geräte filtern",
"ctrl_mode": "Steuerungsmodus",
"action_debug": "Action-Debug-Modus",
"hide_non_standard_entities": "Nicht standardmäßige Entitäten ausblenden",
"display_binary_mode": "Binärsensor-Anzeigemodus",
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen"
}
},
@ -119,6 +120,7 @@
"update_devices": "Geräteliste aktualisieren",
"action_debug": "Action-Debug-Modus",
"hide_non_standard_entities": "Verstecke Nicht-Standard-Entitäten",
"display_binary_mode": "Binärsensor-Anzeigemodus",
"display_devices_changed_notify": "Gerätestatusänderungen anzeigen",
"update_trans_rules": "Entitätskonvertierungsregeln aktualisieren",
"update_lan_ctrl_config": "LAN-Steuerungskonfiguration aktualisieren",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Advanced Settings",
"description": "## Introduction\r\n### Unless you are very clear about the meaning of the following options, please keep the default settings.\r\n### Filter Devices\r\nSupports filtering devices by room name and device type, and also supports device dimension filtering.\r\n### Control Mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi OT protocol to achieve local control. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Action Debug Mode\r\nFor the methods defined by the device MIoT-Spec-V2, in addition to generating notification entities, a text input box entity will also be generated. You can use it to send control commands to the device during debugging.\r\n### Hide Non-Standard Generated Entities\r\nHide entities generated by non-standard MIoT-Spec-V2 instances with names starting with \"*\".\r\n### Display Device Status Change Notifications\r\nDisplay detailed device status change notifications, only showing the selected notifications.",
"description": "## Introduction\r\n### Unless you are very clear about the meaning of the following options, please keep the default settings.\r\n### Filter Devices\r\nSupports filtering devices by room name and device type, and also supports device dimension filtering.\r\n### Control Mode\r\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi OT protocol to achieve local control. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\r\n- Cloud: All control commands are sent through the cloud.\r\n### Action Debug Mode\r\nFor the methods defined by the device MIoT-Spec-V2, in addition to generating notification entities, a text input box entity will also be generated. You can use it to send control commands to the device during debugging.\r\n### Hide Non-Standard Generated Entities\r\nHide entities generated by non-standard MIoT-Spec-V2 instances with names starting with \"*\".\r\n### Binary Sensor Display Mode\r\nDisplay binary sensors in Xiaomi Home as text sensor entity or binary sensor entity。\r\n### Display Device Status Change Notifications\r\nDisplay detailed device status change notifications, only showing the selected notifications.",
"data": {
"devices_filter": "Filter Devices",
"ctrl_mode": "Control Mode",
"action_debug": "Action Debug Mode",
"hide_non_standard_entities": "Hide Non-Standard Generated Entities",
"display_binary_mode": "Binary Sensor Display Mode",
"display_devices_changed_notify": "Display Device Status Change Notifications"
}
},
@ -119,6 +120,7 @@
"update_devices": "Update device list",
"action_debug": "Debug mode for action",
"hide_non_standard_entities": "Hide non-standard created entities",
"display_binary_mode": "Binary Sensor Display Mode",
"display_devices_changed_notify": "Display device status change notifications",
"update_trans_rules": "Update entity conversion rules",
"update_lan_ctrl_config": "Update LAN control configuration",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Opciones Avanzadas",
"description": "## Introducción\r\n### A menos que entienda claramente el significado de las siguientes opciones, manténgalas en su configuración predeterminada.\r\n### Filtrar dispositivos\r\nAdmite la filtración de dispositivos por nombre de habitación y tipo de dispositivo, y también admite la filtración por familia.\r\n### Modo de Control\r\n- Automático: Cuando hay una puerta de enlace central de Xiaomi disponible en la red local, Home Assistant enviará comandos de control de dispositivos a través de la puerta de enlace central para lograr la función de control local. Cuando no hay una puerta de enlace central en la red local, intentará enviar comandos de control a través del protocolo OT de Xiaomi para lograr la función de control local. Solo cuando no se cumplan las condiciones de control local anteriores, los comandos de control de dispositivos se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Modo de Depuración de Acciones\r\nPara los métodos definidos por el dispositivo MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro de texto que se puede utilizar para enviar comandos de control al dispositivo durante la depuración.\r\n### Ocultar Entidades Generadas No Estándar\r\nOcultar entidades generadas por instancias MIoT-Spec-V2 no estándar que comienzan con \"*\".\r\n### Mostrar notificaciones de cambio de estado del dispositivo\r\nMostrar notificaciones detalladas de cambio de estado del dispositivo, mostrando solo las notificaciones seleccionadas.",
"description": "## Introducción\r\n### A menos que entienda claramente el significado de las siguientes opciones, manténgalas en su configuración predeterminada.\r\n### Filtrar dispositivos\r\nAdmite la filtración de dispositivos por nombre de habitación y tipo de dispositivo, y también admite la filtración por familia.\r\n### Modo de Control\r\n- Automático: Cuando hay una puerta de enlace central de Xiaomi disponible en la red local, Home Assistant enviará comandos de control de dispositivos a través de la puerta de enlace central para lograr la función de control local. Cuando no hay una puerta de enlace central en la red local, intentará enviar comandos de control a través del protocolo OT de Xiaomi para lograr la función de control local. Solo cuando no se cumplan las condiciones de control local anteriores, los comandos de control de dispositivos se enviarán a través de la nube.\r\n- Nube: Los comandos de control solo se envían a través de la nube.\r\n### Modo de Depuración de Acciones\r\nPara los métodos definidos por el dispositivo MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro de texto que se puede utilizar para enviar comandos de control al dispositivo durante la depuración.\r\n### Ocultar Entidades Generadas No Estándar\r\nOcultar entidades generadas por instancias MIoT-Spec-V2 no estándar que comienzan con \"*\".\r\n### Modo de visualización del sensor binario\r\nMuestra los sensores binarios en Xiaomi Home como entidad de sensor de texto o entidad de sensor binario。\r\n### Mostrar notificaciones de cambio de estado del dispositivo\r\nMostrar notificaciones detalladas de cambio de estado del dispositivo, mostrando solo las notificaciones seleccionadas.",
"data": {
"devices_filter": "Filtrar Dispositivos",
"ctrl_mode": "Modo de Control",
"action_debug": "Modo de Depuración de Acciones",
"hide_non_standard_entities": "Ocultar Entidades Generadas No Estándar",
"display_binary_mode": "Modo de visualización del sensor binario",
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo"
}
},
@ -119,6 +120,7 @@
"update_devices": "Actualizar lista de dispositivos",
"action_debug": "Modo de depuración de Action",
"hide_non_standard_entities": "Ocultar entidades generadas no estándar",
"display_binary_mode": "Modo de visualización del sensor binario",
"display_devices_changed_notify": "Mostrar notificaciones de cambio de estado del dispositivo",
"update_trans_rules": "Actualizar reglas de conversión de entidad",
"update_lan_ctrl_config": "Actualizar configuración de control LAN",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Paramètres Avancés",
"description": "## Introduction\r\n### Sauf si vous comprenez très bien la signification des options suivantes, veuillez les laisser par défaut.\r\n### Filtrer les appareils\r\nPrend en charge le filtrage des appareils en fonction du nom de la pièce et du type d'appareil, ainsi que le filtrage basé sur les appareils.\r\n### Mode de Contrôle\r\n- Automatique : Lorsqu'une passerelle Xiaomi est disponible dans le réseau local, Home Assistant enverra les commandes de contrôle des appareils via la passerelle pour permettre le contrôle local. Si aucune passerelle n'est disponible dans le réseau local, Home Assistant essaiera d'envoyer les commandes de contrôle des appareils via le protocole OT Xiaomi pour permettre le contrôle local. Seules si les conditions de contrôle local ci-dessus ne sont pas remplies, les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud : Les commandes de contrôle des appareils sont envoyées uniquement via le cloud.\r\n### Mode de Débogage dActions\r\nPour les méthodes définies par les appareils MIoT-Spec-V2, en plus de générer une entité de notification, une entité de champ de texte sera également générée pour vous permettre d'envoyer des commandes de contrôle aux appareils lors du débogage.\r\n### Masquer les Entités Non Standard\r\nMasquer les entités générées par des instances MIoT-Spec-V2 non standard et commençant par \"*\".\r\n### Afficher les notifications de changement d'état de l'appareil\r\nAfficher les notifications détaillées de changement d'état de l'appareil, en affichant uniquement les notifications sélectionnées.",
"description": "## Introduction\r\n### Sauf si vous comprenez très bien la signification des options suivantes, veuillez les laisser par défaut.\r\n### Filtrer les appareils\r\nPrend en charge le filtrage des appareils en fonction du nom de la pièce et du type d'appareil, ainsi que le filtrage basé sur les appareils.\r\n### Mode de Contrôle\r\n- Automatique : Lorsqu'une passerelle Xiaomi est disponible dans le réseau local, Home Assistant enverra les commandes de contrôle des appareils via la passerelle pour permettre le contrôle local. Si aucune passerelle n'est disponible dans le réseau local, Home Assistant essaiera d'envoyer les commandes de contrôle des appareils via le protocole OT Xiaomi pour permettre le contrôle local. Seules si les conditions de contrôle local ci-dessus ne sont pas remplies, les commandes de contrôle des appareils seront envoyées via le cloud.\r\n- Cloud : Les commandes de contrôle des appareils sont envoyées uniquement via le cloud.\r\n### Mode de Débogage dActions\r\nPour les méthodes définies par les appareils MIoT-Spec-V2, en plus de générer une entité de notification, une entité de champ de texte sera également générée pour vous permettre d'envoyer des commandes de contrôle aux appareils lors du débogage.\r\n### Masquer les Entités Non Standard\r\nMasquer les entités générées par des instances MIoT-Spec-V2 non standard et commençant par \"*\".\r\n### Mode d'affichage du capteur binaire\r\nAffiche les capteurs binaires dans Xiaomi Home comme entité de capteur de texte ou entité de capteur binaire。\r\n### Afficher les notifications de changement d'état de l'appareil\r\nAfficher les notifications détaillées de changement d'état de l'appareil, en affichant uniquement les notifications sélectionnées.",
"data": {
"devices_filter": "Filtrer les Appareils",
"ctrl_mode": "Mode de Contrôle",
"action_debug": "Mode de Débogage dActions",
"hide_non_standard_entities": "Masquer les Entités Non Standard",
"display_binary_mode": "Mode d'affichage du capteur binaire",
"display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil"
}
},
@ -119,6 +120,7 @@
"update_devices": "Mettre à jour la liste des appareils",
"action_debug": "Mode de débogage d'action",
"hide_non_standard_entities": "Masquer les entités générées non standard",
"display_binary_mode": "Mode d'affichage du capteur binaire",
"display_devices_changed_notify": "Afficher les notifications de changement d'état de l'appareil",
"update_trans_rules": "Mettre à jour les règles de conversion d'entités",
"update_lan_ctrl_config": "Mettre à jour la configuration de contrôle LAN",

View File

@ -16,7 +16,7 @@
"cloud_server": "Regione di Login",
"integration_language": "Lingua",
"oauth_redirect_url": "URL di reindirizzamento OAuth2",
"network_detect_config": "Configurazione di rete integrata"
"network_detect_config": "Configurazione di rete integrata"
}
},
"network_detect_config": {
@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Impostazioni Avanzate",
"description": "## Introduzione\r\n### A meno che tu non abbia chiaro il significato delle seguenti opzioni, si prega di mantenere le impostazioni predefinite.\r\n### Filtra Dispositivi\r\nSupporta il filtraggio dei dispositivi per nome della stanza e tipo di dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo.\r\n### Modalità di Controllo\r\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite il protocollo OT di Xiaomi per ottenere il controllo locale. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\r\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\r\n### Modalità di Debug delle Azioni\r\nPer i metodi definiti dal dispositivo MIoT-Spec-V2, oltre a generare entità di notifica, verrà generata anche un'entità di casella di input di testo. È possibile utilizzarla per inviare comandi di controllo al dispositivo durante il debug.\r\n### Nascondi Entità Generate Non Standard\r\nNasconde le entità generate da istanze non standard MIoT-Spec-V2 con nomi che iniziano con \"*\".\r\n### Mostra Notifiche di Cambio di Stato del Dispositivo\r\nMostra notifiche dettagliate sui cambiamenti di stato del dispositivo, mostrando solo le notifiche selezionate.",
"description": "## Introduzione\r\n### A meno che tu non abbia chiaro il significato delle seguenti opzioni, si prega di mantenere le impostazioni predefinite.\r\n### Filtra Dispositivi\r\nSupporta il filtraggio dei dispositivi per nome della stanza e tipo di dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo.\r\n### Modalità di Controllo\r\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite il protocollo OT di Xiaomi per ottenere il controllo locale. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\r\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\r\n### Modalità di Debug delle Azioni\r\nPer i metodi definiti dal dispositivo MIoT-Spec-V2, oltre a generare entità di notifica, verrà generata anche un'entità di casella di input di testo. È possibile utilizzarla per inviare comandi di controllo al dispositivo durante il debug.\r\n### Nascondi Entità Generate Non Standard\r\nNasconde le entità generate da istanze non standard MIoT-Spec-V2 con nomi che iniziano con \"*\".\r\n### Modalità di visualizzazione del sensore binario\r\nVisualizza i sensori binari in Mi Home come entità del sensore di testo o entità del sensore binario。\r\n### Mostra Notifiche di Cambio di Stato del Dispositivo\r\nMostra notifiche dettagliate sui cambiamenti di stato del dispositivo, mostrando solo le notifiche selezionate.",
"data": {
"devices_filter": "Filtra Dispositivi",
"ctrl_mode": "Modalità di Controllo",
"action_debug": "Modalità di Debug delle Azioni",
"hide_non_standard_entities": "Nascondi Entità Generate Non Standard",
"display_binary_mode": "Modalità di visualizzazione del sensore binario",
"display_devices_changed_notify": "Mostra Notifiche di Cambio di Stato del Dispositivo"
}
},
@ -119,6 +120,7 @@
"update_devices": "Aggiorna l'elenco dei dispositivi",
"action_debug": "Modalità debug per azione",
"hide_non_standard_entities": "Nascondi entità create non standard",
"display_binary_mode": "Modalità di visualizzazione del sensore binario",
"display_devices_changed_notify": "Mostra notifiche di cambio stato del dispositivo",
"update_trans_rules": "Aggiorna le regole di conversione delle entità",
"update_lan_ctrl_config": "Aggiorna configurazione del controllo LAN",
@ -219,4 +221,4 @@
"inconsistent_account": "Le informazioni dell'account sono incoerenti."
}
}
}
}

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "高度な設定オプション",
"description": "## 紹介\r\n### 以下のオプションの意味がよくわからない場合は、デフォルトのままにしてください。\r\n### デバイスのフィルタリング\r\n部屋名とデバイスタイプでデバイスをフィルタリングすることができます。デバイスの次元でフィルタリングすることもできます。\r\n### コントロールモード\r\n- 自動ローカルネットワーク内に利用可能なXiaomi中央ゲートウェイがある場合、Home Assistantはデバイス制御命令を送信するために優先的に中央ゲートウェイを使用します。ローカルネットワークに中央ゲートウェイがない場合、Xiaomi OTプロトコルを使用してデバイス制御命令を送信し、ローカル制御機能を実現します。上記のローカル制御条件が満たされない場合のみ、デバイス制御命令はクラウドを介して送信されます。\r\n- クラウド:制御命令はクラウドを介してのみ送信されます。\r\n### Actionデバッグモード\r\nデバイスが定義するMIoT-Spec-V2のメソッドに対して、通知エンティティを生成するだけでなく、デバイスに制御命令を送信するためのテキスト入力ボックスエンティティも生成されます。デバッグ時にデバイスに制御命令を送信するために使用できます。\r\n### 非標準生成エンティティを隠す\r\n「*」で始まる名前の非標準MIoT-Spec-V2インスタンスによって生成されたエンティティを非表示にします。\r\n### デバイスの状態変化通知を表示\r\nデバイスの状態変化通知を詳細に表示し、選択された通知のみを表示します。",
"description": "## 紹介\r\n### 以下のオプションの意味がよくわからない場合は、デフォルトのままにしてください。\r\n### デバイスのフィルタリング\r\n部屋名とデバイスタイプでデバイスをフィルタリングすることができます。デバイスの次元でフィルタリングすることもできます。\r\n### コントロールモード\r\n- 自動ローカルネットワーク内に利用可能なXiaomi中央ゲートウェイがある場合、Home Assistantはデバイス制御命令を送信するために優先的に中央ゲートウェイを使用します。ローカルネットワークに中央ゲートウェイがない場合、Xiaomi OTプロトコルを使用してデバイス制御命令を送信し、ローカル制御機能を実現します。上記のローカル制御条件が満たされない場合のみ、デバイス制御命令はクラウドを介して送信されます。\r\n- クラウド:制御命令はクラウドを介してのみ送信されます。\r\n### Actionデバッグモード\r\nデバイスが定義するMIoT-Spec-V2のメソッドに対して、通知エンティティを生成するだけでなく、デバイスに制御命令を送信するためのテキスト入力ボックスエンティティも生成されます。デバッグ時にデバイスに制御命令を送信するために使用できます。\r\n### 非標準生成エンティティを隠す\r\n「*」で始まる名前の非標準MIoT-Spec-V2インスタンスによって生成されたエンティティを非表示にします。\r\n### バイナリセンサー表示モード\r\nXiaomi Homeのバイナリセンサーをテキストセンサーエンティティまたはバイナリセンサーエンティティとして表示します。\r\n### デバイスの状態変化通知を表示\r\nデバイスの状態変化通知を詳細に表示し、選択された通知のみを表示します。",
"data": {
"devices_filter": "デバイスをフィルタリング",
"ctrl_mode": "コントロールモード",
"action_debug": "Actionデバッグモード",
"hide_non_standard_entities": "非標準生成エンティティを隠す",
"display_binary_mode": "バイナリセンサー表示モード",
"display_devices_changed_notify": "デバイスの状態変化通知を表示"
}
},
@ -119,6 +120,7 @@
"update_devices": "デバイスリストを更新する",
"action_debug": "Action デバッグモード",
"hide_non_standard_entities": "非標準生成エンティティを非表示にする",
"display_binary_mode": "バイナリセンサー表示モード",
"display_devices_changed_notify": "デバイスの状態変化通知を表示",
"update_trans_rules": "エンティティ変換ルールを更新する",
"update_lan_ctrl_config": "LAN制御構成を更新する",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Geavanceerde Instellingen",
"description": "## Inleiding\r\n### Tenzij u zeer goed op de hoogte bent van de betekenis van de volgende opties, houdt u de standaardinstellingen.\r\n### Apparaten filteren\r\nOndersteunt het filteren van apparaten op basis van kamer- en apparaattypen, en ondersteunt ook apparaatdimensiefiltering.\r\n### Besturingsmodus\r\n- Automatisch: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, zal Home Assistant eerst apparaatbesturingsinstructies via de centrale hubgateway verzenden om lokale controlefunctionaliteit te bereiken. Als er geen centrale hub in het lokale netwerk is, zal het proberen om besturingsinstructies via het Xiaomi OT-protocol te verzenden om lokale controlefunctionaliteit te bereiken. Alleen als de bovenstaande lokale controlevoorwaarden niet worden vervuld, worden apparaatbesturingsinstructies via de cloud verzonden.\r\n- Cloud: Besturingsinstructies worden alleen via de cloud verzonden.\r\n### Actie-debugmodus\r\nVoor methoden die zijn gedefinieerd in de MIoT-Spec-V2 van het apparaat, wordt naast het genereren van een meldingsentiteit ook een tekstinvoerveldentiteit gegenereerd. U kunt dit gebruiken om besturingsinstructies naar het apparaat te sturen tijdens het debuggen.\r\n### Niet-standaard entiteiten verbergen\r\nVerberg entiteiten die zijn gegenereerd door niet-standaard MIoT-Spec-V2-instanties die beginnen met \"*\".\r\n### Apparaatstatuswijzigingen weergeven\r\nGedetailleerde apparaatstatuswijzigingen weergeven, alleen de geselecteerde meldingen weergeven.",
"description": "## Inleiding\r\n### Tenzij u zeer goed op de hoogte bent van de betekenis van de volgende opties, houdt u de standaardinstellingen.\r\n### Apparaten filteren\r\nOndersteunt het filteren van apparaten op basis van kamer- en apparaattypen, en ondersteunt ook apparaatdimensiefiltering.\r\n### Besturingsmodus\r\n- Automatisch: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, zal Home Assistant eerst apparaatbesturingsinstructies via de centrale hubgateway verzenden om lokale controlefunctionaliteit te bereiken. Als er geen centrale hub in het lokale netwerk is, zal het proberen om besturingsinstructies via het Xiaomi OT-protocol te verzenden om lokale controlefunctionaliteit te bereiken. Alleen als de bovenstaande lokale controlevoorwaarden niet worden vervuld, worden apparaatbesturingsinstructies via de cloud verzonden.\r\n- Cloud: Besturingsinstructies worden alleen via de cloud verzonden.\r\n### Actie-debugmodus\r\nVoor methoden die zijn gedefinieerd in de MIoT-Spec-V2 van het apparaat, wordt naast het genereren van een meldingsentiteit ook een tekstinvoerveldentiteit gegenereerd. U kunt dit gebruiken om besturingsinstructies naar het apparaat te sturen tijdens het debuggen.\r\n### Niet-standaard entiteiten verbergen\r\nVerberg entiteiten die zijn gegenereerd door niet-standaard MIoT-Spec-V2-instanties die beginnen met \"*\".\r\n### Binaire sensorweergavemodus\r\nToont binaire sensoren in Xiaomi Home als tekstsensor-entiteit of binairesensor-entiteit。\r\n### Apparaatstatuswijzigingen weergeven\r\nGedetailleerde apparaatstatuswijzigingen weergeven, alleen de geselecteerde meldingen weergeven.",
"data": {
"devices_filter": "Apparaten filteren",
"ctrl_mode": "Besturingsmodus",
"action_debug": "Actie-debugmodus",
"hide_non_standard_entities": "Niet-standaard entiteiten verbergen",
"display_binary_mode": "Binaire sensorweergavemodus",
"display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven"
}
},
@ -119,6 +120,7 @@
"update_devices": "Werk apparatenlijst bij",
"action_debug": "Debugmodus voor actie",
"hide_non_standard_entities": "Verberg niet-standaard gemaakte entiteiten",
"display_binary_mode": "Binaire sensorweergavemodus",
"display_devices_changed_notify": "Apparaatstatuswijzigingen weergeven",
"update_trans_rules": "Werk entiteitsconversieregels bij",
"update_lan_ctrl_config": "Werk LAN controleconfiguratie bij",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Configurações Avançadas",
"description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções a seguir, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi disponível na rede local está disponível, o Home Assistant enviará comandos de controle de dispositivo através do gateway central para realizar a função de controle local. Quando não há gateway central na rede local, ele tentará enviar comandos de controle através do protocolo OT da Xiaomi para realizar a função de controle local. Somente quando as condições de controle local acima não forem atendidas, os comandos de controle do dispositivo serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controle são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2 do dispositivo, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para você enviar comandos de controle ao dispositivo durante a depuração.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão que começam com \"*\".\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.",
"description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções a seguir, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi disponível na rede local está disponível, o Home Assistant enviará comandos de controle de dispositivo através do gateway central para realizar a função de controle local. Quando não há gateway central na rede local, ele tentará enviar comandos de controle através do protocolo OT da Xiaomi para realizar a função de controle local. Somente quando as condições de controle local acima não forem atendidas, os comandos de controle do dispositivo serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controle são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2 do dispositivo, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para você enviar comandos de controle ao dispositivo durante a depuração.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão que começam com \"*\".\r\n### Modo de exibição do sensor binário\r\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.",
"data": {
"devices_filter": "Filtrar Dispositivos",
"ctrl_mode": "Modo de Controle",
"action_debug": "Modo de Depuração de Ações",
"hide_non_standard_entities": "Ocultar Entidades Geradas Não Padrão",
"display_binary_mode": "Modo de exibição do sensor binário",
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo"
}
},
@ -119,6 +120,7 @@
"update_devices": "Atualizar lista de dispositivos",
"action_debug": "Modo de depuração para ação",
"hide_non_standard_entities": "Ocultar entidades não padrão criadas",
"display_binary_mode": "Modo de exibição do sensor binário",
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
"update_trans_rules": "Atualizar regras de conversão de entidades",
"update_lan_ctrl_config": "Atualizar configuração de controle LAN",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Opções Avançadas",
"description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções abaixo, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi está disponível na rede local, o Home Assistant enviará comandos de controlo de dispositivos através do gateway central para realizar o controlo local. Quando não há gateway central na rede local, tentará enviar comandos de controlo através do protocolo Xiaomi OT para realizar o controlo local. Apenas quando as condições de controlo local acima não são atendidas, os comandos de controlo de dispositivos serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controlo são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para depuração de controlo de dispositivos.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão, cujos nomes começam com \"*\".\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.",
"description": "## Introdução\r\n### A menos que você entenda claramente o significado das opções abaixo, mantenha as configurações padrão.\r\n### Filtrar Dispositivos\r\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\r\n### Modo de Controle\r\n- Automático: Quando um gateway central Xiaomi está disponível na rede local, o Home Assistant enviará comandos de controlo de dispositivos através do gateway central para realizar o controlo local. Quando não há gateway central na rede local, tentará enviar comandos de controlo através do protocolo Xiaomi OT para realizar o controlo local. Apenas quando as condições de controlo local acima não são atendidas, os comandos de controlo de dispositivos serão enviados através da nuvem.\r\n- Nuvem: Os comandos de controlo são enviados apenas através da nuvem.\r\n### Modo de Depuração de Ações\r\nPara métodos definidos pelo MIoT-Spec-V2, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para depuração de controlo de dispositivos.\r\n### Ocultar Entidades Geradas Não Padrão\r\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão, cujos nomes começam com \"*\".\r\n### Modo de exibição do sensor binário\r\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\r\n### Exibir notificações de mudança de status do dispositivo\r\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.",
"data": {
"devices_filter": "Filtrar Dispositivos",
"ctrl_mode": "Modo de Controlo",
"action_debug": "Modo de Depuração de Ações",
"hide_non_standard_entities": "Ocultar Entidades Geradas Não Padrão",
"display_binary_mode": "Modo de exibição do sensor binário",
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo"
}
},
@ -119,6 +120,7 @@
"update_devices": "Atualizar lista de dispositivos",
"action_debug": "Modo de depuração de ação",
"hide_non_standard_entities": "Ocultar entidades não padrão",
"display_binary_mode": "Modo de exibição do sensor binário",
"display_devices_changed_notify": "Exibir notificações de mudança de status do dispositivo",
"update_trans_rules": "Atualizar regras de conversão de entidades",
"update_lan_ctrl_config": "Atualizar configuração de controlo LAN",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "Расширенные настройки",
"description": "## Введение\r\n### Если вы не очень хорошо понимаете значение следующих параметров, оставьте их по умолчанию.\r\n### Фильтрация устройств\r\nПоддерживает фильтрацию устройств по названию комнаты и типу устройства, а также фильтрацию по уровню устройства.\r\n### Режим управления\r\n- Автоматически: при наличии доступного центрального шлюза Xiaomi в локальной сети Home Assistant Home Assistant будет отправлять команды управления устройствами через центральный шлюз для локального управления. Если центрального шлюза нет в локальной сети, Home Assistant попытается отправить команды управления устройствами через протокол OT Xiaomi для локального управления. Только если вышеуказанные условия локального управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: команды управления отправляются только через облако.\r\n### Режим отладки действий\r\nДля методов, определенных устройством MIoT-Spec-V2, помимо создания уведомления, будет создана сущность текстового поля, которую вы можете использовать для отправки команд управления устройством во время отладки.\r\n### Скрыть нестандартные сущности\r\nСкрыть сущности, созданные нестандартными экземплярами MIoT-Spec-V2, имена которых начинаются с «*».\r\n### Отображать уведомления о изменении состояния устройства\r\nОтображать подробные уведомления о изменении состояния устройства, показывая только выбранные уведомления.",
"description": "## Введение\r\n### Если вы не очень хорошо понимаете значение следующих параметров, оставьте их по умолчанию.\r\n### Фильтрация устройств\r\nПоддерживает фильтрацию устройств по названию комнаты и типу устройства, а также фильтрацию по уровню устройства.\r\n### Режим управления\r\n- Автоматически: при наличии доступного центрального шлюза Xiaomi в локальной сети Home Assistant Home Assistant будет отправлять команды управления устройствами через центральный шлюз для локального управления. Если центрального шлюза нет в локальной сети, Home Assistant попытается отправить команды управления устройствами через протокол OT Xiaomi для локального управления. Только если вышеуказанные условия локального управления не выполняются, команды управления устройствами будут отправляться через облако.\r\n- Облако: команды управления отправляются только через облако.\r\n### Режим отладки действий\r\nДля методов, определенных устройством MIoT-Spec-V2, помимо создания уведомления, будет создана сущность текстового поля, которую вы можете использовать для отправки команд управления устройством во время отладки.\r\n### Скрыть нестандартные сущности\r\nСкрыть сущности, созданные нестандартными экземплярами MIoT-Spec-V2, имена которых начинаются с «*».\r\n### Режим отображения бинарного датчика\r\nОтображает бинарные датчики в Xiaomi Home как сущность текстового датчика или сущность бинарного датчика。\r\n### Отображать уведомления о изменении состояния устройства\r\nОтображать подробные уведомления о изменении состояния устройства, показывая только выбранные уведомления.",
"data": {
"devices_filter": "Фильтрация устройств",
"ctrl_mode": "Режим управления",
"action_debug": "Режим отладки действий",
"hide_non_standard_entities": "Скрыть нестандартные сущности",
"display_binary_mode": "Режим отображения бинарного датчика",
"display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства"
}
},
@ -119,6 +120,7 @@
"update_devices": "Обновить список устройств",
"action_debug": "Режим отладки Action",
"hide_non_standard_entities": "Скрыть нестандартные сущности",
"display_binary_mode": "Режим отображения бинарного датчика",
"display_devices_changed_notify": "Отображать уведомления о изменении состояния устройства",
"update_trans_rules": "Обновить правила преобразования сущностей",
"update_lan_ctrl_config": "Обновить конфигурацию управления LAN",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "高级设置选项",
"description": "## 使用介绍\r\n### 除非您非常清楚下列选项的含义,否则请保持默认。\r\n### 筛选设备\r\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备同时也支持设备维度筛选。\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令以实现本地化控制功能。本地局域网不存在中枢时会尝试通过小米OT协议发送控制指令以实现本地化控制功能。只有当上述本地化控制条件不满足时设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### Action 调试模式\r\n对于设备 MIoT-Spec-V2 定义的方法,在生成通知实体之外,还会生成一个文本输入框实体,您可以在调试时用它向设备发送控制指令。\r\n### 隐藏非标准生成实体\r\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\r\n### 显示设备状态变化通知\r\n细化显示设备状态变化通知只显示勾选的通知消息。",
"description": "## 使用介绍\r\n### 除非您非常清楚下列选项的含义,否则请保持默认。\r\n### 筛选设备\r\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备同时也支持设备维度筛选。\r\n### 控制模式\r\n- 自动:本地局域网内存在可用的小米中枢网关时, Home Assistant 会优先通过中枢网关发送设备控制指令以实现本地化控制功能。本地局域网不存在中枢时会尝试通过小米OT协议发送控制指令以实现本地化控制功能。只有当上述本地化控制条件不满足时设备控制指令才会通过云端发送。\r\n- 云端:控制指令仅通过云端发送。\r\n### Action 调试模式\r\n对于设备 MIoT-Spec-V2 定义的方法,在生成通知实体之外,还会生成一个文本输入框实体,您可以在调试时用它向设备发送控制指令。\r\n### 隐藏非标准生成实体\r\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\r\n### 二进制传感器显示模式\r\n将米家中的二进制传感器显示为文本传感器实体或者二进制传感器实体。\r\n### 显示设备状态变化通知\r\n细化显示设备状态变化通知只显示勾选的通知消息。",
"data": {
"devices_filter": "筛选设备",
"ctrl_mode": "控制模式",
"action_debug": "Action 调试模式",
"hide_non_standard_entities": "隐藏非标准生成实体",
"display_binary_mode": "二进制传感器显示模式",
"display_devices_changed_notify": "显示设备状态变化通知"
}
},
@ -119,6 +120,7 @@
"update_devices": "更新设备列表",
"action_debug": "Action 调试模式",
"hide_non_standard_entities": "隐藏非标准生成实体",
"display_binary_mode": "二进制传感器显示模式",
"display_devices_changed_notify": "显示设备状态变化通知",
"update_trans_rules": "更新实体转换规则",
"update_lan_ctrl_config": "更新局域网控制配置",

View File

@ -42,12 +42,13 @@
},
"advanced_options": {
"title": "高級設置選項",
"description": "## 使用介紹\r\n### 除非您非常清楚下列選項的含義,否則請保持默認。\r\n### 篩選設備\r\n支持按照房間名稱和設備類型篩選設備同時也支持設備維度篩選。\r\n### 控制模式\r\n- 自動:本地局域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令以實現本地化控制功能。本地局域網不存在中樞時會嘗試通過小米OT協議發送控制指令以實現本地化控制功能。只有當上述本地化控制條件不滿足時設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令僅通過雲端發送。\r\n### Action 調試模式\r\n對於設備 MIoT-Spec-V2 定義的方法,在生成通知實體之外,還會生成一個文本輸入框實體,您可以在調試時用它向設備發送控制指令。\r\n### 隱藏非標準生成實體\r\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\r\n### 顯示設備狀態變化通知\r\n細化顯示設備狀態變化通知只顯示勾選的通知消息。",
"description": "## 使用介紹\r\n### 除非您非常清楚下列選項的含義,否則請保持默認。\r\n### 篩選設備\r\n支持按照房間名稱和設備類型篩選設備同時也支持設備維度篩選。\r\n### 控制模式\r\n- 自動:本地局域網內存在可用的小米中樞網關時, Home Assistant 會優先通過中樞網關發送設備控制指令以實現本地化控制功能。本地局域網不存在中樞時會嘗試通過小米OT協議發送控制指令以實現本地化控制功能。只有當上述本地化控制條件不滿足時設備控制指令才會通過雲端發送。\r\n- 雲端:控制指令僅通過雲端發送。\r\n### Action 調試模式\r\n對於設備 MIoT-Spec-V2 定義的方法,在生成通知實體之外,還會生成一個文本輸入框實體,您可以在調試時用它向設備發送控制指令。\r\n### 隱藏非標準生成實體\r\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\r\n### 二進制傳感器顯示模式\r\n將米家中的二進制傳感器顯示為文本傳感器實體或者二進制傳感器實體。\r\n### 顯示設備狀態變化通知\r\n細化顯示設備狀態變化通知只顯示勾選的通知消息。",
"data": {
"devices_filter": "篩選設備",
"ctrl_mode": "控制模式",
"action_debug": "Action 調試模式",
"hide_non_standard_entities": "隱藏非標準生成實體",
"display_binary_mode": "二進制傳感器顯示模式",
"display_devices_changed_notify": "顯示設備狀態變化通知"
}
},
@ -119,6 +120,7 @@
"update_devices": "更新設備列表",
"action_debug": "Action 調試模式",
"hide_non_standard_entities": "隱藏非標準生成實體",
"display_binary_mode": "二進制傳感器顯示模式",
"display_devices_changed_notify": "顯示設備狀態變化通知",
"update_trans_rules": "更新實體轉換規則",
"update_lan_ctrl_config": "更新局域網控制配置",

View File

@ -120,28 +120,18 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
# properties
for prop in entity_data.props:
if prop.name == 'status':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid status value_list, %s', self.entity_id)
continue
self._status_map = {
item['value']: item['description']
for item in prop.value_list}
self._status_map = prop.value_list.to_map()
self._prop_status = prop
elif prop.name == 'fan-level':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
continue
self._fan_level_map = {
item['value']: item['description']
for item in prop.value_list}
self._fan_level_map = prop.value_list.to_map()
self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop
@ -202,7 +192,7 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
return self.get_map_description(
return self.get_map_value(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
@ -214,6 +204,6 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
@property
def fan_speed(self) -> Optional[str]:
"""Return the current fan speed of the vacuum cleaner."""
return self.get_map_description(
return self.get_map_value(
map_=self._fan_level_map,
key=self.get_prop_value(prop=self._prop_fan_level))

View File

@ -93,7 +93,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
_prop_target_temp: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty]
_mode_list: Optional[dict[Any, Any]]
_mode_map: Optional[dict[Any, Any]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
@ -106,7 +106,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_temp = None
self._prop_target_temp = None
self._prop_mode = None
self._mode_list = None
self._mode_map = None
# properties
for prop in entity_data.props:
@ -115,7 +115,7 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_on = prop
# temperature
if prop.name == 'temperature':
if isinstance(prop.value_range, dict):
if prop.value_range:
if (
self._attr_temperature_unit is None
and prop.external_unit
@ -128,9 +128,14 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self.entity_id)
# target-temperature
if prop.name == 'target-temperature':
self._attr_min_temp = prop.value_range['min']
self._attr_max_temp = prop.value_range['max']
self._attr_precision = prop.value_range['step']
if not prop.value_range:
_LOGGER.error(
'invalid target-temperature value_range format, %s',
self.entity_id)
continue
self._attr_min_temp = prop.value_range.min_
self._attr_max_temp = prop.value_range.max_
self._attr_precision = prop.value_range.step
if self._attr_temperature_unit is None and prop.external_unit:
self._attr_temperature_unit = prop.external_unit
self._attr_supported_features |= (
@ -138,17 +143,12 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
self._prop_target_temp = prop
# mode
if prop.name == 'mode':
if (
not isinstance(prop.value_list, list)
or not prop.value_list
):
if not prop.value_list:
_LOGGER.error(
'mode value_list is None, %s', self.entity_id)
continue
self._mode_list = {
item['value']: item['description']
for item in prop.value_list}
self._attr_operation_list = list(self._mode_list.values())
self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop
@ -184,7 +184,9 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
prop=self._prop_on, value=True, update=False)
await self.set_property_async(
prop=self._prop_mode,
value=self.__get_mode_value(description=operation_mode))
value=self.get_map_key(
map_=self._mode_map,
value=operation_mode))
async def async_turn_away_mode_on(self) -> None:
"""Set the water heater to away mode."""
@ -207,20 +209,6 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON
return self.__get_mode_description(
return self.get_map_value(
map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode))
def __get_mode_description(self, key: int) -> Optional[str]:
"""Convert mode value to description."""
if self._mode_list is None:
return None
return self._mode_list.get(key, None)
def __get_mode_value(self, description: str) -> Optional[int]:
"""Convert mode description to value."""
if self._mode_list is None:
return None
for key, value in self._mode_list.items():
if value == description:
return key
return None

View File

@ -16,13 +16,10 @@ MIOT_I18N_RELATIVE_PATH: str = path.join(
ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')
SPEC_BOOL_TRANS_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/bool_trans.json')
SPEC_MULTI_LANG_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/multi_lang.json')
'../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')
SPEC_FILTER_FILE = path.join(
ROOT_PATH,
'../custom_components/xiaomi_home/miot/specs/spec_filter.json')
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
def load_json_file(file_path: str) -> Optional[dict]:
@ -54,6 +51,12 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
return None
def save_yaml_file(file_path: str, data: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as file:
yaml.safe_dump(
data, file, default_flow_style=False, allow_unicode=True, indent=2)
def dict_str_str(d: dict) -> bool:
"""restricted format: dict[str, str]"""
if not isinstance(d, dict):
@ -161,25 +164,17 @@ def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
def sort_bool_trans(file_path: str):
trans_data: dict = load_json_file(file_path=file_path)
trans_data = load_yaml_file(file_path=file_path)
assert isinstance(trans_data, dict), f'{file_path} format error'
trans_data['data'] = dict(sorted(trans_data['data'].items()))
for key, trans in trans_data['translate'].items():
trans_data['translate'][key] = dict(sorted(trans.items()))
return trans_data
def sort_multi_lang(file_path: str):
multi_lang: dict = load_json_file(file_path=file_path)
multi_lang = dict(sorted(multi_lang.items()))
for urn, trans in multi_lang.items():
multi_lang[urn] = dict(sorted(trans.items()))
for lang, spec in multi_lang[urn].items():
multi_lang[urn][lang] = dict(sorted(spec.items()))
return multi_lang
def sort_spec_filter(file_path: str):
filter_data: dict = load_json_file(file_path=file_path)
filter_data = load_yaml_file(file_path=file_path)
assert isinstance(filter_data, dict), f'{file_path} format error'
filter_data = dict(sorted(filter_data.items()))
for urn, spec in filter_data.items():
filter_data[urn] = dict(sorted(spec.items()))
@ -188,30 +183,26 @@ def sort_spec_filter(file_path: str):
@pytest.mark.github
def test_bool_trans():
data: dict = load_json_file(SPEC_BOOL_TRANS_FILE)
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_BOOL_TRANS_FILE} failed'
assert bool_trans(data), f'{SPEC_BOOL_TRANS_FILE} format error'
@pytest.mark.github
def test_spec_filter():
data: dict = load_json_file(SPEC_FILTER_FILE)
data = load_yaml_file(SPEC_FILTER_FILE)
assert isinstance(data, dict)
assert data, f'load {SPEC_FILTER_FILE} failed'
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
@pytest.mark.github
def test_multi_lang():
data: dict = load_json_file(SPEC_MULTI_LANG_FILE)
assert data, f'load {SPEC_MULTI_LANG_FILE} failed'
assert nested_3_dict_str_str(data), f'{SPEC_MULTI_LANG_FILE} format error'
@pytest.mark.github
def test_miot_i18n():
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
file_path: str = path.join(MIOT_I18N_RELATIVE_PATH, file_name)
data: dict = load_json_file(file_path)
data = load_json_file(file_path)
assert isinstance(data, dict)
assert data, f'load {file_path} failed'
assert nested_3_dict_str_str(data), f'{file_path} format error'
@ -220,7 +211,8 @@ def test_miot_i18n():
def test_translations():
for file_name in listdir(TRANS_RELATIVE_PATH):
file_path: str = path.join(TRANS_RELATIVE_PATH, file_name)
data: dict = load_json_file(file_path)
data = load_json_file(file_path)
assert isinstance(data, dict)
assert data, f'load {file_path} failed'
assert dict_str_dict(data), f'{file_path} format error'
@ -237,15 +229,16 @@ def test_miot_lang_integrity():
i18n_names: set[str] = set(listdir(MIOT_I18N_RELATIVE_PATH))
assert len(i18n_names) == len(translations_names)
assert i18n_names == translations_names
bool_trans_data: set[str] = load_json_file(SPEC_BOOL_TRANS_FILE)
bool_trans_data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
assert isinstance(bool_trans_data, dict)
bool_trans_names: set[str] = set(
bool_trans_data['translate']['default'].keys())
assert len(bool_trans_names) == len(translations_names)
# Check translation files structure
default_dict: dict = load_json_file(
default_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]:
compare_dict: dict = load_json_file(
compare_dict = load_json_file(
path.join(TRANS_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info(
@ -255,7 +248,7 @@ def test_miot_lang_integrity():
default_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))
for name in list(integration_lang_list)[1:]:
compare_dict: dict = load_json_file(
compare_dict = load_json_file(
path.join(MIOT_I18N_RELATIVE_PATH, name))
if not compare_dict_structure(default_dict, compare_dict):
_LOGGER.info(
@ -272,19 +265,13 @@ def test_miot_data_sort():
'INTEGRATION_LANGUAGES not sorted, correct order\r\n'
f'{list(sort_langs.keys())}')
assert json.dumps(
load_json_file(file_path=SPEC_BOOL_TRANS_FILE)) == json.dumps(
load_yaml_file(file_path=SPEC_BOOL_TRANS_FILE)) == json.dumps(
sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)), (
f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(
load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(
sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (
f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
'pytest -s -v -m update ./test/check_rule_format.py')
assert json.dumps(
load_json_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(
sort_spec_filter(file_path=SPEC_FILTER_FILE)), (
f'{SPEC_FILTER_FILE} not sorted, goto project root path'
' and run the following command sorting, ',
@ -294,11 +281,8 @@ def test_miot_data_sort():
@pytest.mark.update
def test_sort_spec_data():
sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)
save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)
save_yaml_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_BOOL_TRANS_FILE)
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_MULTI_LANG_FILE)
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data)
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)