初始化config文件

This commit is contained in:
2026-08-24 21:55:33 +08:00
commit 80a05d172e
2983 changed files with 1221334 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
"""Support alarm_control_panel entity for Xiaomi Miot."""
import logging
from homeassistant.components.alarm_control_panel import (
DOMAIN as ENTITY_DOMAIN,
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, # v2022.5
)
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
MiotEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
)
from .core.const import AlarmControlPanelState
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services('arming'):
if not srv.get_property('arming_mode'):
continue
entities.append(MiotAlarmEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class MiotAlarmEntity(MiotEntity, AlarmControlPanelEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
self._attr_code_arm_required = False
self._is_mgl03 = self.model == 'lumi.gateway.mgl03'
self._prop_mode = miot_service.get_property('arming_mode')
if self._prop_mode:
if self._prop_mode.list_value('home_arming') is not None:
self._supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
if self._prop_mode.list_value('away_arming') is not None:
self._supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
if self._prop_mode.list_value('sleep_arming') is not None:
self._supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
if self._is_mgl03:
self._supported_features |= AlarmControlPanelEntityFeature.TRIGGER
async def async_update(self):
await super().async_update()
if not self._available:
return
self.update_state()
def update_state(self):
sta = None
if self._prop_mode:
val = self._prop_mode.from_device(self.device)
des = self._prop_mode.list_description(val) if val is not None else None
if des is not None:
des = f'{des}'.lower()
if 'basic' in des:
sta = AlarmControlPanelState.DISARMED
elif 'home' in des:
sta = AlarmControlPanelState.ARMED_HOME
elif 'away' in des:
sta = AlarmControlPanelState.ARMED_AWAY
elif 'sleep' in des:
sta = AlarmControlPanelState.ARMED_NIGHT
if self._is_mgl03:
if self.device.props.get('arming.alarm'):
sta = AlarmControlPanelState.TRIGGERED
if hasattr(self, '_attr_alarm_state'):
self._attr_alarm_state = sta
else:
self._attr_state = sta
async def async_set_arm_mode(self, mode):
ret = False
val = self._prop_mode.list_value(mode)
if val is not None:
ret = await self.async_set_property(self._prop_mode, val)
if ret:
self.update_state()
return ret
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
return await self.async_set_arm_mode('basic_arming')
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
return await self.async_set_arm_mode('home_arming')
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
return await self.async_set_arm_mode('away_arming')
async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
return await self.async_set_arm_mode('sleep_arming')
def alarm_arm_vacation(self, code=None):
"""Send arm vacation command."""
raise NotImplementedError()
async def async_alarm_trigger(self, code=None):
"""Send alarm trigger command."""
if self._is_mgl03:
return await self.async_set_miot_property(3, 22, 1)
raise NotImplementedError()
def alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command."""
raise NotImplementedError()
@@ -0,0 +1,343 @@
"""Support for Xiaomi binary sensors."""
import logging
import time
import json
from datetime import datetime
from functools import cached_property
from homeassistant.components.binary_sensor import (
DOMAIN as ENTITY_DOMAIN,
BinarySensorEntity as BaseEntity,
BinarySensorDeviceClass,
)
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotToggleEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
)
from .core.xiaomi_cloud import MiotCloud
from .core.utils import local_zone
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
did = str(config.get('miot_did') or '')
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services(
'toilet', 'seat', 'motion_sensor', 'magnet_sensor', 'submersion_sensor', 'alertor',
):
if spec.get_service('nobody_time'):
# lumi.motion.agl02
# lumi.motion.agl04
pass
elif model in ['lumi.sensor_wleak.aq1', 'htcx.alarm.dt210']:
pass
elif not srv.mapping():
continue
if srv.name in ['toilet']:
entities.append(MiotToiletEntity(config, srv))
elif srv.name in ['seat'] and spec.name in ['toilet']:
if spec.get_service('toilet'):
continue
# tinymu.toiletlid.v1
entities.append(MiotToiletEntity(config, srv))
elif 'blt.' in did:
entities.append(BleBinarySensorEntity(config, srv))
elif 'lumi.' in model:
entities.append(LumiBinarySensorEntity(config, srv))
else:
entities.append(MiotBinarySensorEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class BinarySensorEntity(XEntity, BaseEntity, RestoreEntity):
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
if self.custom_reverse:
self._attr_extra_state_attributes['reverse_state'] = True
val = not val
self._attr_is_on = val
def get_state(self) -> dict:
return {self.attr: not self._attr_is_on if self.custom_reverse else self._attr_is_on}
@cached_property
def custom_reverse(self):
return self.custom_config_bool('reverse_state', False)
XEntity.CLS[ENTITY_DOMAIN] = BinarySensorEntity
class MiotBinarySensorEntity(MiotToggleEntity, BaseEntity):
def __init__(self, config, miot_service: MiotService, **kwargs):
kwargs.setdefault('logger', _LOGGER)
super().__init__(miot_service, config=config, **kwargs)
pls = []
if len(miot_service.properties) > 0:
first_property = list(miot_service.properties.values() or [])[0].name
if first_property:
pls.append(first_property)
self._prop_state = miot_service.get_property(*pls)
if miot_service.name in ['motion_sensor']:
self._prop_state = miot_service.get_property('motion_state', 'no_motion_duration') or self._prop_state
if self._prop_state.name in ['illumination']:
# cgllc.motion.cgpr1
self._prop_state = None
self._attr_device_class = BinarySensorDeviceClass.MOTION
if self._prop_state is None:
srv = miot_service.spec.get_service('nobody_time')
if srv:
self._prop_state = srv.get_property('nobody_time')
if miot_service.name in ['magnet_sensor']:
self._prop_state = miot_service.get_property('contact_state') or self._prop_state
if self._prop_state and self._prop_state.name in ['contact_state']:
# https://github.com/al-one/hass-xiaomi-miot/issues/270
self._vars['reverse_state'] = True
self._attr_device_class = BinarySensorDeviceClass.DOOR
if miot_service.name in ['submersion_sensor']:
self._prop_state = miot_service.get_property('submersion_state') or self._prop_state
self._attr_device_class = BinarySensorDeviceClass.MOISTURE
async def async_added_to_hass(self):
await super().async_added_to_hass()
if prop := self.custom_config('state_property'):
self._prop_state = self._miot_service.get_property_by_full_name(prop) or self._prop_state
self._state_attrs.update({
'state_property': self._prop_state.full_name if self._prop_state else None,
})
rev = self.custom_config_bool('reverse_state', None)
if rev is not None:
self._vars['reverse_state'] = rev
@property
def is_on(self):
ret = self._state
if self._prop_state:
val = self._prop_state.from_device(self.device)
if val is None:
pass
elif self._prop_state.name in ['no_motion_duration', 'nobody_time']:
if self._prop_state.unit in ['minutes']:
val *= 60
dur = self.custom_config_integer('motion_timeout')
if dur is None and self._prop_state.value_range:
stp = self._prop_state.range_step()
if stp >= 10:
dur = self._prop_state.range_min() + stp
if dur is None:
dur = 60
ret = val <= dur
elif self._prop_state.value_list:
# linp.magnet.m1
if des := self._prop_state.list_description(val):
des = f'{des}'.lower()
return des in ['open', 'opened']
else:
ret = val
if self._vars.get('reverse_state'):
ret = not ret
return ret
@property
def device_class(self):
return self.get_device_class(BinarySensorDeviceClass)
class BleBinarySensorEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
self._prop_illumination = miot_service.get_property('illumination')
if not self._prop_illumination:
if srv := miot_service.spec.get_service('illumination_sensor'):
self._prop_illumination = srv.get_property('illumination')
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition
self._props = [
'event.15', # 0x000F motion&illumination
'prop.4103', # 0x1007 lux
'prop.4106', # 0x100A battery
'prop.4117', # 0x1015 smoke
'prop.4119', # 0x1017 no_motion_duration
'prop.4123', # 0x101B no_motion_timeout
'prop.4120', # 0x1018 illumination_level
'prop.4121', # 0x1019 magnet
]
async def async_update_for_main_entity(self):
if self.custom_config_bool('use_ble_object', True):
await self.async_update_ble_data()
await super().async_update_for_main_entity()
async def async_update_ble_data(self):
did = self.miot_did
mic = self.miot_cloud
if not did or not isinstance(mic, MiotCloud):
return
pms = {
'did': did,
'props': self._props,
}
rdt = await mic.async_request_api('device/batchdevicedatas', [pms]) or {}
self.logger.debug('%s: Got miio cloud props: %s', self.name_model, rdt)
props = (rdt.get('result') or {}).get(did, {})
sta = None
adt = {}
for k, v in props.items():
if v is None:
continue
ise = 'event.' in k
evt = json.loads(v) if ise else {'value': [v]}
tim = float(evt.get('timestamp') or 0)
val = vlk = None
if vls := evt.get('value'):
val = vls[0]
if val:
try:
val = int.from_bytes(bytes.fromhex(val), 'little')
except (TypeError, ValueError):
val = None
self.logger.warning('%s: BLE object data invalid: %s (%s)', self.name_model, k, vls)
if ise and not tim:
continue
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E6%9C%89%E4%BA%BA%E7%A7%BB%E5%8A%A8%E4%BA%8B%E4%BB%B6%EF%BC%88%E5%B8%A6%E5%85%89%E7%85%A7%EF%BC%89
if k == 'event.15':
adt.update({
'trigger_time': tim,
'trigger_at': datetime.fromtimestamp(tim, local_zone()),
})
dif = time.time() - adt['trigger_time']
sta = dif <= (self.custom_config_integer('motion_timeout') or 60)
vlk = 'illumination'
if self._prop_illumination and self._prop_illumination.value_range:
vlk = self._prop_illumination.full_name
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E5%85%89%E7%85%A7%E5%BA%A6%E5%B1%9E%E6%80%A7
elif k == 'prop.4103':
vlk = 'illumination'
if self._prop_illumination and self._prop_illumination.value_range:
vlk = self._prop_illumination.full_name
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E6%97%A0%E4%BA%BA%E7%A7%BB%E5%8A%A8%E5%B1%9E%E6%80%A7
elif k == 'prop.4119':
vlk = 'no_motion_seconds'
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E5%85%89%E7%85%A7%E5%BC%BA%E5%BC%B1%E5%B1%9E%E6%80%A7
elif k == 'prop.4120':
adt['light_strong'] = not not val
vlk = 'illumination_level'
val = 'strong' if val else 'weak'
if self._prop_illumination and self._prop_illumination.value_list:
vid = self._prop_illumination.list_value(val)
if vid is not None:
adt[self._prop_illumination.full_name] = vid
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E9%97%A8%E7%A3%81%E5%B1%9E%E6%80%A7
elif k == 'prop.4121':
sta = val != 2
if vlk is not None and val is not None:
adt[vlk] = val
if sta is not None:
self._state = sta
if adt:
await self.async_update_attrs(adt)
class MiotToiletEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
self._prop_state = None
for s in miot_service.spec.get_services('toilet', 'seat'):
if p := s.get_property('seating_state'):
self._prop_state = p
break
if not self._prop_state:
self._prop_state = miot_service.get_property(
self._prop_state.name if self._prop_state else 'status',
)
@property
def icon(self):
return 'mdi:toilet'
class LumiBinarySensorEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
async def async_update(self):
await super().async_update()
if not self._available:
return
mic = self.miot_cloud
pes = dlg = None
if isinstance(mic, MiotCloud):
now = int(time.time())
ofs = self.custom_config_integer('time_start_offset') or -86400 * 3
dlg = await mic.async_get_last_device_data(self.miot_did, 'device_log', time_start=now + ofs)
pes = json.loads(dlg or '[]')
adt = {}
typ = None
dif = time.time()
if pes and len(pes) >= 2:
typ = pes[1][0]
adt['trigger_type'] = typ
adt['trigger_time'] = int(pes[0] or 0)
adt['trigger_at'] = f'{datetime.fromtimestamp(adt["trigger_time"])}'
dif = time.time() - adt['trigger_time']
if typ == 'prop.illumination':
prop = self._miot_service.get_property('illumination')
if prop:
adt[prop.full_name] = pes[1][1][0]
self._state = None
if typ == 'event.motion' or self._miot_service.name in ['motion_sensor']:
self._state = dif <= (self.custom_config_integer('motion_timeout') or 60)
elif typ in ['event.open', 'event.close']:
self._state = typ == 'event.open'
elif typ in ['event.leak', 'event.no_leak']:
self._state = typ == 'event.leak'
elif self._prop_state and self._prop_state.full_name in self._state_attrs:
_LOGGER.info('%s: Get miio data failed: %s', self.name_model, dlg)
else:
_LOGGER.warning('%s: Get miio data failed: %s', self.name_model, dlg)
if self._prop_state and self._state is not None:
adt[self._prop_state.full_name] = self._state
if adt:
await self.async_update_attrs(adt)
@@ -0,0 +1,89 @@
"""Support button entity for Xiaomi Miot."""
import logging
from homeassistant.components.button import (
DOMAIN as ENTITY_DOMAIN,
ButtonEntity as BaseEntity,
)
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
BaseSubEntity,
async_setup_config_entry,
)
from .core.templates import template
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
class ButtonEntity(XEntity, BaseEntity):
def on_init(self):
self._attr_available = True
if des := getattr(self.conv, 'description', None):
self._attr_name = f'{self._attr_name} {des}'
def set_state(self, data: dict):
pass
async def async_press(self):
pms = getattr(self.conv, 'value', None)
if self._miot_action and self._miot_action.ins:
pms = self.custom_config_list('action_params', pms)
if pms:
vars = {
'attrs': self.device.props,
}
pms = [
v if not isinstance(v, str) else template(v, self.hass).async_render(vars)
for v in pms
]
await self.device.async_write({self.attr: pms})
XEntity.CLS[ENTITY_DOMAIN] = ButtonEntity
class ButtonSubEntity(BaseEntity, BaseSubEntity):
def __init__(self, parent, attr, option=None):
BaseSubEntity.__init__(self, parent, attr, option)
self._available = True
self._async_action = self._option.get('async_press_action')
self._press_action = self._option.get('press_action')
self._press_kwargs = {
'attr': self._attr,
**(self._option.get('press_kwargs') or {}),
}
self._state_attrs = self._option.get('state_attrs') or {}
def update(self, data=None):
return
def press(self):
"""Press the button."""
if not self._press_action:
raise NotImplementedError()
if ret := self._press_action(**self._press_kwargs):
self.schedule_update_ha_state()
return ret
async def async_press(self):
if self._async_action:
if ret := await self._async_action(**self._press_kwargs):
self.schedule_update_ha_state()
return ret
await super().async_press()
@@ -0,0 +1,705 @@
"""Support for Xiaomi cameras."""
import logging
import json
import time
import locale
import base64
import requests
import re
import collections
from os import urandom
from functools import partial
from urllib.parse import urlencode
from datetime import datetime, timedelta
from homeassistant.const import STATE_IDLE
from homeassistant.core import HomeAssistant
from homeassistant.components.camera import (
DOMAIN as ENTITY_DOMAIN,
Camera,
CameraEntityFeature, # v2022.5
)
from homeassistant.components.ffmpeg import async_get_image, DATA_FFMPEG
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from haffmpeg.camera import CameraMjpeg
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
Device,
HassEntry,
XEntity,
MiotToggleEntity,
BaseSubEntity,
MiotCloud,
MiCloudException,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.const import CameraState
from .core.miot_spec import (
MiotSpec,
MiotService,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
svs = spec.get_services(ENTITY_DOMAIN, 'camera_control', 'video_doorbell')
if svs:
svs = [] # migrate to converter
elif spec.services:
srv = None
if spec.name in ['video_doorbell']:
# loock.cateye.v02
srv = spec.get_service('p2p_stream') or spec.first_service()
elif model in [
'lumi.lock.bmcn05',
'lumi.lock.mcn002',
'lumi.lock.wbmcn1',
'loock.lock.t1pro',
]:
srv = spec.first_service()
if isinstance(srv, MiotService):
svs = [srv]
for srv in svs:
entities.append(MiotCameraEntity(hass, config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class BaseCameraEntity(Camera):
_state_attrs: dict
_last_image = None
_last_url = None
_url_expiration = 0
_extra_arguments = None
device: Device = None
def __init__(self, hass: HomeAssistant):
super().__init__()
self._supported_features = CameraEntityFeature(0)
self.access_tokens = collections.deque(self.access_tokens, 12 * 2)
self._manager = hass.data.get(DATA_FFMPEG)
# http://ffmpeg.org/ffmpeg-all.html
self._ffmpeg_options = ''
self._segment_iv_hex = urandom(16).hex()
self._segment_iv_b64 = base64.b64encode(bytes.fromhex(self._segment_iv_hex)).decode()
if not hasattr(self, '_attr_extra_state_attributes'):
self._attr_extra_state_attributes = {}
@property
def brand(self):
return self.device_info.get('manufacturer')
@property
def is_doorbell(self):
if '.lock.' in self.model:
return True
service = getattr(self, '_miot_service', None)
if service and service.name in ['video_doorbell']:
return True
return False
async def image_source(self, **kwargs):
raise NotImplementedError()
async def async_camera_image(self, width=None, height=None):
url = await self.image_source()
if url:
if '-i ' not in str(url):
url = f'-i "{url}"'
self._last_image = await async_get_image(
self.hass,
f'{self._ffmpeg_options or ""} {url}'.strip(),
extra_cmd=self._extra_arguments,
width=width,
height=height,
)
return self._last_image
async def handle_async_mjpeg_stream(self, request):
if not self.is_on:
_LOGGER.debug('%s: camera is off. %s', self.name, self._state_attrs)
return
url = await self.stream_source()
if not url:
_LOGGER.debug('%s: stream source is empty. %s', self.name, self._state_attrs)
return
if '-i ' not in str(url):
url = f'-i "{url}"'
stream = CameraMjpeg(self._manager.binary)
await stream.open_camera(
f'{self._ffmpeg_options or ""} {url}'.strip(),
extra_cmd=self._extra_arguments,
)
try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self.hass,
request,
stream_reader,
self._manager.ffmpeg_stream_content_type,
timeout=60,
)
finally:
try:
await stream.close()
except BrokenPipeError:
_LOGGER.error('%s: Got BrokenPipeError when close stream: %s', self.name, url)
async def _async_log_stderr_stream(self, stderr_reader):
"""Log output from ffmpeg."""
while True:
line = await stderr_reader.readline()
if line == b'':
return
_LOGGER.info('%s: ffmpeg stderr: %s', self.name, line.rstrip())
async def get_alarm_playlist(self, begin, ended=None, limit=2):
cloud = self.device.cloud
if not cloud:
return None
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'miot/camera/app/v1/alarm/playlist/limit')
rqd = {
'did': self.device.did,
'region': str(cloud.default_server).upper(),
'language': locale.getlocale()[0],
'beginTime': begin,
'endTime': ended or int(time.time() * 1000 + 999),
'limit': limit,
}
rdt = await cloud.async_request_api(api, rqd, method='GET', crypt=True) or {}
rls = rdt.get('data', {}).get('playUnits') or []
adt = {}
if rls:
fst = rls[0] or {}
tim = fst.pop('createTime', 0) / 1000
adt = {
'motion_video_time': f'{datetime.fromtimestamp(tim)}',
'motion_video_type': ','.join(fst.get('tags') or []),
'motion_video_latest': fst,
}
else:
self.log.warning('Camera alarm playlist is empty. %s', rdt)
return adt
async def get_alarm_eventlist(self, begin, ended=None, doorbell=False, limit=2):
cloud = self.device.cloud
if not cloud:
return None
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'common/app/get/eventlist')
rqd = {
'did': self.device.did,
'model': self.model,
'doorBell': doorbell,
'eventType': 'Default',
'needMerge': True,
'sortType': 'DESC',
'region': str(cloud.default_server).upper(),
'language': locale.getlocale()[0],
'beginTime': begin,
'endTime': ended or int(time.time() * 1000 + 999),
'limit': limit,
}
rdt = await cloud.async_request_api(api, rqd, method='GET', crypt=True) or {}
rls = rdt.get('data', {}).get('thirdPartPlayUnits') or []
adt = {}
if rls:
fst = rls[0] or {}
tim = fst.pop('createTime', 0) / 1000
adt = {
'motion_video_time': f'{datetime.fromtimestamp(tim)}',
'motion_video_type': fst.get('eventType'),
'motion_video_latest': fst,
}
else:
self.log.info('Camera events is empty. %s', rdt)
return adt
def get_alarm_m3u8_url(self, fileId, isAlarm=False, videoCodec='H265'):
cloud = self.device.cloud
if not cloud or not fileId:
return None
pms = {
'did': str(self.device.did),
'model': self.device_info.get('model'),
'fileId': fileId,
'isAlarm': not not isAlarm,
'videoCodec': videoCodec,
}
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'common/app/m3u8')
pms = cloud.rc4_params('GET', api, {'data': cloud.json_encode(pms)})
pms['yetAnotherServiceToken'] = cloud.service_token
return f'{api}?{urlencode(pms)}'
def get_alarm_image_address(self, fileId, storeId, crypto=False):
cloud = self.device.cloud
if not (cloud and fileId and storeId):
return None
dat = {
'did': str(self.device.did),
'fileId': fileId,
'stoId': storeId,
'segmentIv': self._segment_iv_b64,
}
api = cloud.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/img')
pms = cloud.rc4_params('GET', api, {'data': cloud.json_encode(dat)})
pms['yetAnotherServiceToken'] = cloud.service_token
url = f'{api}?{urlencode(pms)}'
if crypto:
key = base64.b64decode(cloud.ssecurity).hex()
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
return url
class CameraEntity(XEntity, BaseCameraEntity):
_attr_should_poll = True
_attr_camera_image = None
_attr_stream_source = None
_last_motion_time = None
def on_init(self):
BaseCameraEntity.__init__(self, self.hass)
self._attr_brand = self.device_info.get('manufacturer')
self._attr_model = self.device_info.get('model')
async def async_added_to_hass(self):
await super().async_added_to_hass()
if self._attr_should_poll:
await self.async_update_ha_state(True)
def get_state(self) -> dict:
return {}
def set_state(self, data: dict):
if 'motion_video_latest' in self.device.props:
self._attr_available = True
self._attr_should_poll = False
self.update_motion_video(self.device.props)
async def image_source(self):
return self._attr_camera_image
async def stream_source(self):
return self._attr_stream_source
def update_motion_video(self, data: dict):
tim = data.get('motion_video_time')
if self._last_motion_time == tim:
return
self._last_motion_time = tim
self._attr_extra_state_attributes.update({
'motion_video_time': tim,
'motion_video_type': data.get('motion_video_type'),
})
if adt := data.get('motion_video_latest'):
if fid := adt.get('fileId'):
self._attr_camera_image = self.get_alarm_image_address(fid, adt.get('imgStoreId'), True)
self._attr_stream_source = self.get_alarm_m3u8_url(fid, adt.get('isAlarm'))
self._attr_extra_state_attributes.update({
'stream_address': self._attr_stream_source,
})
async def async_update(self):
adt = None
stm = int(time.time() - 86400 * 7) * 1000
if not self.device.cloud:
pass
elif self.custom_config_bool('miio_event_human_visit_details_template'):
await self.device.update_miio_cloud_records()
elif self.custom_config_bool('use_alarm_playlist'):
adt = await self.get_alarm_playlist(stm)
else:
adt = await self.get_alarm_eventlist(stm, None, self.is_doorbell)
if adt:
self.log.debug('Camera alarm data: %s', adt)
self._attr_available = True
self.update_motion_video(adt)
XEntity.CLS[ENTITY_DOMAIN] = CameraEntity
class MiotCameraEntity(MiotToggleEntity, BaseCameraEntity):
_srv_stream = None
_act_start_stream = None
_act_stop_stream = None
_prop_stream_address = None
_prop_expiration_time = None
_prop_motion_tracking = None
_stream_refresh_unsub = None
_motion_entity = None
_motion_enable = None
_use_motion_stream = False
_sub_motion_stream = False
def __init__(self, hass: HomeAssistant, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
BaseCameraEntity.__init__(self, hass)
if self._prop_power:
self._supported_features |= CameraEntityFeature.ON_OFF
if miot_service:
self._prop_motion_tracking = miot_service.bool_property('motion_detection', 'motion_tracking')
async def async_added_to_hass(self):
await super().async_added_to_hass()
sls = ['camera_stream_for_google_home', 'camera_stream_for_amazon_alexa']
if self.custom_config_bool('use_rtsp_stream'):
sls.reverse()
for s in sls:
if not self._miot_service:
break
srv = self._miot_service.spec.get_service(s)
if not srv:
continue
act = srv.get_action('start_hls_stream', 'start_rtsp_stream')
if act:
self._srv_stream = srv
self._act_start_stream = act
self._act_stop_stream = srv.get_action('stop_stream')
self._prop_stream_address = srv.get_property('stream_address')
self._prop_expiration_time = srv.get_property('expiration_time')
break
if self._prop_stream_address:
self._supported_features |= CameraEntityFeature.STREAM
self._sub_motion_stream = True
elif self._miot_service.name in ['camera_control'] or self.is_doorbell:
if self.custom_config_bool('use_motion_stream'):
pass
elif self.custom_config_bool('sub_motion_stream'):
pass
else:
self._use_motion_stream = True
@property
def should_poll(self):
return True
@property
def state(self): # noqa
if self.is_recording:
return CameraState.RECORDING
if self.is_streaming:
return CameraState.STREAMING
return STATE_IDLE
async def async_update(self):
self._state_attrs.pop('motion_video_latest', None) # remove
await super().async_update()
if not self._available:
return
self._motion_enable = self.custom_config_bool('use_motion_stream', self._use_motion_stream)
add_cameras = self._add_entities.get(ENTITY_DOMAIN)
if not self._motion_entity \
and add_cameras \
and self.custom_config_bool('sub_motion_stream', self._sub_motion_stream):
self._motion_entity = MotionCameraEntity(self, self.hass)
self._subs['motion_event'] = self._motion_entity
add_cameras([self._motion_entity], update_before_add=True)
adt = None
stm = int(time.time() - 86400 * 7) * 1000
if not self._motion_enable and not self._motion_entity:
pass
elif 'motion_video_latest' in self._state_attrs:
adt = {
'motion_video_updated': 1,
}
elif not self.xiaomi_cloud:
pass
elif self.custom_config_bool('use_alarm_playlist'):
adt = await self.get_alarm_playlist(stm)
else:
adt = await self.get_alarm_eventlist(stm, None, self.is_doorbell)
if adt:
self._supported_features |= CameraEntityFeature.STREAM
await self.async_update_attrs(adt)
if self._motion_enable:
await self.async_update_attrs(self.motion_event_attributes)
if self._motion_entity:
await self.hass.async_add_executor_job(self._motion_entity.update)
@property
def is_on(self):
if self._prop_power:
return self._prop_power.from_device(self.device) and True
return True
async def stream_source(self, **kwargs):
fun = self.async_get_stream_address()
if self._motion_enable:
kwargs['crypto'] = True
fun = self.hass.async_add_executor_job(partial(self.get_motion_stream_address, **kwargs))
idx = self.custom_config_integer('motion_stream_slice')
if idx is not None:
kwargs['index'] = idx
fun = self.hass.async_add_executor_job(partial(self.get_motion_stream_slice_video), **kwargs)
return await fun
async def image_source(self, **kwargs):
if self._motion_enable:
kwargs['crypto'] = True
return self.get_motion_image_address(**kwargs)
return await self.stream_source()
async def async_get_stream_address(self, **kwargs):
now = time.time()
if now >= self._url_expiration:
self._last_url = None
_LOGGER.debug('%s: camera stream: %s expired: %s', self.name_model, self._last_url, self._url_expiration)
result = {}
if not self._act_start_stream:
self.update_attrs({
'miot_error': 'Nonsupport start hls/rstp stream via miot-spec',
})
elif not self._last_url:
updater = 'lan'
try:
vav = self.custom_config_integer('video_attribute')
vap = self._srv_stream.get_property('video_attribute')
if vav is None and vap and vap.value_list:
vav = (vap.value_list.pop(0) or {}).get('value')
if self.xiaomi_cloud:
if self._act_stop_stream:
await self.async_call_action(self._act_stop_stream)
result = await self.async_call_action(self._act_start_stream, [] if vav is None else [vav]) or {}
updater = 'cloud'
if isinstance(result, dict):
_LOGGER.debug('%s: Get miot camera stream from %s: %s', self.name_model, updater, result)
else:
_LOGGER.warning('%s: Get miot camera stream error from %s: %s', self.name_model, updater, result)
result = {}
except MiCloudException as exc:
_LOGGER.error('%s: Get miot camera stream from %s failed: %s', self.name_model, updater, exc)
odt = self._act_start_stream.out_results(result.get('out')) or {
'stream_address': '',
}
self._url_expiration = 0
if self._prop_expiration_time:
self._url_expiration = int(self._prop_expiration_time.from_dict(odt) or 0) / 1000
if self._url_expiration:
self._url_expiration -= 10
else:
self._url_expiration = now + 60 * 4.5
if self._prop_stream_address:
self._last_url = self._prop_stream_address.from_dict(odt)
self.schedule_update_ha_state()
self.async_check_stream_address(self._last_url)
if not kwargs.get('scheduled') or self.custom_config('keep_streaming'):
self._schedule_stream_refresh()
odt['expire_at'] = f'{datetime.fromtimestamp(self._url_expiration)}'
self.update_attrs(odt)
self._attr_is_streaming = self._last_url and True
if self._attr_is_streaming:
self.update_attrs({
'miot_error': None,
})
return self._last_url
def async_check_stream_address(self, url):
if not url or self.custom_config_bool('disable_check_stream'):
return False
res = requests.head(url)
if res.status_code > 200:
self.update_attrs({
'stream_http_status': res.status_code,
'stream_http_reason': res.reason,
})
_LOGGER.warning(
'%s: stream address status invalid: %s (%s)',
self.name,
res.status_code,
res.reason,
)
return False
return True
async def _handle_stream_refresh(self, now, *_):
self._stream_refresh_unsub = None
await self.stream_source(scheduled=True)
def _schedule_stream_refresh(self):
if self._stream_refresh_unsub is not None:
self._stream_refresh_unsub()
self._stream_refresh_unsub = async_track_point_in_utc_time(
self.hass,
self._handle_stream_refresh, # noqa
datetime.fromtimestamp(self._url_expiration),
)
@property
def motion_event_attributes(self):
return {
'motion_video_time': self._state_attrs.get('motion_video_time'),
'motion_video_type': self._state_attrs.get('motion_video_type'),
'stream_address': self.get_motion_stream_address(),
'image_address': self.get_motion_image_address(),
}
def get_motion_stream_address(self, **kwargs):
mic = self.xiaomi_cloud
if not mic:
_LOGGER.info('%s: camera does not have cloud.', self.name)
return None
mvd = self._state_attrs.get('motion_video_latest') or {}
fid = mvd.get('fileId')
if not fid:
_LOGGER.info('%s: camera does not have motion file in cloud.', self.name)
return None
url = self.get_alarm_m3u8_url(fid, mvd.get('isAlarm'))
_LOGGER.debug('%s: Got stream url: %s', self.name_model, url)
return url
def get_motion_video_address(self, **kwargs):
mic = self.xiaomi_cloud
if not mic:
_LOGGER.info('%s: camera does not have cloud.', self.name)
return None
mvd = self._state_attrs.get('motion_video_latest') or {}
fid = mvd.get('fileId')
vid = mvd.get('videoStoreId')
if not fid or not vid:
_LOGGER.info('%s: camera does not have motion video in cloud.', self.name)
return None
dat = {
'did': str(self.miot_did),
'fileId': fid,
'stoId': vid,
'segmentIv': self._segment_iv_b64,
}
api = mic.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/mp4')
pms = mic.rc4_params('GET', api, {'data': mic.json_encode(dat)})
pms['yetAnotherServiceToken'] = mic.service_token
url = f'{api}?{urlencode(pms)}'
_LOGGER.debug('%s: Got video url: %s', self.name_model, url)
if kwargs.get('debug'):
req = requests.get(url)
if float(req.headers.get('x-xiaomi-status-code', 200)) >= 400:
try:
signed_nonce = mic.signed_nonce(pms['_nonce'])
rdt = json.loads(MiotCloud.decrypt_data(signed_nonce, req.text).decode())
_LOGGER.info('%s: video stream content: %s', self.name_model, rdt)
except (TypeError, ValueError):
pass
if kwargs.get('crypto'):
key = base64.b64decode(mic.ssecurity).hex()
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
return url
def get_motion_stream_slice_video(self, **kwargs):
url = self.get_motion_stream_address()
if not url:
_LOGGER.info('%s: camera does not have motion stream in cloud.', self.name)
return None
req = requests.get(url)
if float(req.headers.get('x-xiaomi-status-code', 200)) >= 400:
_LOGGER.warning('%s: camera motion stream with a failed http code: %s', self.name_model, req)
return url
aes_key = None
aes__iv = None
mat = re.search(r'AES-128,\s*URI="?(https?://[^",]+)"?,\s*IV=(?:0x)?(\w+)', req.text)
if mat:
aes_key, aes__iv = mat.groups()
mat = re.findall(r'[\r\n](https?://[^\r\n]+)', req.text)
idx = kwargs.get('index', -1)
mp4 = mat.pop(idx) if mat else None
if mp4 and aes_key:
req = requests.get(aes_key)
key = req.content.hex()
mp4 = f'-decryption_key {key} -decryption_iv {aes__iv} -i "crypto+{mp4}"'
_LOGGER.debug('%s: Got video url: %s', self.name_model, mp4)
return mp4
def get_motion_image_address(self, **kwargs):
mic = self.xiaomi_cloud
if not mic:
_LOGGER.info('%s: camera does not have cloud.', self.name)
return None
mvd = self._state_attrs.get('motion_video_latest') or {}
fid = mvd.get('fileId')
iid = mvd.get('imgStoreId')
if not fid or not iid:
_LOGGER.info('%s: camera does not have motion image in cloud.', self.name)
return None
dat = {
'did': str(self.miot_did),
'fileId': fid,
'stoId': iid,
'segmentIv': self._segment_iv_b64,
}
api = mic.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/img')
pms = mic.rc4_params('GET', api, {'data': mic.json_encode(dat)})
pms['yetAnotherServiceToken'] = mic.service_token
url = f'{api}?{urlencode(pms)}'
_LOGGER.debug('%s: Got image url: %s', self.name_model, url)
if kwargs.get('crypto'):
key = base64.b64decode(mic.ssecurity).hex()
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
return url
@property
def motion_detection_enabled(self):
if self._prop_motion_tracking:
return self._prop_motion_tracking.from_device(self.device)
return None
async def async_enable_motion_detection(self):
if self._prop_motion_tracking:
return await self.async_set_property(self._prop_motion_tracking, True)
return False
async def async_disable_motion_detection(self):
if self._prop_motion_tracking:
return await self.async_set_property(self._prop_motion_tracking, False)
return False
class MotionCameraEntity(BaseSubEntity, BaseCameraEntity):
def __init__(self, parent, hass: HomeAssistant, option=None):
super().__init__(parent, 'motion_event', option, domain=ENTITY_DOMAIN)
BaseCameraEntity.__init__(self, hass)
self._available = True
self._supported_features |= CameraEntityFeature.STREAM
def update(self, data=None):
super().update(data)
self._available = not not self.parent_attributes.get('motion_video_latest')
if not self._available:
return
self.update_attrs(self._parent.motion_event_attributes, update_parent=False)
async def stream_source(self, **kwargs):
kwargs['crypto'] = True
return await self.hass.async_add_executor_job(
partial(self._parent.get_motion_stream_address, **kwargs)
)
async def image_source(self, **kwargs):
kwargs['crypto'] = True
return self._parent.get_motion_image_address(**kwargs)
@@ -0,0 +1,533 @@
"""Support for Xiaomi Aircondition."""
import logging
from enum import Enum
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_MODE,
ATTR_FAN_MODE,
HVACAction,
HVACMode,
)
from homeassistant.components.climate import (
DOMAIN as ENTITY_DOMAIN,
ClimateEntity as BaseEntity,
ClimateEntityFeature,
SWING_ON,
SWING_OFF,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
MiotProperty,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
DEFAULT_MIN_TEMP = 16.0
DEFAULT_MAX_TEMP = 31.0
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services('ir_aircondition_control'):
if srv.name in ['ir_aircondition_control']:
entities.append(MiirClimateEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class SwingModes(Enum):
off = 0
vertical = 1
horizontal = 2
both = 3
class BaseClimateEntity(BaseEntity):
_hvac_modes = None
_attr_is_on = None
_attr_device_class = None
_attr_fan_mode = None
_attr_fan_modes = None
_attr_hvac_mode = None
_attr_preset_mode = None
_attr_swing_mode = None
_attr_swing_modes = None
_attr_swing_horizontal_mode = None
_attr_swing_horizontal_modes = None
_attr_max_temp = None
_attr_min_temp = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def on_init(self):
self._attr_hvac_modes = []
self._hvac_modes = {
HVACMode.OFF: {'list': ['Off', 'Idle', 'None'], 'action': HVACAction.OFF},
HVACMode.AUTO: {'list': ['Auto', 'Manual', 'Normal'], 'action': HVACAction.IDLE},
HVACMode.COOL: {'list': ['Cool'], 'action': HVACAction.COOLING},
HVACMode.HEAT: {'list': ['Heat'], 'action': HVACAction.HEATING},
HVACMode.DRY: {'list': ['Dry'], 'action': HVACAction.DRYING},
HVACMode.FAN_ONLY: {'list': ['Fan'], 'action': HVACAction.FAN},
}
def prop_temperature_unit(self, prop: MiotProperty):
if prop:
if prop.unit in ['celsius', UnitOfTemperature.CELSIUS]:
return UnitOfTemperature.CELSIUS
if prop.unit in ['fahrenheit', UnitOfTemperature.FAHRENHEIT]:
return UnitOfTemperature.FAHRENHEIT
if prop.unit in ['kelvin', UnitOfTemperature.KELVIN]:
return UnitOfTemperature.KELVIN
return UnitOfTemperature.CELSIUS
def update_bind_sensor(self):
bss = self.custom_config_list('bind_sensor') or []
ext = {}
for bse in bss:
bse = f'{bse}'.strip()
if not bse:
continue
sta = self.hass.states.get(bse)
if not sta or not sta.state or sta.state == STATE_UNKNOWN:
continue
try:
num = float(sta.state)
except ValueError:
num = None
self.log.info('Got bound state from %s: %s, state invalid', bse, sta.state)
if num is not None:
cls = sta.attributes.get(ATTR_DEVICE_CLASS)
unit = sta.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if cls == SensorDeviceClass.TEMPERATURE.value or unit in [
UnitOfTemperature.CELSIUS,
UnitOfTemperature.KELVIN,
UnitOfTemperature.FAHRENHEIT,
]:
ext[ATTR_CURRENT_TEMPERATURE] = self.hass.config.units.temperature(num, unit)
elif cls == SensorDeviceClass.HUMIDITY.value:
ext[ATTR_CURRENT_HUMIDITY] = num
if ext:
self.update_attrs(ext)
self.log.debug('Got bound state from %s: %s', bss, ext)
class ClimateEntity(XEntity, BaseClimateEntity):
_conv_power = None
_conv_mode = None
_conv_speed = None
_conv_swing = None
_conv_swing_h = None
_conv_target_temp = None
_conv_current_temp = None
_conv_target_humidity = None
_conv_current_humidity = None
_prop_temperature_name = None
def on_init(self):
BaseClimateEntity.on_init(self)
self._prop_temperature_name = self.custom_config('current_temp_property') or 'indoor_temperature'
hvac_modes = set()
for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['on']):
self._conv_power = conv
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
hvac_modes.add(HVACMode.OFF)
hvac_modes.add(HVACMode.AUTO)
elif prop.in_list(['mode']):
self._conv_mode = conv
self._attr_preset_modes = prop.list_descriptions()
for mk, mv in self._hvac_modes.items():
val = prop.list_first(*(mv.get('list') or []))
if val is not None:
des = prop.list_description(val)
hvac_modes.add(mk)
self._hvac_modes[mk]['value'] = val
self._hvac_modes[mk]['description'] = des
if mk != HVACMode.OFF:
self._attr_preset_modes.remove(des)
if self._attr_preset_modes:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
elif prop.in_list(['fan_level', 'speed_level', 'heat_level']):
self._conv_speed = conv
self._attr_fan_modes = prop.list_descriptions(lower=True)
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
elif prop.in_list(['vertical_swing']):
self._conv_swing = conv
self._attr_swing_modes = [SWING_ON, SWING_OFF]
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
elif prop.in_list(['horizontal_swing']):
if hasattr(ClimateEntityFeature, 'SWING_HORIZONTAL_MODE'): # v2024.12
self._conv_swing_h = conv
self._attr_swing_horizontal_modes = [SWING_ON, SWING_OFF]
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
elif prop.in_list(['target_temperature']):
self._conv_target_temp = conv
self._attr_min_temp = prop.range_min()
self._attr_max_temp = prop.range_max()
self._attr_target_temperature_step = prop.range_step()
self._attr_temperature_unit = self.prop_temperature_unit(prop)
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
elif prop.in_list([self._prop_temperature_name, 'temperature']):
if prop.in_list([self._prop_temperature_name]):
self._conv_current_temp = conv
elif not self._conv_current_temp:
self._conv_current_temp = conv
else:
continue
self._attr_temperature_unit = self.prop_temperature_unit(prop)
elif prop.in_list(['relative_humidity', 'humidity']):
self._conv_current_humidity = conv
elif prop.in_list(['target_humidity']):
self._conv_target_humidity = conv
if prop.value_range:
self._attr_min_humidity = prop.range_min()
self._attr_max_humidity = prop.range_max()
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
self._attr_hvac_modes = list(hvac_modes)
def set_state(self, data: dict):
if self._conv_mode:
val = self._conv_mode.value_from_dict(data)
if val in self._attr_preset_modes:
self._attr_preset_mode = val
if val is not None:
for mk, mv in self._hvac_modes.items():
if val == mv.get('description'):
self._attr_hvac_mode = mk
self._attr_hvac_action = mv.get('action')
self._attr_preset_mode = None
break
if self._conv_power:
val = self._conv_power.value_from_dict(data)
if val is not None:
self._attr_is_on = val
if val in [False, 0]:
self._attr_hvac_mode = HVACMode.OFF
self._attr_hvac_action = HVACAction.OFF
self._attr_preset_mode = None
elif val and self._attr_hvac_mode in [None, HVACMode.OFF]:
self._attr_hvac_mode = HVACMode.AUTO
if val and self._miot_service.name in ['heater'] and self._attr_hvac_action is None:
self._attr_hvac_action = HVACAction.HEATING
self._attr_state = self._attr_hvac_mode
if self._conv_speed:
val = self._conv_speed.value_from_dict(data)
if val is not None:
self._attr_fan_mode = val
if self._conv_swing:
val = self._conv_swing.value_from_dict(data)
if val is not None:
self._attr_swing_mode = SWING_ON if val else SWING_OFF
if self._conv_swing_h:
val = self._conv_swing_h.value_from_dict(data)
if val is not None:
self._attr_swing_horizontal_mode = SWING_ON if val else SWING_OFF
self.update_bind_sensor()
if self._conv_target_temp:
val = self._conv_target_temp.value_from_dict(data)
if val is not None:
self._attr_target_temperature = val
if self._conv_current_temp:
val = self._conv_current_temp.value_from_dict(data)
if val is not None:
self._attr_current_temperature = val
if self._conv_target_humidity:
val = self._conv_target_humidity.value_from_dict(data)
if val is not None:
self._attr_target_humidity = val
if self._conv_current_humidity:
val = self._conv_current_humidity.value_from_dict(data)
if val is not None:
self._attr_current_humidity = val
def update_attrs(self, attrs):
temp = attrs.get(ATTR_CURRENT_TEMPERATURE)
if temp is not None:
self._attr_current_temperature = temp
humi = attrs.get(ATTR_CURRENT_HUMIDITY)
if humi is not None:
self._attr_current_humidity = humi
async def async_turn_on(self):
if self._conv_power:
await self.async_turn_switch(True)
return
await super().async_turn_on()
async def async_turn_off(self):
if self._conv_power:
await self.async_turn_switch(False)
return
await super().async_turn_off()
async def async_turn_switch(self, state):
if self._conv_power:
await self.device.async_write({self._conv_power.full_name: state})
async def async_set_hvac_mode(self, hvac_mode: HVACMode):
await self.async_set_temperature(**{ATTR_HVAC_MODE: hvac_mode})
async def async_set_preset_mode(self, preset_mode: str):
if self._conv_mode:
await self.device.async_write({self._conv_mode.full_name: preset_mode})
async def async_set_temperature(self, **kwargs):
dat = {}
hvac = kwargs.get(ATTR_HVAC_MODE)
if hvac == HVACMode.OFF and self._conv_power:
await self.async_turn_switch(False)
return
if hvac and self._conv_power and self._attr_is_on is False:
dat[self._conv_power.full_name] = True
if hvac and hvac != self._attr_hvac_mode and self._conv_mode:
mode = self._hvac_modes.get(hvac)
if not mode:
self.log.warning('Unsupported hvac mode: %s', hvac)
elif (desc := mode.get('description')) is not None:
dat[self._conv_mode.full_name] = desc
if self._conv_target_temp and self._attr_target_temperature and hvac in [HVACMode.HEAT, HVACMode.COOL]:
dat[self._conv_target_temp.full_name] = self._attr_target_temperature
temp = kwargs.get(ATTR_TEMPERATURE)
if temp and self._conv_target_temp:
prop = self._conv_target_temp.prop
if not isinstance(prop, MiotProperty):
pass
elif prop.is_integer or prop.range_step() == 1:
temp = int(temp)
dat[self._conv_target_temp.full_name] = temp
fan_mode = kwargs.get(ATTR_FAN_MODE)
if fan_mode and self._conv_speed:
dat[self._conv_speed.full_name] = fan_mode
await self.device.async_write(dat)
async def async_set_humidity(self, humidity: int):
if not self._conv_target_humidity:
return
await self.device.async_write({self._conv_target_humidity.full_name: humidity})
async def async_set_fan_mode(self, fan_mode: str):
if not self._conv_speed:
return
dat = {
ATTR_FAN_MODE: fan_mode,
}
if self._attr_is_on is False and HVACMode.FAN_ONLY in self.hvac_modes:
dat[ATTR_HVAC_MODE] = HVACMode.FAN_ONLY
await self.async_set_temperature(**dat)
async def async_set_swing_mode(self, swing_mode: str):
if not self._conv_swing:
return
await self.device.async_write({self._conv_swing.full_name: swing_mode == SWING_ON})
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str):
if not self._conv_swing_h:
return
await self.device.async_write({self._conv_swing_h.full_name: swing_horizontal_mode == SWING_ON})
XEntity.CLS[ENTITY_DOMAIN] = ClimateEntity
class MiirClimateEntity(MiotEntity, BaseClimateEntity, RestoreEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
self._available = True
self._act_turn_on = miot_service.get_action('turn_on')
self._act_turn_off = miot_service.get_action('turn_off')
# Add turn on/off features if actions are available
if self._act_turn_on:
if hasattr(ClimateEntityFeature, 'TURN_ON'): # v2024.2+
self._supported_features |= ClimateEntityFeature.TURN_ON
if self._act_turn_off:
if hasattr(ClimateEntityFeature, 'TURN_OFF'): # v2024.2+
self._supported_features |= ClimateEntityFeature.TURN_OFF
self._attr_hvac_mode = None
self._hvac_modes = {
HVACMode.OFF: {'list': ['Off', 'Idle', 'None'], 'action': HVACAction.OFF},
HVACMode.AUTO: {'list': ['Auto', 'Manual', 'Normal']},
HVACMode.COOL: {'list': ['Cool'], 'action': HVACAction.COOLING},
HVACMode.HEAT: {'list': ['Heat'], 'action': HVACAction.HEATING},
HVACMode.DRY: {'list': ['Dry'], 'action': HVACAction.DRYING},
HVACMode.FAN_ONLY: {'list': ['Fan'], 'action': HVACAction.FAN},
}
self._attr_hvac_modes = []
self._prop_mode = miot_service.get_property('ir_mode')
if self._prop_mode:
for mk, mv in self._hvac_modes.items():
val = self._prop_mode.list_first(*(mv.get('list') or []))
if val is not None:
self._hvac_modes[mk]['value'] = val
self._attr_hvac_modes.append(mk)
elif mk == HVACMode.OFF:
self._attr_hvac_modes.append(mk)
self._attr_current_temperature = None
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_max_temp = DEFAULT_MAX_TEMP
self._attr_min_temp = DEFAULT_MIN_TEMP
self._attr_target_temperature_step = 1
self._prop_temperature = miot_service.get_property('ir_temperature')
if self._prop_temperature:
self._supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
self._attr_temperature_unit = self._prop_temperature.unit_of_measurement
self._attr_target_temperature_step = self._prop_temperature.range_step() or 1
self._attr_max_temp = self._prop_temperature.range_max() or DEFAULT_MAX_TEMP
self._attr_min_temp = self._prop_temperature.range_min() or DEFAULT_MIN_TEMP
self._attr_target_temperature_high = self._attr_max_temp
self._attr_target_temperature_low = self._attr_min_temp
self._attr_target_temperature = int((self._attr_max_temp - self._attr_min_temp) / 1.2) + self._attr_min_temp
self._fan_modes = {}
self._attr_fan_modes = []
self._attr_fan_mode = None
self._act_fan_down = miot_service.get_action('fan_speed_down')
if self._act_fan_down:
self._fan_modes[self._act_fan_down.friendly_desc] = self._act_fan_down
self._act_fan_up = miot_service.get_action('fan_speed_up')
if self._act_fan_up:
self._fan_modes[self._act_fan_up.friendly_desc] = self._act_fan_up
if self._fan_modes:
self._supported_features |= ClimateEntityFeature.FAN_MODE
self._attr_fan_modes = list(self._fan_modes.keys())
async def async_added_to_hass(self):
await super().async_added_to_hass()
if hasattr(self, 'async_get_last_state'):
if state := await self.async_get_last_state():
self._attr_hvac_mode = state.state
if self._attr_hvac_mode not in self._hvac_modes:
self._attr_hvac_mode = None
async def async_update(self):
self.update_bind_sensor()
if ATTR_CURRENT_TEMPERATURE in self._state_attrs:
self._attr_current_temperature = self._state_attrs.get(ATTR_CURRENT_TEMPERATURE)
@property
def is_on(self):
return self.hvac_mode not in [None, HVACMode.OFF]
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
if not self._act_turn_on:
raise NotImplementedError()
if ret := await self.async_call_action(self._act_turn_on):
self._attr_hvac_mode = HVACMode.AUTO
return ret
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
if not self._act_turn_off:
raise NotImplementedError()
if ret := await self.async_call_action(self._act_turn_off):
self._attr_hvac_mode = HVACMode.OFF
return ret
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
if hvac_mode == HVACMode.OFF:
return await self.async_turn_off()
if not self._prop_mode:
raise NotImplementedError()
val = self._hvac_modes.get(hvac_mode, {}).get('value')
if val is None:
return False
if ret := await self.async_set_property(self._prop_mode, val):
self._attr_hvac_mode = hvac_mode
return ret
@property
def hvac_action(self):
"""Return the current running hvac operation if supported.
Need to be one of HVACAction.*.
"""
if not self.is_on:
return HVACAction.OFF
hvac = self.hvac_mode
if hvac is None:
return HVACAction.IDLE
return self._hvac_modes.get(hvac, {}).get('action')
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_HVAC_MODE in kwargs:
try:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
except (Exception, NotImplementedError):
pass
if not self._prop_temperature:
raise NotImplementedError()
ret = False
if ATTR_TEMPERATURE in kwargs:
val = kwargs.get(ATTR_TEMPERATURE)
if ret := await self.async_set_property(self._prop_temperature, val):
self._attr_target_temperature = val
return ret
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
if not self._attr_fan_modes:
raise NotImplementedError()
ret = False
if act := self._fan_modes.get(fan_mode):
ret = await self.async_call_action(act)
return ret
@@ -0,0 +1,911 @@
"""Config flow to configure Xiaomi Miot."""
import logging
import re
import copy
import requests
import voluptuous as vol
from typing import Optional
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
)
from homeassistant.core import callback, split_entity_id
from homeassistant.util import yaml
from homeassistant.components import persistent_notification
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import ObjectSelector
from . import (
DOMAIN,
CONF_MODEL,
CONF_CONN_MODE,
CONF_SERVER_COUNTRY,
CONF_CONFIG_VERSION,
DEFAULT_NAME,
DEFAULT_CONN_MODE,
init_integration_data,
)
from .core.utils import (
DeviceException,
get_customize_via_entity,
get_customize_via_model,
in_china,
async_analytics_track_event,
)
from .core.const import SUPPORTED_DOMAINS, CLOUD_SERVERS, CONF_XIAOMI_CLOUD, HA_VERSION
from .core.device import MiioInfo
from .core.miot_spec import MiotSpec
from .core.mini_miio import AsyncMiIO
from .core.xiaomi_cloud import (
MiotCloud,
MiCloudException,
MiCloudAccessDenied,
MiCloudNeedVerify,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_INTERVAL = 30
# 0.1 support multiple integration to add the same device
# 0.2 new entity id format (model_mac[-4:]_suffix)
# 0.3 washer modes via select
ENTRY_VERSION = 0.3
CONN_MODES = {
'auto': 'Automatic (自动模式)',
'local': 'Local (本地模式)',
'cloud': 'Cloud (云端模式)',
}
async def check_miio_device(hass, user_input, errors):
host = user_input.get(CONF_HOST)
token = user_input.get(CONF_TOKEN)
try:
device = AsyncMiIO(host, token)
info = await device.info()
if info:
info = MiioInfo(info)
except DeviceException:
device = None
info = None
errors['base'] = 'cannot_connect'
_LOGGER.debug('Xiaomi Miot config flow: %s', {
'user_input': user_input,
'miio_info': info,
'errors': errors,
})
model = ''
if info is not None:
if not user_input.get(CONF_MODEL):
model = str(info.model or '')
user_input[CONF_MODEL] = model
user_input['miio_info'] = dict(info or {})
miot_type = await MiotSpec.async_get_model_type(hass, model)
if not miot_type:
miot_type = await MiotSpec.async_get_model_type(hass, model, use_remote=True)
user_input['miot_type'] = miot_type
user_input['unique_did'] = format_mac(info.mac_address)
if miot_type and device:
try:
pms = [
{'did': 'miot', 'siid': 2, 'piid': 1},
{'did': 'miot', 'siid': 2, 'piid': 2},
{'did': 'miot', 'siid': 3, 'piid': 1},
]
results = await device.send('get_properties', pms) or []
for prop in results:
if not isinstance(prop, dict):
continue
if prop.get('code') == 0:
# Collect supported models in LAN
await async_analytics_track_event(
hass, 'miot', 'local', model,
firmware=info.firmware_version,
results=results,
miot_urn=miot_type,
ap_rssi=info.get('ap', {}).get('rssi', ''),
wifi_fw_ver=info.get('wifi_fw_ver', ''),
)
break
except DeviceException:
pass
return user_input
class BaseFlowHandler:
hass = None
context = None
config_data = None
cloud: Optional[MiotCloud] = None
devices: Optional[list] = None
@property
def placeholders(self):
return self.context.setdefault('placeholders', {})
def pop_placeholders(self):
return {
'tip': '',
**self.context.pop('placeholders', {}),
}
async def get_cloud(self, user_input):
if not self.cloud:
self.cloud = await MiotCloud.from_token(self.hass, user_input, login=False)
self.cloud.login_times = 0
self.cloud.merger_config(user_input)
login_data = {}
if verify_ticket := user_input.pop('verify_ticket', None):
login_data['verify_ticket'] = verify_ticket
if captcha := user_input.pop('captcha', None):
login_data['captcha'] = captcha
if login_data:
await self.cloud.async_login(login_data=login_data)
elif not await self.cloud.async_check_auth(notify=False):
raise MiCloudException('Login failed')
return self.cloud
async def check_xiaomi_account(self, user_input, errors, renew_devices=False):
dvs = []
mic = None
try:
mic = await self.get_cloud(user_input)
dvs = await mic.async_get_devices(renew=renew_devices) or []
if renew_devices:
await MiotSpec.async_get_model_type(self.hass, 'xiaomi.miot.auto', use_remote=True)
self.context.pop('captchaIck', None)
except (MiCloudException, MiCloudAccessDenied, Exception) as exc:
err = f'{exc}'
self.placeholders['tip'] = f'⚠️ {err}'
errors['base'] = 'cannot_login'
if not mic:
mic = self.cloud
if isinstance(exc, MiCloudNeedVerify) and mic:
errors['base'] = exc.message
self.context[exc.message] = True
self.placeholders.update({
'url': exc.url,
'tip': f'[打开验证网页 | Open the verification page]({exc.url})',
})
elif isinstance(exc, MiCloudAccessDenied) and mic:
if url := mic.attrs.pop('captchaImg', None):
err = f'Captcha:\n![captcha](data:image/jpeg;base64,{url})'
self.placeholders['tip'] = f'⚠️ {err}'
self.context['captchaIck'] = mic.attrs.get('captchaIck')
elif isinstance(exc, requests.exceptions.ConnectionError):
errors['base'] = 'cannot_reach'
elif 'ZoneInfoNotFoundError' in err:
errors['base'] = 'tzinfo_error'
unm = mic.username if mic else user_input.get(CONF_USERNAME)
_LOGGER.error('Setup xiaomi cloud for user: %s failed.', unm, exc_info=True)
if not errors:
self.devices = dvs
persistent_notification.dismiss(self.hass, f'{DOMAIN}-login')
return user_input
async def get_cloud_filter_schema(self, user_input, errors, schema=None, via_did=False, home_ids=None):
if not schema:
schema = vol.Schema({})
dvs = self.devices or []
if not dvs:
errors['base'] = 'none_devices'
else:
grp = {}
vls = {}
homes = {}
fls = ['did'] if via_did else ['model', 'home_id', 'ssid', 'bssid']
for f in fls:
for d in dvs:
v = d.get(f)
if v is None:
continue
if home_id := d.get('home_id', 0):
homes.setdefault(home_id, d.get('home_name') or 'Default Home')
if f in ['did'] and v in user_input.get(f'{f}_list', []):
pass
elif home_ids and home_id not in home_ids:
continue
grp.setdefault(v, 0)
grp[v] += 1
vls.setdefault(f, {})
dnm = f'{d.get("name")}'
des = '<empty>' if v == '' else v
if f == 'home_id':
des = d.get('home_name') or des
if f in ['did']:
if MiotCloud.is_hide(d):
continue
dip = d.get('localip')
if not dip or d.get('pid') not in [0, '0', '8', '', None]:
dip = d.get('model')
vls[f][v] = f'{dnm} ({dip})'
elif f in ['model']:
if grp[v] > 1:
dnm += f' * {grp[v]}'
vls[f][v] = f'{des} ({dnm})'
else:
vls[f][v] = f'{des} ({grp[v]})'
ies = {
'exclude': 'Exclude (排除)',
'include': 'Include (包含)',
}
for f in fls:
if not vls.get(f):
continue
fk = f'filter_{f}'
fl = f'{f}_list'
lst = vls.get(f, {})
lst = dict(sorted(lst.items()))
ols = [
v
for v in user_input.get(fl, [])
if v in lst
]
schema = schema.extend({
vol.Required(fk, default=user_input.get(fk, 'exclude')): vol.In(ies),
vol.Optional(fl, default=ols): cv.multi_select(lst),
})
if via_did and homes:
schema = schema.extend({
vol.Optional('home_ids', default=[]): cv.multi_select(homes),
})
tip = ''
if user_input.get(CONF_CONN_MODE) == 'local':
url = 'https://github.com/al-one/hass-xiaomi-miot/issues/100#issuecomment-855183156'
if user_input.get(CONF_SERVER_COUNTRY) == 'cn':
tip = '⚠️ 在本地模式下,所有包含的设备都将通过本地miot协议连接,如果包含了不支持本地miot协议的设备,其实体会不可用,' \
f'建议只选择[支持本地模式的设备]({url})。'
else:
tip = '⚠️ In the local mode, all included devices will be connected via the local miot protocol.' \
'If the devices that does not support the local miot protocol are included,' \
'they will be unavailable. It is recommended to include only ' \
f'[the devices that supports the local mode]({url}).'
self.placeholders['tip'] = tip
return schema
class XiaomiMiotFlowHandler(config_entries.ConfigFlow, BaseFlowHandler, domain=DOMAIN):
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
filter_models = None
@staticmethod
@callback
def async_get_options_flow(entry: config_entries.ConfigEntry):
return OptionsFlowHandler(entry)
async def async_step_user(self, user_input=None):
self.context['last_step'] = False
init_integration_data(self.hass)
errors = {}
if user_input is None:
user_input = {}
else:
action = user_input.get('action')
if action in ['account', 'cloud']:
return await self.async_step_cloud()
elif action in ['customizing_entity', 'customizing_device']:
self.context['customizing_via'] = action
return await self.async_step_customizing()
else:
return await self.async_step_token()
prev_action = user_input.get('action', 'account')
if prev_action == 'cloud':
prev_action = 'account'
actions = {
'account': 'Add devices using Mi Account (账号集成)',
'token': 'Add device using host/token (局域网集成)',
}
if self.hass.data[DOMAIN].get('entities', {}):
actions.update({
'customizing_device': 'Customizing device (自定义设备) <推荐>',
'customizing_entity': 'Customizing entity (自定义实体)',
})
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required('action', default=prev_action): vol.In(actions),
}),
errors=errors,
last_step=self.context.get('last_step'),
)
async def async_step_token(self, user_input=None):
errors = {}
if user_input is None:
user_input = {}
else:
await check_miio_device(self.hass, user_input, errors)
if user_input.get('unique_did'):
await self.async_set_unique_id(user_input['unique_did'])
self._abort_if_unique_id_configured()
if user_input.get('miio_info'):
user_input[CONF_CONFIG_VERSION] = ENTRY_VERSION
return self.async_create_entry(
title=user_input.get(CONF_NAME),
data=user_input,
)
return self.async_show_form(
step_id='token',
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, vol.UNDEFINED)): str,
vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN, vol.UNDEFINED)):
vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)): str,
vol.Optional(CONF_SCAN_INTERVAL, default=user_input.get(CONF_SCAN_INTERVAL, DEFAULT_INTERVAL)):
cv.positive_int,
}),
errors=errors,
)
async def async_step_cloud(self, user_input=None):
# pylint: disable=invalid-name
self.CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
errors = {}
if user_input is None:
user_input = {}
else:
await self.check_xiaomi_account(user_input, errors, renew_devices=True)
if not errors:
user_input['filtering'] = True
self.config_data = user_input
self.filter_models = user_input.get('filter_models')
return await self.async_step_cloud_filter(user_input)
schema = {}
if self.context.get('need_verify'):
schema.update({
vol.Optional('verify_ticket', default=''): str,
})
if self.context.get('captchaIck'):
schema.update({
vol.Optional('captcha', default=''): str,
})
schema.update({
vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, vol.UNDEFINED)): str,
vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, vol.UNDEFINED)): str,
vol.Required(CONF_SERVER_COUNTRY, default=user_input.get(CONF_SERVER_COUNTRY, 'cn')):
vol.In(CLOUD_SERVERS),
vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, 'auto')):
vol.In(CONN_MODES),
vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool,
vol.Optional('filter_models', default=user_input.get('filter_models', False)): bool,
})
return self.async_show_form(
step_id='cloud',
data_schema=vol.Schema(schema),
errors=errors,
description_placeholders=self.pop_placeholders(),
)
async def async_step_cloud_filter(self, user_input=None):
errors = {}
schema = vol.Schema({})
if user_input is None:
user_input = {}
via_did = not self.filter_models
home_ids = user_input.pop('home_ids', [])
if user_input.pop('filtering', None) or home_ids:
schema = await self.get_cloud_filter_schema(user_input, errors, schema, via_did=via_did, home_ids=home_ids)
elif user_input:
if not self.config_data:
self.config_data = {}
self.config_data.update({
**(self.cloud.to_config() or {}),
**user_input,
'filter_models': self.filter_models,
CONF_CONFIG_VERSION: ENTRY_VERSION,
})
_LOGGER.debug('Setup xiaomi cloud: %s', {**self.config_data, CONF_PASSWORD: '*', 'service_token': '*'})
return self.async_create_entry(
title=f"Xiaomi: {self.config_data.get('user_id')}",
data=self.config_data,
)
else:
errors['base'] = 'unknown'
return self.async_show_form(
step_id='cloud_filter',
data_schema=schema,
errors=errors,
description_placeholders=self.pop_placeholders(),
)
async def async_step_customizing(self, user_input=None):
tip = ''
via = self.context.get('customizing_via') or 'customizing_device'
self.context['customizing_via'] = via
entry = await self.async_set_unique_id(f'{DOMAIN}-customizes')
entry_data = copy.deepcopy(dict(entry.data) if entry else {})
customizes = {}
errors = {}
schema = {}
user_input = user_input or {}
bool2selects = [
'auto_cloud',
'ignore_offline',
'miot_local',
'miot_cloud',
'miot_cloud_write',
'miot_cloud_action',
'check_lan',
'unreadable_properties',
]
main_options = {
'bool2selects': cv.multi_select({}),
'interval_seconds': cv.string,
'chunk_properties': cv.string,
'sensor_properties': cv.string,
'binary_sensor_properties': cv.string,
'switch_properties': cv.string,
'number_properties': cv.string,
'select_properties': cv.string,
'button_properties': cv.string,
'target_position_properties': cv.string,
'sensor_attributes': cv.string,
'binary_sensor_attributes': cv.string,
'button_actions': cv.string,
'select_actions': cv.string,
'text_actions': cv.string,
'exclude_miot_services': cv.string,
'exclude_miot_properties': cv.string,
'configuration_entities': cv.string,
'diagnostic_entities': cv.string,
'cloud_delay_update': cv.string,
}
options = {
'entity_category': cv.string,
}
last_step = self.context.pop('last_step', False)
customize_key = self.context.pop('customize_key', None)
if last_step and customize_key:
reset = user_input.pop('reset_customizes', None)
b2s = user_input.pop('bool2selects', None) or []
customize_data = entry_data.setdefault(via, {})
if reset:
entry_data[via].pop(customize_key, None)
elif self.context.get('yaml_mode'):
yml = user_input.get('yaml_customizes') or {}
if yml:
customize_data[customize_key] = yml
else:
entry_data[via].pop(customize_key, None)
else:
for k in b2s:
user_input[k] = True
customize_data[customize_key] = {
k: v
for k, v in user_input.items()
if v not in [' ', '', None, vol.UNDEFINED]
}
if entry:
self.hass.config_entries.async_update_entry(entry, data=entry_data)
await self.hass.config_entries.async_reload(entry.entry_id)
tip = f'```yaml\n{yaml.dump(entry_data)}\n```'
return self.async_abort(
reason='config_saved',
description_placeholders={'tip': tip},
)
return self.async_create_entry(title='Xiaomi: Customizes', data=entry_data)
elif via == 'customizing_entity':
if entity := user_input.get('entity'):
customizes = entry_data.get(via, {}).get(entity) or {}
ent = self.hass.data[DOMAIN].get('entities', {}).get(entity)
model = ent.model or ''
for k, v in (get_customize_via_entity(ent) or {}).items():
customizes.setdefault(k, v)
state = self.hass.states.get(entity)
tip = f'{state.name}\n{entity}'
if model:
tip += f'\n[{model}](https://home.miot-spec.com/spec/{model})'
if not hasattr(ent, 'parent_entity'):
options = {**main_options, **options}
get_customize_options(self.hass, options, bool2selects, entity_id=entity, model=model)
if options:
self.context['last_step'] = True
self.context['customize_key'] = entity
elif domain := user_input.get('domain'):
entities = {}
for state in sorted(
self.hass.states.async_all(domain),
key=lambda item: item.entity_id,
):
entity = state.entity_id
ent = self.hass.data[DOMAIN].get('entities', {}).get(entity)
if not ent:
continue
if user_input.get('only_main_entity') and hasattr(ent, 'parent_entity'):
continue
entities[entity] = f'{state.name} ({entity})'
if entities:
schema.update({
vol.Required('entity'): vol.In(entities),
vol.Optional('yaml_mode', default=user_input.get('yaml_mode', False)): cv.boolean,
})
else:
tip = f'None entities in `{domain}`'
else:
tip = ('⚠️ 自定义实体后续可能会弃用,推荐通过设备型号**自定义设备**。\n'
'The Customization of entity may be deprecated in the future, '
'it is recommended to customize the device through the device model.')
schema.update({
vol.Required('domain', default=user_input.get('domain', vol.UNDEFINED)): vol.In(SUPPORTED_DOMAINS),
vol.Optional('only_main_entity', default=user_input.get('only_main_entity', True)): cv.boolean,
})
elif via == 'customizing_device':
model = user_input.get('model_specified') or user_input.get('model')
if model:
customizes = entry_data.get(via, {}).get(model) or {}
for k, v in (get_customize_via_model(model) or {}).items():
customizes.setdefault(k, v)
if '*' in model or ':' in model:
tip = model
else:
tip = f'[{model}](https://home.miot-spec.com/spec/{model})'
if ':' not in model:
options = {**main_options, **options}
get_customize_options(self.hass, options, bool2selects, model=model)
if options:
self.context['last_step'] = True
self.context['customize_key'] = model
else:
models = {}
uds = {}
for v in self.hass.data[DOMAIN].values():
if isinstance(v, dict):
if mod := v.get('miio_info', {}).get(CONF_MODEL):
models[mod] = v
v = v.get(CONF_XIAOMI_CLOUD)
if isinstance(v, MiotCloud):
mic = v
if mic.user_id not in uds:
uds[mic.user_id] = await mic.async_get_devices_by_key('model') or {}
models.update(uds[mic.user_id])
if models:
models = sorted(models.keys())
schema.update({
vol.Required('model'): vol.In(models),
})
schema.update({
vol.Optional('model_specified'): str,
vol.Optional('yaml_mode', default=user_input.get('yaml_mode', False)): cv.boolean,
})
if last_step := self.context.get('last_step', last_step):
doc = 'https://github.com/al-one/hass-xiaomi-miot/issues/600'
if in_china(self.hass):
tip = f'[📚 自定义选项说明文档]({doc})\n\n------\n{tip}'
else:
tip = f'[❓ Need Help]({doc})\n\n------\n{tip}'
if not options:
tip += f'\n\n无可用的自定义选项。' if in_china(self.hass) else f'\n\nNo customizable options are available.'
customizes.pop('extend_miot_specs', None)
self.context['yaml_mode'] = user_input.get('yaml_mode')
if self.context['yaml_mode']:
schema.update({
vol.Optional('yaml_customizes', default=customizes): ObjectSelector(),
})
else:
if 'bool2selects' in options:
options['bool2selects'] = cv.multi_select(dict(zip(bool2selects, bool2selects)))
customizes['bool2selects'] = [
k
for k in bool2selects
if customizes.get(k)
]
schema.update({
vol.Optional(k, default=customizes.get(k, vol.UNDEFINED), description=k): v
for k, v in options.items()
})
schema.update({
vol.Optional('reset_customizes', default=False): cv.boolean,
})
if customizes:
customizes.pop('bool2selects', None)
tip += f'\n```yaml\n{yaml.dump(customizes)}\n```'
return self.async_show_form(
step_id='customizing',
data_schema=vol.Schema(schema),
errors=errors,
description_placeholders={'tip': tip},
last_step=last_step,
)
class OptionsFlowHandler(config_entries.OptionsFlow, BaseFlowHandler):
def __init__(self, config_entry: config_entries.ConfigEntry):
if HA_VERSION < '2024.12':
self.config_entry = config_entry
@property
def saved_config(self):
return {
**self.config_entry.data,
**self.config_entry.options,
}
@property
def filter_models(self):
data = self.saved_config
if data.get('did_list'):
return False
if data.get('model_list'):
return True
if 'did_list' in data:
return False
if 'model_list' in data:
return True
return data.get('filter_models', False)
async def async_step_init(self, user_input=None):
data = self.config_entry.data
if CONF_USERNAME in data:
self.config_data = self.saved_config
return await self.async_step_cloud()
if 'customizing_entity' in data or 'customizing_device' in data:
tip = f'```yaml\n{yaml.dump(dict(data))}\n```'
return self.async_abort(
reason='show_customizes',
description_placeholders={'tip': tip},
)
return await self.async_step_user()
async def async_step_user(self, user_input=None):
errors = {}
if isinstance(user_input, dict):
cfg = {}
opt = {}
for k, v in user_input.items():
if k in [CONF_HOST, CONF_TOKEN, CONF_NAME, CONF_SCAN_INTERVAL]:
cfg[k] = v
else:
opt[k] = v
await check_miio_device(self.hass, user_input, errors)
if user_input.get('miio_info'):
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, **cfg}
)
return self.async_create_entry(title='', data=opt)
else:
user_input = self.saved_config
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, vol.UNDEFINED)): str,
vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN, vol.UNDEFINED)):
vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_SCAN_INTERVAL, default=user_input.get(CONF_SCAN_INTERVAL, DEFAULT_INTERVAL)):
cv.positive_int,
vol.Optional('miot_cloud', default=user_input.get('miot_cloud', False)): bool,
}),
errors=errors,
)
async def async_step_cloud(self, user_input=None):
errors = {}
prev_input = self.saved_config
if isinstance(user_input, dict):
user_input = {
**prev_input,
**user_input,
}
renew = not not user_input.pop('renew_devices', False)
await self.check_xiaomi_account(user_input, errors, renew_devices=renew)
if not errors:
user_input['filtering'] = True
self.config_data.update(user_input)
return await self.async_step_cloud_filter(user_input)
else:
user_input = prev_input
schema = {}
if self.context.get('need_verify'):
schema.update({
vol.Optional('verify_ticket', default=''): str,
})
if self.context.get('captchaIck'):
schema.update({
vol.Optional('captcha', default=''): str,
})
if user_input.get('trans_options') == None:
user_input['trans_options'] = False
schema.update({
vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, vol.UNDEFINED)): str,
vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, vol.UNDEFINED)): str,
vol.Required(CONF_SERVER_COUNTRY, default=user_input.get(CONF_SERVER_COUNTRY, 'cn')):
vol.In(CLOUD_SERVERS),
vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, DEFAULT_CONN_MODE)):
vol.In(CONN_MODES),
vol.Optional('renew_devices', default=user_input.get('renew_devices', False)): bool,
vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool,
vol.Optional('disable_message', default=user_input.get('disable_message', False)): bool,
vol.Optional('disable_scene_history', default=user_input.get('disable_scene_history', False)): bool,
})
return self.async_show_form(
step_id='cloud',
data_schema=vol.Schema(schema),
errors=errors,
description_placeholders=self.pop_placeholders(),
)
async def async_step_cloud_filter(self, user_input=None):
errors = {}
schema = vol.Schema({})
if user_input is None:
user_input = {}
via_did = not self.filter_models
home_ids = user_input.pop('home_ids', [])
if user_input.pop('filtering', None) or home_ids:
user_input = {
**self.saved_config,
**user_input,
}
schema = await self.get_cloud_filter_schema(user_input, errors, schema, via_did=via_did, home_ids=home_ids)
elif user_input:
self.config_data.update({
**(self.cloud.to_config() or {}),
'filter_models': self.filter_models,
**user_input,
})
self.config_data.pop('filtering', None)
self.config_data.pop('verify_ticket', None)
if self.filter_models:
self.config_data.pop('filter_did', None)
self.config_data.pop('did_list', None)
else:
self.config_data.pop('filter_model', None)
self.config_data.pop('model_list', None)
self.hass.config_entries.async_update_entry(self.config_entry, data=self.config_data)
_LOGGER.debug('Setup xiaomi cloud: %s', {**self.config_data, CONF_PASSWORD: '*', 'service_token': '*'})
return self.async_create_entry(title='', data={})
else:
errors['base'] = 'unknown'
return self.async_show_form(
step_id='cloud_filter',
data_schema=schema,
errors=errors,
description_placeholders=self.pop_placeholders(),
)
def get_customize_options(hass, options={}, bool2selects=[], entity_id='', model=''): # noqa
entity = None
domain = ''
if entity_id:
entity = hass.data[DOMAIN].get('entities', {}).get(entity_id)
domain, _ = split_entity_id(entity_id)
attrs = entity.extra_state_attributes if entity else {}
entity_class = attrs.get('entity_class')
if domain == 'sensor':
if entity_class in ['MiotSensorEntity']:
options.update({
'state_property': cv.string,
})
options.update({
'value_ratio': cv.string,
'state_class': cv.string,
'device_class': cv.string,
'unit_of_measurement': cv.string,
})
if entity_class in ['MihomeMessageSensor']:
options.update({
'filter_home': cv.string,
'exclude_type': cv.string,
})
if entity_class in ['XiaoaiConversationSensor']:
options.update({
'interval_seconds': cv.string,
})
if domain == 'binary_sensor' or re.search(r'motion|magnet', model, re.I):
bool2selects.extend(['reverse_state'])
options.update({
'state_property': cv.string,
'motion_timeout': cv.string,
})
if domain == 'switch' or re.search(r'plug', model, re.I):
bool2selects.extend(['reverse_state'])
options.update({
'descriptions_for_on': cv.string,
'descriptions_for_off': cv.string,
'stat_power_cost_key': cv.string,
'stat_power_cost_type': cv.string,
})
if domain == 'light' or re.search(r'light', model, re.I):
bool2selects.extend(['color_temp_reverse'])
options.update({
'brightness_for_on': cv.string,
'brightness_for_off': cv.string,
})
if domain == 'fan' or re.search(r'\.fan\.|airpurifier|airfresh', model, re.I):
options.update({
'disable_preset_modes': cv.string,
'speed_property': cv.string,
'percentage_property': cv.string,
})
if domain == 'camera' or re.search(r'camera|videodoll', model, re.I):
bool2selects.extend([
'keep_streaming', 'use_rtsp_stream', 'use_alarm_playlist',
'use_motion_stream', 'sub_motion_stream',
])
options.update({
'video_attribute': cv.string,
'motion_stream_slice': cv.string,
})
if domain == 'climate' or re.search(r'aircondition|acpartner|airrtc', model, re.I):
bool2selects.extend(['ignore_fan_switch', 'target2current_temp'])
options.update({
'bind_sensor': cv.string,
'turn_on_hvac': cv.string,
'current_temp_property': cv.string,
})
if domain == 'cover' or re.search(r'airer|curtain|wopener', model, re.I):
bool2selects.extend([
'motor_reverse', 'auto_position_reverse', 'position_reverse', 'target2current_position',
])
options.update({
'closed_position': cv.string,
'deviated_position': cv.string,
'open_texts': cv.string,
'close_texts': cv.string,
})
if domain == 'media_player' or re.search(r'\.tv\.|tvbox|projector', model, re.I):
bool2selects.extend(['turn_off_screen', 'xiaoai_silent'])
options.update({
'miot_did': cv.string,
'bind_xiaoai': cv.string,
'sources_via_apps': cv.string,
'sources_via_keycodes': cv.string,
'screenshot_compress': cv.string,
'television_name': cv.string,
'mitv_lan_host': cv.string,
})
if domain == 'number':
bool2selects.extend(['restore_state'])
if domain == 'humidifier':
options.update({
'mode_property': cv.string,
})
if domain == 'device_tracker' or re.search(r'watch', model, re.I):
options.update({
'coord_type': cv.string,
})
bool2selects.extend(['disable_location_name'])
if re.search(r'sensor_occupy', model, re.I):
options.update({
'scanner_properties': cv.string,
})
if domain == 'text' and re.search(r'execute_text_directive', entity_id, re.I):
bool2selects.extend(['silent_execution'])
if 'yeelink.' in model:
options.update({
'yeelight_smooth_on': cv.string,
'yeelight_smooth_off': cv.string,
})
return options
@@ -0,0 +1,11 @@
from .device import Device, DeviceInfo
from .hass_entry import HassEntry
from .hass_entity import BasicEntity, XEntity
__all__ = [
'Device',
'DeviceInfo',
'HassEntry',
'BasicEntity',
'XEntity',
]
@@ -0,0 +1,114 @@
from enum import Enum
from typing import Union
from homeassistant.const import __version__ as HAVERSION # noqa
from awesomeversion import AwesomeVersion
from .device_customizes import DEVICE_CUSTOMIZES, GLOBAL_CONVERTERS # noqa
from .miot_local_devices import MIOT_LOCAL_MODELS # noqa
from .translation_languages import TRANSLATION_LANGUAGES # noqa
DOMAIN = 'xiaomi_miot'
DEFAULT_NAME = 'Xiaomi Miot'
HA_VERSION = AwesomeVersion(HAVERSION)
CONF_MODEL = 'model'
CONF_XIAOMI_CLOUD = 'xiaomi_cloud'
CONF_SERVER_COUNTRY = 'server_country'
CONF_CONN_MODE = 'conn_mode'
CONF_CONFIG_VERSION = 'config_version'
DEFAULT_CONN_MODE = 'cloud'
SUPPORTED_DOMAINS = [
'sensor',
'binary_sensor',
'switch',
'number',
'select',
'button',
'text',
'light',
'fan',
'climate',
'cover',
'humidifier',
'media_player',
'camera',
'vacuum',
'water_heater',
'device_tracker',
'remote',
'alarm_control_panel',
]
CLOUD_SERVERS = {
'cn': '中国大陆',
'tw': '中國台灣',
'de': 'Europe',
'i2': 'India',
'ru': 'Russia',
'sg': 'Singapore',
'us': 'United States',
}
try:
# python 3.11
from enum import StrEnum
except (ModuleNotFoundError, ImportError):
class StrEnum(str, Enum):
pass
try:
# hass 2023.3
from homeassistant.util.json import JsonObjectType
except (ModuleNotFoundError, ImportError):
JsonObjectType = dict
try:
# hass 2023.7
from homeassistant.core import ServiceResponse, SupportsResponse
except (ModuleNotFoundError, ImportError):
SupportsResponse = None
ServiceResponse = Union[dict, None]
try:
# hass 2024.10
from homeassistant.components.camera import CameraState
except (ModuleNotFoundError, ImportError):
class CameraState(StrEnum):
RECORDING = 'recording'
STREAMING = 'streaming'
IDLE = 'idle'
try:
# hass 2024.11
from homeassistant.core_config import DATA_CUSTOMIZE
except (ModuleNotFoundError, ImportError):
from homeassistant.helpers.entity import DATA_CUSTOMIZE
try:
# hass 2024.11
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
except (ModuleNotFoundError, ImportError):
class AlarmControlPanelState(StrEnum):
"""Alarm control panel entity states."""
DISARMED = "disarmed"
ARMED_HOME = "armed_home"
ARMED_AWAY = "armed_away"
ARMED_NIGHT = "armed_night"
ARMED_VACATION = "armed_vacation"
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
PENDING = "pending"
ARMING = "arming"
DISARMING = "disarming"
TRIGGERED = "triggered"
try:
# hass 2025.1
from homeassistant.components.vacuum import VacuumActivity
except (ModuleNotFoundError, ImportError):
class VacuumActivity(StrEnum):
CLEANING = "cleaning"
DOCKED = "docked"
RETURNING = "returning"
ERROR = "error"
@@ -0,0 +1,349 @@
from typing import TYPE_CHECKING, Any
from dataclasses import dataclass
from homeassistant.util import color, percentage
if TYPE_CHECKING:
from .device import Device
from .miot_spec import MiotService, MiotProperty, MiotAction
@dataclass
class BaseConv:
attr: str
domain: str = None
mi: str | int = None
attrs: list = None
option: dict = None
def __post_init__(self):
if self.attrs is None:
self.attrs = []
if self.option is None:
self.option = {}
def with_option(self, **kwargs):
self.option.update(kwargs)
return self
@property
def full_name(self):
if not self.domain:
return self.attr
return f'{self.domain}.{self.attr}'
def value_from_dict(self, data):
return data.get(self.full_name, data.get(self.attr))
# to hass
def decode(self, device: 'Device', payload: dict, value):
payload[self.full_name] = value
# from hass
def encode(self, device: 'Device', payload: dict, value):
params = None
if self.mi and 'prop.' in self.mi:
_, s, p = self.mi.split('.')
payload['method'] = 'set_properties'
params = {'siid': int(s), 'piid': int(p)}
if params:
params.update({'did': device.did, 'value': value})
payload.setdefault('params', []).append(params)
@dataclass
class InfoConv(BaseConv):
attr: str = 'info'
domain: str = 'button'
def decode(self, device: 'Device', payload: dict, value):
updater = device.data.get('updater')
infos = {
self.full_name: device.name,
'model': device.model,
'did': device.info.did,
'mac': device.info.mac,
'lan_ip': device.info.host,
'app_link': device.app_link,
'miot_type': device.info.urn,
'available': device.available,
'home_room': device.info.home_room,
'icon': self.option.get('icon') if device.available else 'mdi:information-off',
'updater': updater or 'none',
}
customizes = {**device.customizes}
customizes.pop('append_converters', None)
customizes.pop('extend_miot_specs', None)
payload.update({
**infos,
**device.props,
'converters': [c.full_name for c in device.converters],
'customizes': customizes,
**infos,
})
if device.available:
payload.pop('miot_error', None)
if device.miot_results:
if err := device.miot_results.errors:
payload['miot_error'] = str(err)
def encode(self, device: 'Device', payload: dict, value):
payload.update({
'method': 'update_status',
})
@dataclass
class AttrConv(BaseConv):
pass
@dataclass
class MiotPropConv(BaseConv):
prop: 'MiotProperty' = None
desc: bool = None
def __post_init__(self):
super().__post_init__()
if self.prop:
if not self.mi:
from .miot_spec import MiotSpec
self.mi = MiotSpec.unique_prop(self.prop.siid, piid=self.prop.iid)
if self.desc == None:
self.desc = self.prop.use_desc(self.domain)
def decode(self, device: 'Device', payload: dict, value):
if self.desc and self.prop:
value = self.prop.list_description(value)
if self.domain == 'sensor' and isinstance(value, str):
value = value.lower()
super().decode(device, payload, value)
def encode(self, device: 'Device', payload: dict, value):
if self.prop:
if self.desc:
if isinstance(value, list):
value = self.prop.list_first(value)
else:
value = self.prop.list_value(value)
elif self.prop.is_integer:
value = int(value) # bool to int
super().encode(device, payload, value)
@dataclass
class MiotPropValueConv(MiotPropConv):
value: Any = None
description: str = None
def decode(self, device: 'Device', payload: dict, value):
pass
@dataclass
class MiotActionConv(BaseConv):
action: 'MiotAction' = None
prop: 'MiotProperty' = None
def __post_init__(self):
super().__post_init__()
if not self.mi:
from .miot_spec import MiotSpec
self.mi = MiotSpec.unique_prop(self.action.siid, aiid=self.action.iid)
if not self.prop:
self.prop = self.action.in_properties()[0] if self.action.ins else None
def decode(self, device: 'Device', payload: dict, value):
super().decode(device, payload, value)
def encode(self, device: 'Device', payload: dict, value):
if self.prop and isinstance(value, str):
if self.prop.value_list or self.prop.value_range:
value = self.prop.list_value(value)
elif self.prop.is_integer:
value = int(value)
ins = value if isinstance(value, list) else [] if value is None else [value]
_, s, p = self.mi.split('.')
payload['method'] = 'action'
payload['param'] = {
'did': device.did,
'siid': int(s),
'aiid': int(p),
'in': ins,
}
@dataclass
class MiotServiceConv(MiotPropConv):
attr: str = None
service: 'MiotService' = None
prop: 'MiotProperty' = None
main_props: list = None
def __post_init__(self):
if not self.prop and self.service and self.main_props:
self.prop = self.service.get_property(*self.main_props)
super().__post_init__()
if self.attr:
pass
elif self.prop:
self.attr = self.prop.full_name
elif self.service:
self.attr = self.service.desc_name
@dataclass
class MiotSensorConv(MiotServiceConv):
domain: str = 'sensor'
@dataclass
class MiotSwitchConv(MiotServiceConv):
domain: str = 'switch'
def __post_init__(self):
if not self.main_props:
self.main_props = ['on', 'switch']
super().__post_init__()
@dataclass
class MiotLightConv(MiotSwitchConv):
domain: str = 'light'
@dataclass
class MiotBrightnessConv(MiotPropConv):
def decode(self, device: 'Device', payload: dict, value: int):
max = self.prop.range_max()
if max != None:
super().decode(device, payload, value / max * 255.0)
def encode(self, device: 'Device', payload: dict, value: float):
max = self.prop.range_max()
if max != None:
value = round(value / 255.0 * max)
super().encode(device, payload, int(value))
@dataclass
class MiotColorTempConv(MiotPropConv):
def decode(self, device: 'Device', payload: dict, value: int):
if self.prop.unit == 'percentage':
if not value:
return
value = self.percentage_to_kelvin(value)
elif self.prop.unit != 'kelvin':
if not value:
return
value = round(1000000.0 / value)
super().decode(device, payload, value)
def encode(self, device: 'Device', payload: dict, value: int):
if self.prop.unit == 'percentage':
if not value:
return
value = self.kelvin_to_percentage(value)
elif self.prop.unit != 'kelvin':
if not value:
return
value = round(1000000.0 / value)
if value < self.prop.range_min():
value = self.prop.range_min()
if value > self.prop.range_max():
value = self.prop.range_max()
super().encode(device, payload, value)
@staticmethod
def percentage_to_kelvin(p: int) -> int:
return 6500 - p * 40
@staticmethod
def kelvin_to_percentage(k: int) -> int:
return round((6500 - k) / 40)
@dataclass
class MiotRgbColorConv(MiotPropConv):
def decode(self, device: 'Device', payload: dict, value: int):
super().decode(device, payload, MiotRgbColorConv.int_to_rgb(value))
def encode(self, device: 'Device', payload: dict, rgb: tuple[int, int, int]):
super().encode(device, payload, MiotRgbColorConv.rgb_to_int(rgb))
@staticmethod
def rgb_to_int(rgb: tuple[int, int, int]):
num = int(rgb[0]) << 16 | int(rgb[1]) << 8 | int(rgb[2])
return int(num)
@staticmethod
def int_to_rgb(value: int):
x = int(value)
r = (x >> 16) & 0xFF
g = (x >> 8) & 0xFF
b = x & 0xFF
return r, g, b
@dataclass
class MiotHsColorConv(MiotPropConv):
def decode(self, device: 'Device', payload: dict, value: int):
rgb = MiotRgbColorConv.int_to_rgb(value)
super().decode(device, payload, color.color_RGB_to_hs(*rgb))
def encode(self, device: 'Device', payload: dict, value: tuple):
rgb = color.color_hs_to_RGB(*value)
super().encode(device, payload, MiotRgbColorConv.rgb_to_int(rgb))
@dataclass
class MiotFanConv(MiotServiceConv):
domain: str = 'fan'
def __post_init__(self):
if not self.main_props:
self.main_props = ['on', 'fan_level']
super().__post_init__()
@dataclass
class MiotClimateConv(MiotServiceConv):
domain: str = 'climate'
def __post_init__(self):
if not self.main_props:
self.main_props = ['mode', 'target_temperature']
super().__post_init__()
@dataclass
class MiotCoverConv(MiotServiceConv):
domain: str = 'cover'
def __post_init__(self):
if not self.main_props:
self.main_props = ['motor_control']
super().__post_init__()
@dataclass
class MiotCameraConv(MiotServiceConv):
domain: str = 'camera'
@dataclass
class MiotHumidifierConv(MiotServiceConv):
domain: str = 'humidifier'
def __post_init__(self):
if not self.main_props:
self.main_props = ['on', 'target_humidity']
super().__post_init__()
@dataclass
class PercentagePropConv(MiotPropConv):
ranged = None
def __post_init__(self):
super().__post_init__()
if self.prop and self.prop.value_range:
self.ranged = (self.prop.range_min(), self.prop.range_max())
def decode(self, device: 'Device', payload: dict, value: int):
if self.ranged:
value = int(percentage.scale_ranged_value_to_int_range(self.ranged, (0, 100), value))
super().decode(device, payload, value)
def encode(self, device: 'Device', payload: dict, value: int):
if self.ranged:
value = int(percentage.scale_to_ranged_value((0, 100), self.ranged, value))
if value < self.ranged[0]:
value = self.ranged[0]
if value > self.ranged[1]:
value = self.ranged[1]
super().encode(device, payload, value)
class MiotTargetPositionConv(PercentagePropConv):
pass
@@ -0,0 +1,123 @@
"""
https://github.com/wandergis/coordTransform_py/blob/master/coordTransform_utils.py
"""
import math
from math import pi
a = 6378245.0 # 长半轴
ee = 0.00669342162296594323 # 偏心率平方
x_pi = pi * 3000.0 / 180.0
def wgs84_to_gcj02(lng, lat):
"""
WGS84转GCJ02(火星坐标系)
:param lng:WGS84坐标系的经度
:param lat:WGS84坐标系的纬度
:return:火星坐标
"""
if out_of_china(lng, lat): # 判断是否在国内
return [lng, lat]
dlat = _transformlat(lng - 105.0, lat - 35.0)
dlng = _transformlng(lng - 105.0, lat - 35.0)
radlat = lat / 180.0 * pi
magic = math.sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
mglat = lat + dlat
mglng = lng + dlng
return [mglng, mglat]
def wgs84_to_bd09(lon, lat):
lon, lat = wgs84_to_gcj02(lon, lat)
return gcj02_to_bd09(lon, lat)
def gcj02_to_wgs84(lng, lat):
"""
GCJ02(火星坐标系)转GPS84
:param lng:火星坐标系的经度
:param lat:火星坐标系纬度
:return:原始坐标
"""
if out_of_china(lng, lat):
return [lng, lat]
dlat = _transformlat(lng - 105.0, lat - 35.0)
dlng = _transformlng(lng - 105.0, lat - 35.0)
radlat = lat / 180.0 * pi
magic = math.sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
mglat = lat + dlat
mglng = lng + dlng
return [lng * 2 - mglng, lat * 2 - mglat]
def gcj02_to_bd09(lng, lat):
"""
火星坐标系(GCJ-02)转百度坐标系(BD-09)
谷歌、高德——>百度
:param lng:火星坐标经度
:param lat:火星坐标纬度
:return:百度坐标
"""
z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi)
theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi)
bd_lng = z * math.cos(theta) + 0.0065
bd_lat = z * math.sin(theta) + 0.006
return [bd_lng, bd_lat]
def bd09_to_gcj02(bd_lon, bd_lat):
"""
百度坐标系(BD-09)转火星坐标系(GCJ-02)
百度——>谷歌、高德
:param bd_lat:百度坐标纬度
:param bd_lon:百度坐标经度
:return:转换后的坐标列表形式
"""
x = bd_lon - 0.0065
y = bd_lat - 0.006
z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
gg_lng = z * math.cos(theta)
gg_lat = z * math.sin(theta)
return [gg_lng, gg_lat]
def bd09_to_wgs84(bd_lon, bd_lat):
lon, lat = bd09_to_gcj02(bd_lon, bd_lat)
return gcj02_to_wgs84(lon, lat)
def _transformlat(lng, lat):
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \
0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
math.sin(2.0 * lng * pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(lat * pi) + 40.0 *
math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 *
math.sin(lat * pi / 30.0)) * 2.0 / 3.0
return ret
def _transformlng(lng, lat):
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
math.sin(2.0 * lng * pi)) * 2.0 / 3.0
ret += (20.0 * math.sin(lng * pi) + 40.0 *
math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 *
math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
return ret
def out_of_china(lng, lat):
return not (73.66 < lng < 135.05 and 3.86 < lat < 53.55)
@@ -0,0 +1,52 @@
import logging
from typing import TYPE_CHECKING
from homeassistant.core import HassJob, HassJobType
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
if TYPE_CHECKING:
from .device import Device
_LOGGER = logging.getLogger(__name__)
class DataCoordinator(DataUpdateCoordinator):
def __init__(self, device: 'Device', update_method, **kwargs):
kwargs.setdefault('always_update', True)
if callable(update_method):
name = update_method.__name__
elif isinstance(update_method, str):
name = update_method
update_method = getattr(device, name, None)
else:
raise ValueError('Invalid update method')
name = kwargs.pop('name', name)
super().__init__(
device.hass,
logger=device.log,
name=f'{device.unique_id}-{name}',
update_method=update_method,
**kwargs,
)
self.device = device
if not hasattr(self, 'setup_method'):
# hass v2024.7-
self.async_add_listener(self.coordinator_updated)
async def async_setup(self, index=0):
await self._async_setup()
job = HassJob(self._async_refresh_later, job_type=HassJobType.Coroutinefunction)
async_call_later(self.hass, index, job)
async def _async_setup(self):
"""Set up coordinator."""
self.async_add_listener(self.coordinator_updated)
def coordinator_updated(self):
_LOGGER.debug('%s: Coordinator updated: %s', self.device.name_model, [self.name, self.data])
async def _async_refresh_later(self, _=None):
await self.async_request_refresh()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
import logging
from typing import TYPE_CHECKING, Optional, Callable
from functools import cached_property
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity, EntityCategory
from homeassistant.helpers.restore_state import ExtraStoredData, RestoredExtraData
from .const import DOMAIN
from .utils import get_customize_via_entity, wildcard_models, CustomConfigHelper
from .miot_spec import MiotService, MiotProperty, MiotAction
from .converters import BaseConv, InfoConv, MiotServiceConv, MiotPropConv, MiotActionConv
if TYPE_CHECKING:
from .device import Device
_LOGGER = logging.getLogger(__name__)
class BasicEntity(Entity, CustomConfigHelper):
hass: HomeAssistant = None
device: 'Device' = None
conv: 'BaseConv' = None
def custom_config(self, key=None, default=None):
return get_customize_via_entity(self, key, default)
def global_config(self, key=None, default=None):
if not self.hass:
return default
cfg = self.hass.data[DOMAIN]['config'] or {}
return cfg if key is None else cfg.get(key, default)
def filter_state_attributes(self, dat: dict):
if exl := self.global_config('exclude_state_attributes'):
if not isinstance(exl, (list, tuple)):
exl = [exl]
dat = {
k: v
for k, v in dat.items()
if k not in exl
}
return dat
@cached_property
def extra_state_attributes(self):
if hasattr(self, '_attr_extra_state_attributes'):
return self.filter_state_attributes(self._attr_extra_state_attributes)
return None
async def async_get_properties(self, mapping, update_entity=False, **kwargs):
return await self.device.async_get_properties(mapping, update_entity, **kwargs)
async def async_set_property(self, field, value):
return await self.device.async_set_property(field, value)
async def async_set_miot_property(self, siid, piid, value, **kwargs):
return await self.device.async_set_miot_property(siid, piid, value, **kwargs)
async def async_call_action(self, siid, aiid=None, params=None, **kwargs):
if isinstance(siid, MiotAction):
if params is None:
params = aiid
action = siid
siid = action.service.iid
aiid = action.iid
return await self.device.async_call_action(siid, aiid, params, **kwargs)
async def async_miio_command(self, method, params=None, **kwargs):
if not self.device.local:
return {'error': 'Unsupported'}
if params is None:
params = []
_LOGGER.debug('%s: Send miio command: %s(%s)', self.device.name_model, method, params)
return await self.device.local.async_send(method, params)
class XEntity(BasicEntity):
CLS: dict[str, Callable] = {}
log = _LOGGER
added = False
_attr_available = False
_attr_should_poll = False
_attr_has_entity_name = True
_miot_service: Optional[MiotService] = None
_miot_property: Optional[MiotProperty] = None
_miot_action: Optional[MiotAction] = None
def __init__(self, device: 'Device', conv: 'BaseConv'):
self.device = device
self.hass = device.hass
self.conv = conv
self.attr = conv.full_name
self.log = device.log
if isinstance(conv, MiotServiceConv):
self.entity_id = conv.service.generate_entity_id(self, conv.domain)
self._attr_name = str(conv.service.friendly_desc)
self._attr_translation_key = conv.service.name
self._miot_service = conv.service
self._miot_property = conv.prop
elif isinstance(conv, MiotPropConv):
self.entity_id = conv.prop.generate_entity_id(self, conv.domain)
self._attr_name = str(conv.prop.friendly_desc)
self._attr_translation_key = conv.prop.friendly_name
self._miot_service = conv.prop.service
self._miot_property = conv.prop
if not conv.prop.readable:
self._attr_available = True
elif isinstance(conv, MiotActionConv):
self.entity_id = device.spec.generate_entity_id(self, conv.action.name, conv.domain)
self._attr_name = str(conv.action.friendly_desc)
self._attr_translation_key = conv.action.friendly_name
self._miot_service = conv.action.service
self._miot_action = conv.action
self._miot_property = conv.prop
self._attr_available = True
else:
self.entity_id = device.spec.generate_entity_id(self, conv.attr, conv.domain)
# self._attr_name = conv.attr.replace('_', '').title()
self._attr_translation_key = conv.attr
if isinstance(conv, InfoConv):
self._attr_available = True
self.listen_attrs = {self.attr} | set(conv.attrs)
if getattr(self, '_attr_name', None):
self._attr_name = self._attr_name.replace(device.name, '').strip()
self._attr_unique_id = f'{device.unique_id}-{convert_unique_id(conv)}'
self._attr_device_info = self.device.hass_device_info
self._attr_extra_state_attributes = {}
self._attr_icon = conv.option.get('icon')
self._attr_device_class = self.custom_config('device_class') or conv.option.get('device_class')
if self._attr_translation_key:
self._attr_translation_key = ( # hassfest
self._attr_translation_key
.replace(':', '-')
.replace('.', '-')
)
cate = self.custom_config('entity_category') or conv.option.get('entity_category')
if isinstance(cate, str):
try:
cate = EntityCategory(cate)
except Exception:
cate = None
if cate:
self._attr_entity_category = cate
else:
cats = {
'configuration_entities': EntityCategory.CONFIG,
'diagnostic_entities': EntityCategory.DIAGNOSTIC,
}
for k, v in cats.items():
names = self.custom_config_list(k) or []
if self._miot_property and self._miot_property.in_list(names):
self._attr_entity_category = v
break
if self._miot_action and self._miot_action.in_list(names):
self._attr_entity_category = v
break
self.on_init()
self.device.add_listener(self.on_device_update)
@cached_property
def model(self):
return self.device.model
@cached_property
def unique_mac(self):
return self.device.info.unique_id
def on_init(self):
"""Run on class init."""
async def async_update(self):
await self.device.update_status()
def on_device_update(self, data: dict, only_info=False):
state_change = False
available = self.device.available
if not only_info:
pass
elif isinstance(self.conv, InfoConv):
self._attr_available = available = True
self._attr_icon = data.get('icon', self._attr_icon)
old_data = self._attr_extra_state_attributes.copy()
old_data.pop('updated_at', None)
if data == old_data:
return
data['updated_at'] = str(self.device.data.get('updated', ''))
self._attr_extra_state_attributes = data
else:
return
if keys := self.listen_attrs & data.keys():
self._attr_available = available
self.set_state(data)
state_change = True
for key in keys:
if conv := self.device.find_converter(key):
self._attr_extra_state_attributes[conv.attr] = self.device.props.get(conv.attr)
if state_change and self.added:
self._async_write_ha_state()
state = data.get(self.attr, data)
_LOGGER.debug('%s: Entity state updated: %s', self.entity_id, state)
def get_state(self) -> dict:
"""Run before entity remove if entity is subclass from RestoreEntity."""
return {}
def set_state(self, data: dict):
"""Run on data from device."""
self._attr_state = data.get(self.attr)
@property
def extra_restore_state_data(self) -> ExtraStoredData | None:
# filter None values
if state := {k: v for k, v in self.get_state().items() if v is not None}:
return RestoredExtraData(state)
return None
async def async_added_to_hass(self) -> None:
self.added = True
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
if call := getattr(self, 'async_get_last_extra_data', None):
data: RestoredExtraData = await call()
if data and self.listen_attrs & data.as_dict().keys():
self.set_state(data.as_dict())
async def async_will_remove_from_hass(self) -> None:
self.device.remove_listener(self.on_device_update)
@cached_property
def customize_keys(self):
keys = []
prop = getattr(self.conv, 'prop', None)
action = getattr(self.conv, 'action', None)
for mod in wildcard_models(self.device.model):
if isinstance(action, MiotAction):
keys.append(f'{mod}:{action.full_name}')
keys.append(f'{mod}:{action.name}')
if isinstance(prop, MiotProperty):
keys.append(f'{mod}:{prop.full_name}')
keys.append(f'{mod}:{prop.name}')
if self.attr and not (prop or action):
keys.append(f'{mod}:{self.conv.attr}')
return keys
def convert_unique_id(conv: 'BaseConv'):
service = getattr(conv, 'service', None)
if isinstance(conv, MiotServiceConv) and isinstance(service, MiotService):
return service.iid
action = getattr(conv, 'action', None)
if isinstance(action, MiotAction):
return action.unique_name
prop = getattr(conv, 'prop', None)
if isinstance(prop, MiotProperty):
return prop.unique_name
return conv.attr
@@ -0,0 +1,139 @@
import logging
import asyncio
from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_USERNAME
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SUPPORTED_DOMAINS
from .xiaomi_cloud import MiotCloud
if TYPE_CHECKING:
from .device import Device
_LOGGER = logging.getLogger(__name__)
class HassEntry:
ALL: dict[str, 'HassEntry'] = {}
cloud: MiotCloud = None
cloud_devices = None
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
self.id = entry.entry_id
self.hass = hass
self.entry = entry
self.adders: dict[str, AddEntitiesCallback] = {}
self.devices: dict[str, 'Device'] = {}
self.mac_to_did = {}
self.did_to_unique = {}
@staticmethod
def init(hass: HomeAssistant, entry: ConfigEntry):
this = HassEntry.ALL.get(entry.entry_id)
if not this:
this = HassEntry(hass, entry)
HassEntry.ALL[entry.entry_id] = this
return this
async def async_unload(self):
ret = all(
await asyncio.gather(
*[
self.hass.config_entries.async_forward_entry_unload(self.entry, domain)
for domain in SUPPORTED_DOMAINS
]
)
)
if ret:
for device in self.devices.values():
await device.async_unload()
HassEntry.ALL.pop(self.entry.entry_id, None)
return ret
def __getattr__(self, item):
return getattr(self.entry, item)
@property
def setup_in_progress(self):
return self.entry.state == ConfigEntryState.SETUP_IN_PROGRESS
def get_config(self, key=None, default=None):
dat = {
**self.entry.data,
**self.entry.options,
}
if self.filter_models:
dat.pop('filter_did', None)
dat.pop('did_list', None)
else:
dat.pop('filter_model', None)
dat.pop('model_list', None)
if key:
return dat.get(key, default)
return dat
@property
def filter_models(self):
data = {
**self.entry.data,
**self.entry.options,
}
if data.get('did_list'):
return False
if data.get('model_list'):
return True
if 'did_list' in data:
return False
if 'model_list' in data:
return True
return data.get('filter_models', False)
async def new_device(self, device_info: dict):
from .device import Device, DeviceInfo
info = DeviceInfo(device_info)
if device := self.devices.get(info.unique_id):
return device
device = Device(info, self)
self.devices[info.unique_id] = device
self.did_to_unique[info.did] = info.unique_id
await device.async_init()
return device
def new_adder(self, domain, adder: AddEntitiesCallback):
self.adders[domain] = adder
_LOGGER.info('New adder: %s', [domain, adder])
for device in self.devices.values():
device.add_entities(domain)
return self
async def get_cloud(self, check=False, login=False):
if not self.cloud:
if not self.get_config(CONF_USERNAME):
return None
self.cloud = await MiotCloud.from_token(self.hass, self.get_config(), login=login)
if check:
await self.cloud.async_check_auth(notify=True)
return self.cloud
async def get_cloud_devices(self):
if isinstance(self.cloud_devices, dict):
return self.cloud_devices
cloud = await self.get_cloud()
if not cloud:
return {}
config = self.get_config()
self.cloud_devices = await cloud.async_get_devices_by_key('did', filters=config) or {}
for did, info in self.cloud_devices.items():
mac = info.get('mac') or did
self.mac_to_did[mac] = did
return self.cloud_devices
async def get_cloud_device(self, did=None, mac=None):
devices = await self.get_cloud_devices()
if mac and not did:
did = self.mac_to_did.get(mac)
if did:
return devices.get(did)
return None
@@ -0,0 +1,297 @@
import time
import logging
import voluptuous as vol
from typing import Tuple
from .utils import (
DeviceException,
is_offline_exception,
rgb_to_int,
int_to_rgb,
)
from .templates import template
from .miot_spec import (MiotSpec, MiotProperty, MiotAction)
from .miio2miot_specs import MIIO_TO_MIOT_SPECS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
class Miio2MiotHelper:
def __init__(self, hass, config: dict, miot_spec: MiotSpec, from_model=None):
self.hass = hass
if ext := config.get('extend_model'):
if m2m := Miio2MiotHelper.from_model(hass, ext, miot_spec):
sps = {**m2m.config.get('miio_specs', {})}
sps.update(config.get('miio_specs', {}))
config = {**m2m.config, **config, 'miio_specs': sps}
self.config = config
self.miot_spec = miot_spec
self.specs = config.get('miio_specs', {})
self.model = from_model or config.get('model', None)
self.miio_props = []
for k, v in self.specs.items():
if p := v.get('prop'):
self.miio_props.append(p)
self.extend_miio_props(config.get('miio_props', []))
self.miio_props_values = {}
@staticmethod
def from_model(hass, model, miot_spec, from_model=None):
if not from_model:
from_model = model
cfg = MIIO_TO_MIOT_SPECS.get(model) or {}
if isinstance(cfg, str):
return Miio2MiotHelper.from_model(hass, cfg, miot_spec, from_model)
if cfg:
cfg.setdefault('model', model)
return Miio2MiotHelper(hass, cfg, miot_spec, from_model)
return None
def extend_miio_props(self, props: list):
self.miio_props.extend(props)
self.miio_props = list(dict(zip(self.miio_props, self.miio_props)).keys())
return self.miio_props
async def async_get_miio_props(self, device):
dic = {}
if not self.config.get('without_props'):
try:
num = int(self.config.get('chunk_properties'))
except (TypeError, ValueError):
num = None
try:
vls = await device.async_get_prop(self.miio_props, max_properties=num)
dic.update(dict(zip(self.miio_props, vls)))
except (DeviceException, OSError) as exc:
if is_offline_exception(exc):
raise exc
_LOGGER.error('%s: Got MiioException: %s while get_prop(%s)', self.model, exc, self.miio_props)
except TypeError:
_LOGGER.error('%s: Got TypeError while get_prop(%s)', self.model, self.miio_props, exc_info=True)
if cls := self.config.get('miio_commands'):
for c in cls:
if dly := c.get('delay', 0):
time.sleep(dly)
pms = c.get('params', [])
try:
vls = await device.async_send(c['method'], pms)
except (DeviceException, OSError, TypeError, UnboundLocalError) as exc:
if is_offline_exception(exc):
raise exc
if not c.get('ignore_error'):
_LOGGER.error('%s: Got MiioException: %s while %s(%s)', self.model, exc, c['method'], pms)
continue
kls = c.get('values', [])
if kls is True:
kls = c.get('params', [])
if tpl := c.get('template'):
tpl = template(tpl, self.hass)
pdt = tpl.async_render({'results': vls})
if isinstance(pdt, dict):
dic.update(pdt)
elif kls:
if isinstance(vls, dict):
vls = list(vls.values())
i = 0
for k in kls:
if i >= len(vls):
break
dic[k] = vls[i]
i += 1
if tpl := self.config.get('miio_template'):
tpl = template(tpl, self.hass)
pdt = tpl.async_render({'props': dic})
if isinstance(pdt, dict):
dic.update(pdt)
self.miio_props_values = dic
_LOGGER.debug('%s: Got miio props for miot: %s', self.model, dic)
return dic
async def async_get_miot_props(self, device, mapping: dict = None):
if mapping is None:
mapping = device.mapping or {}
rls = []
if dic := await self.async_get_miio_props(device):
for k, v in mapping.items():
u = MiotSpec.unique_prop(v, valid=True)
c = self.specs.get(u, {})
p = c.get('prop')
if not p:
continue
val = dic.get(p)
prop = self.miot_spec.specs.get(u)
if prop and isinstance(prop, MiotProperty):
mph = MiioPropertyHelper(prop)
fmt = c.get('format')
try:
if tpl := c.get('template', {}):
tpl = template(tpl, self.hass)
val = tpl.async_render({
'value': val,
'props': dic,
'dict': c.get('dict', {}),
'min': prop.range_min(),
'max': prop.range_max(),
'step': prop.range_step(),
'description': prop.list_description(val) if prop.value_list else None,
})
elif fmt and hasattr(mph, fmt):
val = getattr(mph, fmt)(val)
elif d := c.get('dict', {}):
val = d.get(val, c.get('default', val))
elif prop.format in ['bool']:
val = cv.boolean(val)
elif r := c.get('value_ratio'):
val = round(float(val) * float(r), 3)
elif prop.format in ['float']:
val = round(float(val), 4)
elif prop.is_integer:
val = int(val)
except (TypeError, ValueError, vol.Invalid):
val = dic.get(p)
rls.append({
**v,
'code': 0,
'value': val,
})
return rls
def has_setter(self, siid, piid=None, aiid=None):
key = MiotSpec.unique_prop(siid=siid, piid=piid, aiid=aiid)
ret = self.specs.get(key, {}).get('setter')
return ret
async def async_set_property(self, device, siid, piid, value):
key = MiotSpec.unique_prop(siid=siid, piid=piid)
cfg = self.specs.get(key, {})
setter = cfg.get('setter')
if setter is True:
setter = None
if prop := cfg.get('prop'):
setter = f'set_{prop}'
pms = [value]
prop = self.miot_spec.specs.get(key)
if prop and isinstance(prop, MiotProperty):
mph = MiioPropertyHelper(prop, reverse=True)
fmt = cfg.get('format')
if tpl := cfg.get('set_template'):
tpl = template(tpl, self.hass)
pms = tpl.async_render({
'value': value,
'props': self.miio_props_values,
'dict': cfg.get('dict', {}),
'min': prop.range_min(),
'max': prop.range_max(),
'step': prop.range_step(),
'description': prop.list_description(value) if prop.value_list else None,
}) or []
if isinstance(pms, dict) and 'method' in pms:
setter = pms.get('method', setter)
pms = pms.get('params', [])
elif fmt and hasattr(mph, fmt):
pms = [getattr(mph, fmt)(value)]
elif d := cfg.get('dict', {}):
for dk, dv in d.items():
if dv == value:
pms = [dk]
break
pms = cv.ensure_list(pms)
if not setter:
_LOGGER.warning('%s: Set miio prop via miot failed: %s', self.model, [key, setter, cfg])
return None
_LOGGER.info('%s: Set miio prop via miot: %s', self.model, [key, setter, pms])
ret = await device.async_send(setter, pms) or ['']
iok = ret == ['ok']
if self.config.get('ignore_result'):
iok = ret or isinstance(ret, list)
cbk = cfg.get('set_callback')
if iok and cbk:
cbk(prop=cfg.get('prop'), config=cfg, setter=setter, params=pms, props=self.miio_props_values)
return {
'code': 0 if iok else 1,
'siid': siid,
'piid': piid,
'result': ret,
}
async def async_call_action(self, device, siid, aiid, params):
key = MiotSpec.unique_prop(siid=siid, aiid=aiid)
cfg = self.specs.get(key, {})
setter = cfg.get('setter')
pms = cv.ensure_list(params)
act = self.miot_spec.specs.get(key)
if act and isinstance(act, MiotAction):
if tpl := cfg.get('set_template'):
tpl = template(tpl, self.hass)
pms = tpl.async_render({
'params': pms,
'props': self.miio_props_values,
}) or []
if isinstance(pms, dict) and 'method' in pms:
setter = pms.get('method', setter)
pms = pms.get('params', [])
pms = cv.ensure_list(pms)
if not setter:
_LOGGER.warning('%s: Call miio method via miot action failed: %s', self.model, [key, setter, cfg])
return None
_LOGGER.info('%s: Call miio method via miot action: %s', self.model, [key, setter, pms])
ret = await device.async_send(setter, pms) or ['']
iok = ret == ['ok']
if self.config.get('ignore_result'):
iok = ret or isinstance(ret, list)
return {
'code': 0 if iok else 1,
'siid': siid,
'aiid': aiid,
'result': ret,
}
def entity_attrs(self):
adt = {}
eas = self.config.get('entity_attrs', [])
if isinstance(eas, list):
eas = {
k: k
for k in eas
}
for k, p in eas.items():
v = self.miio_props_values.get(p)
if v is not None:
adt[k] = v
return adt
def only_miio_props(self, props: list):
rls = []
for p in props:
rls.append(self.miio_props_values.get(p))
return rls
class MiioPropertyHelper:
def __init__(self, prop: MiotProperty, reverse=False):
self.property = prop
self.reverse = reverse
def onoff(self, value):
if self.reverse:
return 'on' if value else 'off'
return value is True or 'on' == f'{value}'.lower()
def rgb(self, value):
is_tuple = isinstance(value, Tuple)
if self.reverse:
if not is_tuple:
value = int_to_rgb(value)
value = list(value)
elif is_tuple:
return rgb_to_int(value)
return value
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,353 @@
# https://github.com/AlexxIT/XiaomiGateway3/blob/02aaddc3c16f63df116a95578f44d5b0da1c4f2c/custom_components/xiaomi_gateway3/core/mini_miio.py
import asyncio
import hashlib
import json
import logging
import random
import socket
import time
from asyncio import DatagramProtocol, Future
from asyncio.protocols import BaseProtocol
from asyncio.transports import DatagramTransport
from typing import Union
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
_LOGGER = logging.getLogger(__package__ + ".miio")
HELLO = bytes.fromhex(
"21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
)
class BasemiIO:
"""A simple class that implements the miIO protocol."""
device_id = None
delta_ts = None
debug = False
def __init__(self, host: str, token: str, timeout: float = 3):
self.addr = (host, 54321)
self.token = bytes.fromhex(token)
self.timeout = timeout
key = hashlib.md5(self.token).digest()
iv = hashlib.md5(key + self.token).digest()
self.cipher = Cipher(
algorithms.AES(key), modes.CBC(iv), backend=default_backend()
)
def _encrypt(self, plaintext: bytes):
padder = padding.PKCS7(128).padder()
padded_plaintext = padder.update(plaintext) + padder.finalize()
encryptor = self.cipher.encryptor()
return encryptor.update(padded_plaintext) + encryptor.finalize()
def _decrypt(self, ciphertext: bytes):
decryptor = self.cipher.decryptor()
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(padded_plaintext) + unpadder.finalize()
def _pack_raw(self, msg_id: int, method: str, params: Union[dict, list] = None):
# latest zero unnecessary
payload = (
json.dumps(
{"id": msg_id, "method": method, "params": params or []},
separators=(",", ":"),
).encode()
+ b"\x00"
)
data = self._encrypt(payload)
raw = b"\x21\x31"
raw += (32 + len(data)).to_bytes(2, "big") # total length
raw += b"\x00\x00\x00\x00" # unknow
raw += self.device_id.to_bytes(4, "big")
raw += int(time.time() - self.delta_ts).to_bytes(4, "big")
raw += hashlib.md5(raw + self.token + data).digest()
raw += data
assert len(raw) < 1024, "Exceeded message size"
return raw
def _unpack_raw(self, raw: bytes):
assert raw[:2] == b"\x21\x31"
# length = int.from_bytes(raw[2:4], 'big')
# unknown = raw[4:8]
# device_id = int.from_bytes(raw[8:12], 'big')
# ts = int.from_bytes(raw[12:16], 'big')
# checksum = raw[16:32]
return self._decrypt(raw[32:])
class SyncMiIO(BasemiIO):
"""Synchronous miIO protocol."""
def ping(self, sock: socket) -> bool:
"""Returns `true` if the connection to the miio device is working. The
token is not verified at this stage.
"""
try:
sock.sendto(HELLO, self.addr)
raw = sock.recv(1024)
if raw[:2] == b"\x21\x31":
self.device_id = int.from_bytes(raw[8:12], "big")
self.delta_ts = time.time() - int.from_bytes(raw[12:16], "big")
return True
except Exception:
pass
return False
def send(self, method: str, params: Union[dict, list] = None):
"""Send command to miIO device and get result from it. Params can be
dict or list depend on command.
"""
pings = 0
for times in range(1, 4):
try:
# create socket every time for reset connection, because we can
# reseive answer on previous request or request from another
# thread
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(self.timeout)
# need device_id for send command, can get it from ping cmd
if self.delta_ts is None and not self.ping(sock):
pings += 1
continue
# pack each time for new message id
msg_id = random.randint(100000000, 999999999)
raw_send = self._pack_raw(msg_id, method, params)
t = time.monotonic()
sock.sendto(raw_send, self.addr)
# can receive more than 1024 bytes (1056 approximate maximum)
raw_recv = sock.recv(10240)
t = time.monotonic() - t
data = self._unpack_raw(raw_recv).rstrip(b"\x00")
if data == b"":
# mgl03 fw 1.4.6_0012 without Internet respond on miIO.info
# command with empty answer
data = {"result": ""}
break
data = json.loads(data)
# check if we received response for our cmd
if data["id"] == msg_id:
break
_LOGGER.debug(f"{self.addr[0]} | wrong ID")
except socket.timeout:
_LOGGER.debug(f"{self.addr[0]} | timeout {times}")
except Exception as e:
_LOGGER.debug(f"{self.addr[0]}", exc_info=e)
# init ping again
self.delta_ts = None
else:
_LOGGER.debug(
f"{self.addr[0]} | Device offline"
if pings >= 2
else f"{self.addr[0]} | Can't send {method} {params}"
)
return None
if self.debug:
_LOGGER.debug(
f"{self.addr[0]} | Send {method} {len(raw_send)}B, "
f"recv {len(raw_recv)}B in {t:.1f} sec and {times} try"
)
if "result" in data:
return data["result"]
else:
_LOGGER.debug(f"{self.addr[0]} | {data}")
return None
def send_bulk(self, method: str, params: list):
"""Sends a command with a large number of parameters. Splits into
multiple requests when the size of one request is exceeded.
"""
try:
result = []
# Chunk of 15 is seems like the best size. Because request should
# be lower than 1024 and response should be not more than 1056.
# {'did':'1234567890','siid': 2,'piid': 1,'value': False,'code': 0}
for i in range(0, len(params), 15):
result += self.send(method, params[i : i + 15])
return result
except Exception:
return None
def info(self) -> Union[dict, str, None]:
"""Get info about miIO device.
Response dict - device ok, token ok
Response empty string - device ok, token ok (mgl03 on fw 1.4.6_0012
without cloud connection)
Response None, device_id not None - device ok, token wrong
Response None, device_id None - device offline
"""
return self.send("miIO.info")
# noinspection PyUnusedLocal
class AsyncSocket(DatagramProtocol):
timeout = 0
transport: DatagramTransport = None
response: Future = None
def __init__(self):
self.response = asyncio.get_event_loop().create_future()
def connection_made(self, transport: DatagramTransport):
# _LOGGER.debug(f"connection_made {transport}")
self.transport = transport
def datagram_received(self, data: bytes, addr):
# _LOGGER.debug(f"datagram_received {data}")
self.response.set_result(data)
def settimeout(self, value: int):
self.timeout = value
def sendto(self, data: bytes):
self.transport.sendto(data)
def close(self):
if not self.transport:
return
try:
self.transport.close()
except Exception as e:
_LOGGER.error("Error when closing async socket", exc_info=e)
async def connect(self, addr: tuple[str, int]):
coro = asyncio.get_event_loop().create_datagram_endpoint(
lambda: self, remote_addr=addr
)
if self.timeout:
await asyncio.wait_for(coro, self.timeout)
else:
await coro
async def recv(self, *args):
self.response = asyncio.get_event_loop().create_future()
if self.timeout:
return await asyncio.wait_for(self.response, self.timeout)
return await self.response
# noinspection PyMethodMayBeStatic,PyTypeChecker
class AsyncMiIO(BasemiIO, BaseProtocol):
async def ping(self, sock: AsyncSocket) -> bool:
"""Returns `true` if the connection to the miio device is working. The
token is not verified at this stage.
"""
try:
sock.sendto(HELLO)
raw = await sock.recv(1024)
if raw[:2] == b"\x21\x31":
self.device_id = int.from_bytes(raw[8:12], "big")
self.delta_ts = time.time() - int.from_bytes(raw[12:16], "big")
return True
except Exception:
pass
return False
async def send(self, method: str, params: Union[dict, list] = None, tries=3):
"""Send command to miIO device and get result from it. Params can be
dict or list depend on command.
Possible results:
- None - device offline
- {} (empty dict) - device online but can't response on cmd
- {'id':123,'result':...} - device answered on cmd with good result
- {'id':123,'error':...}
"""
offline = False
for _ in range(0, tries):
sock = AsyncSocket()
sock.settimeout(self.timeout)
try:
# create socket every time for reset connection, because we can
# reseive answer on previous request or request from another
# thread
await sock.connect(self.addr)
# need device_id for send command, can get it from ping cmd
if self.delta_ts is None and not await self.ping(sock):
# device doesn't answered on ping
offline = True
continue
# pack each time for new message id
msg_id = random.randint(100000000, 999999999)
raw_send = self._pack_raw(msg_id, method, params)
sock.sendto(raw_send)
# can receive more than 1024 bytes (1056 approximate maximum)
raw_recv = await sock.recv(10240)
data = self._unpack_raw(raw_recv).rstrip(b"\x00")
if data == b"":
# mgl03 fw 1.4.6_0012 without Internet respond on miIO.info
# command with empty answer
continue
data = json.loads(data)
# check if we received response for our cmd
if data["id"] != msg_id:
_LOGGER.debug(f"{self.addr[0]} | wrong answer ID")
continue
return data
except (asyncio.TimeoutError, OSError):
# OSError: [Errno 101] Network unreachable
pass
except Exception as e:
_LOGGER.debug(f"{self.addr[0]} | {method}", exc_info=e)
finally:
sock.close()
# init ping again
self.delta_ts = None
if offline:
_LOGGER.debug(f"{self.addr[0]} | Device offline")
return None
_LOGGER.debug(f"{self.addr[0]} | No answer on {method} {params}")
return {}
async def send_bulk(self, method: str, params: list, chunk: int = 0) -> list:
"""Sends a command with a large number of parameters. Splits into
multiple requests when the size of one request is exceeded.
"""
if not chunk:
chunk = 15
try:
result = []
for i in range(0, len(params), chunk):
resp = await self.send(method, params[i : i + chunk])
result += resp["result"]
return result
except Exception:
return None
async def info(self, tries: int = 3) -> dict | None:
"""Get info about miIO device."""
resp = await self.send("miIO.info", tries=tries)
return resp.get("result") if resp else resp
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
from homeassistant.helpers.template import Template
CUSTOM_TEMPLATES = {
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E7%89%99%E5%88%B7%E4%BA%8B%E4%BB%B6
'ble_toothbrush_events': "{%- set dat = props.get('event.16') | default('{}',true) | from_json %}"
"{%- set tim = dat.timestamp | default(0,true) | timestamp_local %}"
"{%- set val = dat.get('value',[]).0 | default('0000') %}"
"{%- set typ = val[0:2] | int(0,16) %}"
"{%- set num = val[2:4] | int(0,16) %}"
"{{ {"
"'event': 'start',"
"'counter': num,"
"'timestamp': tim,"
"} if typ == 0 else {"
"'event': 'finish',"
"'score': num,"
"'timestamp': tim,"
"} }}",
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E9%94%81%E4%BA%8B%E4%BB%B6
'ble_lock_events': "{%- set mark_data = props.get('event.6') | default('{}',true) | from_json %}"
"{%- set mark = mark_data.get('value',[]).0 | default('') %}"
"{%- set door_data = props.get('event.7') | default('{}',true) | from_json %}"
"{%- set door = (door_data.get('value',[]).0 | default(''))[:2] | int(-1,16) %}"
"{%- set fang_data = props.get('event.8') | default('{}',true) | from_json %}"
"{%- set fang = (fang_data.get('value',[]).0 | default(''))[:2] | int(-1,16) %}"
"{%- set lock_data = props.get('event.11') | default('{}',true) | from_json %}"
"{%- set lock = lock_data.get('value',[]).0 | default('') %}"
"{%- set lock_action = lock[:2] | int(-1,16) % 16 %}"
"{%- set lock_method = lock[:2] | int(-1,16) // 16 %}"
"{%- set lock_key = lock[2:10] %}"
"{%- set lock_fault = lock_key if lock_key[:4] == 'c0de' else None %}"
"{%- set key_id = (0).from_bytes((0).to_bytes(0,'little').fromhex(lock_key),'little') %}"
"{%- set key_types = {'00000000':'admin','ffffffff':'unknown','deadbeef':'invalid'} %}"
"{%- set key_results = ['success','fail','timeout','blurry','less','dry','wet'] %}"
"{%- set door_states = ['open','close','close_timeout','knock','breaking','stuck'] %}"
"{%- set lock_actions = ['outside_unlock','lock','anti_lock_on','anti_lock_off',"
"'inside_unlock','lock_inside','child_lock_on','child_lock_off','lock_outside'] %}"
"{%- set lock_methods = ['bluetooth','password','biological','key','turntable','nfc',"
"'one_time_password','two_step_verification','coercion','homekit','manual','automatic'] %}"
"{{ {"
"'fingerprint_id': key_types[mark[:8]] | default(mark[:8]),"
"'fingerprint_result': key_results[mark[8:10] | int(-1,16)] | default('unknown'),"
"'door_state': door_states[door] | default('unknown'),"
"'armed_state': true if fang > 0 else false,"
"'lock_action': lock_actions[lock_action] | default('unknown'),"
"'lock_method': lock_methods[lock_method] | default('unknown'),"
"'lock_key': key_types[lock_key] | default(lock_key),"
"'lock_key_id': key_id,"
"'lock_fault': lock_fault | default('none',true),"
"'lock_data': lock,"
"'timestamp': lock_data.timestamp | default(0,true) | timestamp_local,"
"} }}",
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E7%83%9F%E9%9B%BE%E5%B1%9E%E6%80%A7
'ble_sensor_smoke': "{%- set val = props.get('prop.4117','00') | int(0,16) %}"
"{{ {"
"'smoke_status': val == 1,"
"} }}",
'chuangmi_plug_v3_power_cost': "{%- set val = (result.0 | default({})).get('value',{}) %}"
"{%- set day = now().day %}"
"{{ {"
"'today': val.pc | default(0),"
"'today_duration': val.pc_time | default(0),"
"'month': result[:day] | sum(attribute='value.pc'),"
"'month_duration': result[:day] | sum(attribute='value.pc_time'),"
"} }}",
'lock_event_7_template': "{%- set val = (result.0 | default({})).get('value','[-1]') %}"
"{%- set val = (val | from_json).0 | string %}"
"{%- set evt = val[:2] | int(-1,16) %}"
"{%- set els = ['open','close','close_timeout',"
"'knock','breaking','stuck','unknown'] %}"
"{{ {"
"'door_event': evt,"
"'door_state': els[evt] | default('unknown'),"
"} }}",
'lock_event_11_template': "{%- set val = (result.0 | default({})).get('value','[-1]') %}"
"{%- set val = (val | from_json).0 | string %}"
"{%- set evt = val[:2] | int(-1,16) % 16 %}"
"{%- set how = val[:2] | int(-1,16) // 16 %}"
"{%- set key = (0).from_bytes((0).to_bytes(0,'little')"
".fromhex(val[2:10]), 'little') %}"
"{%- set els = ['outside_unlock','lock','anti_lock_on','anti_lock_off',"
"'inside_unlock','lock_inside','child_lock_on','child_lock_off','unknown'] %}"
"{%- set mls = ['bluetooth','password','biological','key','turntable',"
"'nfc','one_time_password','two_step_verification','coercion','homekit',"
"'manual','automatic','unknown'] %}"
"{{ {"
"'lock_event': evt,"
"'lock_state': els[evt] | default('unknown'),"
"'method_id': how,"
"'method': mls[how] | default('unknown'),"
"'key_id': key,"
"} }}",
'lumi_acpartner_electric_power': "{%- set val = props.get('prop.load_power') or props.get('prop.ac_power',0) %}"
"{{ {"
"'electric_power': val | default(0,true) | round(2),"
"} }}",
'lumi_acpartner_miio_status': "{%- set model = results[0] | default('') %}"
"{%- set state = results[1] | default('') %}"
"{{ {"
"'power': state[2:3] | int(0) == 1,"
"'mode': [3,1,0,2,4][state[3:4] | int(2)],"
"'fan_level': [1,2,3,0][state[4:5] | int(3)],"
"'vertical_swing': state[5:6] in ['0','C'],"
"'target_temperature': state[6:8] | int(0,16),"
"'load_power': results[2] | default(0) | float | round(2),"
"} }}",
'micloud_statistics_power_cost': "{%- set dat = namespace(today=0,month=0) %}"
"{%- set tim = now() %}"
"{%- set stm = tim - timedelta(minutes=tim.minute,seconds=tim.second) %}"
"{%- set tod = stm - timedelta(hours=tim.hour) %}"
"{%- set stm = tod - timedelta(days=tim.day-1) %}"
"{%- set tod = tod | as_timestamp | int(0) %}"
"{%- set stm = stm | as_timestamp | int(0) %}"
"{%- for d in (result or []) %}"
"{%- set t = d.time | default(0) | int(0) %}"
"{%- if t >= stm %}"
"{%- set v = (d.value | default('[]') | string | from_json) or [] %}"
"{%- set n = v[0] | default(0) %}"
"{%- if t >= tod %}"
"{%- set dat.today = n %}"
"{%- endif %}"
"{%- set dat.month = dat.month + n %}"
"{%- endif %}"
"{%- endfor %}"
"{{ {"
"'power_cost_today': dat.today | round(3),"
"'power_cost_month': dat.month | round(3),"
"} }}",
'midr_rv_mirror_cloud_props': "{%- set sta = props.get('prop.Status',0) | int %}"
"{%- set pos = props.get('prop.Position','{}') | from_json %}"
"{%- set tim = pos.get('up_time_stamp',0) | int %}"
"{{ {"
"'prop.status': sta,"
"'prop.position': pos,"
"'prop.update_at': (tim / 1000) | timestamp_local,"
"} }}",
'mxiang_cateye_cloud_props': "{{ {"
"'battery_level': props.get('prop.battery_level','0') | from_json | int,"
"'is_can_open_video': props.get('prop.is_can_open_video','0') | from_json | int,"
"} }}",
'mxiang_cateye_human_visit_details': "{%- set val = (result.0 | default({})).get('value','{}') %}"
"{%- set val = val | from_json %}"
"{{ {"
"'motion_video_time': val.get('visitTime',0) | timestamp_local,"
"'motion_video_type': val.get('action'),"
"'motion_video_latest': val,"
"'_entity_attrs': True,"
"} }}",
'yeelink_bhf_light_v2_fan_levels': "{%- set val = ('00000' ~ value)[-5:] %}"
"{%- set mds = {"
"'drying_cloth': val[0],"
"'coolwind': val[1],"
"'drying': val[2],"
"'venting': val[3],"
"'warmwind': val[4],"
"} %}"
"{{ [1,2,3][mds[props.bh_mode] | default(0) | int(0)] | default(3) }}",
'yeelink_bhf_light_v5_fan_levels': "{%- set val = ('000' ~ value)[-3:] %}"
"{%- set mds = {"
"'warmwind': val[0],"
"'coolwind': val[1],"
"'venting': val[2],"
"} %}"
"{{ [1,1,3,3][mds[props.bh_mode] | default(0) | int(0)] | default(1) }}",
'yeelink_bhf_light_v5_miio_props': "{%- set val = ('000' ~ props.fan_speed_idx)[-3:] %}"
"{{ {"
"'warmwind_gear': val[0],"
"'coolwind_gear': val[1],"
"'venting_gear': val[2],"
"} }}",
'zimi_powerstrip_v2_power_cost': "{%- set val = (result.0 | default({})).get('value','[0]') %}"
"{%- set day = now().day %}"
"{%- set vls = (val | from_json)[0-day:] %}"
"{%- set dat = namespace(today=0,month=0) %}"
"{%- for v in vls %}"
"{%- set v = (v | string).split(',') %}"
"{%- if v[0] | default(0) | int(0) > 86400 %}"
"{%- set dat.today = v[1] | default(0) | round(3) %}"
"{%- set dat.month = dat.month + dat.today %}"
"{%- endif %}"
"{%- endfor %}"
"{{ {"
"'today': dat.today,"
"'month': dat.month | round(3),"
"} }}",
}
def template(value, hass):
if value is None:
raise ValueError('template value is None')
if isinstance(value, (list, dict, Template)):
raise TypeError('template value should be a string')
value = CUSTOM_TEMPLATES.get(value, value)
template_value = Template(str(value), hass)
template_value.ensure_valid()
return template_value
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,369 @@
import os
import re
import json
import locale
import aiohttp
import asyncio
import tzlocal
import logging
import fnmatch
import voluptuous as vol
from typing import Type, Tuple, Optional, Callable, Set
from functools import wraps
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.util import language as language_util
from homeassistant.util.dt import DEFAULT_TIME_ZONE, get_time_zone
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, DEVICE_CUSTOMIZES, DATA_CUSTOMIZE
from .translation_languages import TRANSLATION_LANGUAGES
def get_value(obj, key, def_value=None, sep='.'):
keys = f'{key}'.split(sep)
result = obj
for k in keys:
if result is None:
return None
if isinstance(result, dict):
result = result.get(k, def_value)
elif isinstance(result, (list, tuple)):
try:
result = result[int(k)]
except Exception:
result = def_value
return result
def get_customize_via_model(model, key=None, default=None):
cfg = {}
for m in wildcard_models(model):
cus = DEVICE_CUSTOMIZES.get(m) or {}
if key is not None and key not in cus:
continue
if cus:
cfg = {**cus, **cfg}
return cfg if key is None else cfg.get(key, default)
def get_customize_via_entity(entity, key=None, default=None):
if key is None:
default = {}
if not isinstance(entity, Entity):
return default
cfg = {}
if entity.hass and entity.entity_id:
cfg = {
**(entity.hass.data[DATA_CUSTOMIZE].get(entity.entity_id) or {}),
**(entity.hass.data[DOMAIN].get(DATA_CUSTOMIZE, {}).get(entity.entity_id) or {}),
}
if key is not None and key in cfg:
return cfg.get(key)
mls = []
if model := getattr(entity, 'model', None):
if hasattr(entity, 'customize_keys'):
mls.extend(entity.customize_keys)
mls.append(model)
for mod in mls:
cus = get_customize_via_model(mod)
cfg = {**cus, **cfg}
return cfg if key is None else cfg.get(key, default)
class CustomConfigHelper:
def custom_config(self, key=None, default=None):
raise NotImplementedError
def custom_config_bool(self, key=None, default=None):
val = self.custom_config(key, default)
try:
val = cv.boolean(val)
except vol.Invalid:
val = default
return val
def custom_config_number(self, key=None, default=None):
num = default
val = self.custom_config(key)
if val is not None:
try:
num = float(f'{val}')
except (TypeError, ValueError):
num = default
return num
def custom_config_integer(self, key=None, default=None):
num = self.custom_config_number(key, default)
if num is not None:
num = int(num)
return num
def custom_config_list(self, key=None, default=None):
lst = self.custom_config(key)
if lst is None:
return default
if isinstance(lst, (int, float, bool)):
lst = [lst]
elif not isinstance(lst, list):
lst = f'{lst}'.split(',')
lst = list(map(lambda x: x.strip(), lst))
return lst
def custom_config_json(self, key=None, default=None):
dic = self.custom_config(key)
if dic:
if not isinstance(dic, (dict, list)):
try:
dic = json.loads(dic or '{}')
except (TypeError, ValueError):
dic = None
if isinstance(dic, (dict, list)):
return dic
return default
def get_manifest(field=None, default=None):
manifest = {}
with open(f'{os.path.dirname(__file__)}/../manifest.json') as fil:
manifest = json.load(fil) or {}
return manifest.get(field, default) if field else manifest
async def async_get_manifest(hass: HomeAssistant, field=None, default=None):
return await hass.async_add_executor_job(get_manifest, field, default)
def local_zone(hass=None):
try:
if isinstance(hass, HomeAssistant):
return get_time_zone(hass.config.time_zone)
return tzlocal.get_localzone()
except KeyError:
pass
return DEFAULT_TIME_ZONE
def in_china(hass=None):
if isinstance(hass, HomeAssistant):
if hass.config.time_zone in ['Asia/Shanghai', 'Asia/Hong_Kong']:
return True
try:
return f'{locale.getdefaultlocale()[0]}'[:3] == 'zh_'
except (KeyError, Exception):
pass
return False
def wildcard_models(model):
if not model:
return []
if ':' in model:
return [model]
wil = re.sub(r'\.[^.]+$', '.*', model)
return [
model,
wil,
re.sub(r'^[^.]+\.', '*.', wil),
'*',
]
def convert_globs_to_pattern(globs: list[str] | None):
"""Convert a list of globs to a re pattern list."""
if not globs:
return None
translated_patterns = [
pattern for glob in set(globs) if (pattern := fnmatch.translate(str(glob)))
]
if not translated_patterns:
return None
inner = '|'.join(translated_patterns)
combined = f'(?:{inner})'
return re.compile(combined)
def get_translation(key, keys=None):
dic = get_translations(*(keys or []))
val = dic.get(key, key)
if isinstance(val, str):
return val
return key
def get_translations(*keys):
dic = {
**TRANSLATION_LANGUAGES,
**(TRANSLATION_LANGUAGES.get('_globals', {})),
}
for k in keys:
tls = TRANSLATION_LANGUAGES.get(k)
if not isinstance(tls, dict):
continue
dic.update(tls)
return dic
def get_translation_langs(hass: HomeAssistant, langs=None):
lang = hass.config.language
if not langs:
return [lang]
if 'en' not in langs:
langs.append('en')
return language_util.matches(lang, langs)
class DeviceException(Exception):
"""Exception wrapping any communication errors with the device."""
def is_offline_exception(exc):
err = f'{exc}'
ret = 'Unable to discover the device' in err
if not ret:
ret = 'No response from the device' in err
if not ret:
ret = 'OSError: [Errno 64] Host is down' in err
if not ret:
ret = 'OSError: [Errno 65] No route to host' in err
return ret
def update_attrs_with_suffix(attrs, new_dict):
updated_attrs = {}
for key, value in new_dict.items():
if key in attrs:
suffix = 2
while f"{key}_{suffix}" in attrs:
suffix += 1
updated_key = f"{key}_{suffix}"
else:
updated_key = key
updated_attrs[updated_key] = value
attrs.update(updated_attrs)
def logger_filter(record):
def sub(msg):
return re.sub(r'[\w/+-]{30,}', '***', msg)
if isinstance(record, str):
return sub(record)
if isinstance(record, logging.LogRecord):
record.msg = sub(record.getMessage())
return True
return record
def int_to_rgb(x: int) -> Tuple[int, int, int]:
"""Return a RGB tuple from integer."""
red = (x >> 16) & 0xFF
green = (x >> 8) & 0xFF
blue = x & 0xFF
return red, green, blue
def rgb_to_int(x: Tuple[int, int, int]) -> int:
"""Return an integer from RGB tuple."""
return int(x[0] << 16 | x[1] << 8 | x[2])
def aiohttp_retry(
max_retries: int = 3,
backoff_factor: float = 2.0,
retry_on_status: Optional[Set[int]] = None,
exceptions: Tuple[Type[BaseException], ...] = (aiohttp.ClientError, asyncio.TimeoutError),
logger=None,
):
if retry_on_status is None:
retry_on_status = {500, 502, 503, 504}
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return await func(*args, **kwargs)
except exceptions as exc:
if logger:
logger.warning(
"Error while executing request from %s, attempt: %s",
func.__name__, attempt, exc_info=True,
)
last_exception = exc
if attempt == max_retries:
break
is_retryable_status = isinstance(exc, aiohttp.ClientConnectorDNSError)
if isinstance(exc, aiohttp.ClientResponseError) and exc.status in retry_on_status:
is_retryable_status = True
if not is_retryable_status:
break
delay = backoff_factor * (2 ** attempt)
await asyncio.sleep(delay)
if last_exception:
raise last_exception
return wrapper
return decorator
async def async_analytics_track_event(hass: HomeAssistant, event, action, label, value=0, **kwargs):
pms = {
'model': label,
'event': event,
'action': action,
'label': label,
'value': value,
'tz': hass.config.time_zone,
'ver': await async_get_manifest(hass, 'version', ''),
**kwargs,
}
url = 'https://hacc.miot-spec.com/api/track'
try:
session = async_get_clientsession(hass)
return await session.post(url, data=pms, timeout=3)
except (Exception, ValueError):
return False
class RC4:
_idx = 0
_jdx = 0
_ksa: list
def __init__(self, pwd):
self.init_key(pwd)
def init_key(self, pwd):
cnt = len(pwd)
ksa = list(range(256))
j = 0
for i in range(256):
j = (j + ksa[i] + pwd[i % cnt]) & 255
ksa[i], ksa[j] = ksa[j], ksa[i]
self._ksa = ksa
self._idx = 0
self._jdx = 0
return self
def crypt(self, data):
if isinstance(data, str):
data = data.encode()
ksa = self._ksa
i = self._idx
j = self._jdx
out = []
for byt in data:
i = (i + 1) & 255
j = (j + ksa[i]) & 255
ksa[i], ksa[j] = ksa[j], ksa[i]
out.append(byt ^ ksa[(ksa[i] + ksa[j]) & 255])
self._idx = i
self._jdx = j
self._ksa = ksa
return bytearray(out)
def init1024(self):
self.crypt(bytes(1024))
return self
@@ -0,0 +1,954 @@
import logging
import aiohttp
import asyncio
import json
import time
import string
import random
import base64
import locale
import hashlib
import micloud
import requests
from datetime import datetime
from functools import partial
from typing import Optional
from urllib import parse
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.helpers.storage import Store
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.components import persistent_notification
from .const import DOMAIN, CONF_XIAOMI_CLOUD
from .utils import RC4, aiohttp_retry, local_zone, logger_filter
from micloud import miutils
from micloud.micloudexception import MiCloudException
try:
from micloud.micloudexception import MiCloudAccessDenied
except (ModuleNotFoundError, ImportError):
class MiCloudAccessDenied(MiCloudException):
""" micloud==0.4 """
_LOGGER = logging.getLogger(__name__)
_LOGGER.addFilter(logger_filter)
ACCOUNT_BASE = 'https://account.xiaomi.com'
UA = "Android-7.1.1-1.0.0-ONEPLUS A3010-136-%s APP/xiaomi.smarthome APPV/62830"
class MiotCloud(micloud.MiCloud):
user_id = None
cuser_id = None
ssecurity = None
pass_token = None
service_token = None
failed_logins = 0
session = None
async_session: Optional[aiohttp.ClientSession] = None
def __init__(self, hass, username, password, country=None, sid=None):
try:
super().__init__(username, password)
timezone = datetime.now(local_zone(hass)).strftime('%z')
self.timezone = 'GMT{0}:{1}'.format(timezone[:-2], timezone[-2:])
self.locale = locale.getlocale()[0]
except Exception:
self.timezone = 'GMT+08:00'
self.locale = 'zh_CN'
self.hass = hass
self.username = username
self.password = password
self.default_server = country or 'cn'
self.sid = sid or 'xiaomiio'
self.agent_id = self.get_random_string(16)
self.client_id = self.agent_id
self.useragent = UA % self.client_id
self.http_timeout = int(hass.data[DOMAIN].get('config', {}).get('http_timeout') or 10)
self.login_times = 0
self.cookies = {}
self.attrs = {}
@property
def unique_id(self):
uid = self.user_id or self.username
return f'{uid}-{self.default_server}-{self.sid}'
async def async_get_properties_for_mapping(self, did, mapping: dict):
pms = []
rmp = {}
for k, v in mapping.items():
if not isinstance(v, dict):
continue
s = v.get('siid')
p = v.get('piid')
pms.append({'did': str(did), 'siid': s, 'piid': p})
rmp[f'prop.{s}.{p}'] = k
rls = await self.async_get_props(pms)
if not rls:
return None
dls = []
for v in rls:
s = v.get('siid')
p = v.get('piid')
k = rmp.get(f'prop.{s}.{p}')
if not k:
continue
v['prop'] = k
dls.append(v)
return dls
async def async_get_props(self, params=None):
return await self.async_request_miot_spec('prop/get', params)
@aiohttp_retry(3, logger=_LOGGER)
async def async_set_props(self, params=None):
return await self.async_request_miot_spec('prop/set', params, timeout=5, raise_timeout=True)
async def async_do_action(self, params=None):
return await self.async_request_miot_spec('action', params)
async def async_request_miot_spec(self, api, params=None, **kwargs):
resp = await self.async_request_api('miotspec/' + api, {
'params': params or [],
}, **kwargs) or {}
result = resp.get('result')
if not result and self.is_token_expired(resp):
now = int(time.time())
tim = self.attrs.get('last_relogin_time', 0)
if now - tim > 600:
self.attrs['last_relogin_time'] = now
if await self.async_check_auth(notify=True):
return await self.async_request_miot_spec(api, params, **kwargs)
raise MiCloudException(json.dumps(resp, ensure_ascii=False))
if not result and resp.get('code'):
raise MiCloudException(json.dumps(resp, ensure_ascii=False))
return result
async def async_get_user_device_data(self, did, key, typ='prop', raw=False, **kwargs):
now = int(time.time())
timeout = kwargs.pop('timeout', self.http_timeout)
params = {
'did': did,
'key': key,
'type': typ,
'time_start': now - 86400 * 7,
'time_end': now + 60,
'limit': 5,
**kwargs,
}
rdt = await self.async_request_api('user/get_user_device_data', params, timeout=timeout) or {}
return rdt if raw else rdt.get('result')
async def async_get_last_device_data(self, did, key, typ='prop', **kwargs):
kwargs['raw'] = False
kwargs['limit'] = 1
rls = await self.async_get_user_device_data(did, key, typ, **kwargs) or [None]
rdt = rls.pop(0) or {}
if kwargs.get('not_value'):
return rdt
val = rdt.get('value')
if val is None:
return None
try:
vls = json.loads(val)
except (TypeError, ValueError):
vls = [val]
return vls.pop(0)
async def async_check_auth(self, notify=False):
if self.service_token:
api = 'v2/message/v2/check_new_msg'
dat = {
'begin_at': int(time.time()) - 60,
}
try:
rdt = await self.async_request_api(api, dat, method='POST') or {}
if not self.is_token_expired(rdt):
return True
except requests.exceptions.ConnectionError:
return None
except requests.exceptions.Timeout:
return None
# auth err
_LOGGER.info('Xiaomi auth failed, try relogin. %s', rdt)
nid = f'xiaomi-miot-auth-warning-{self.user_id}'
need_verify = None
try:
if await self.async_relogin():
persistent_notification.dismiss(self.hass, nid)
return True
except MiCloudNeedVerify as exc:
need_verify = exc
if notify:
lnk = f'/config/integrations/integration/{DOMAIN}'
persistent_notification.create(
self.hass,
f'Xiaomi account: {self.user_id} auth failed, '
f'Your Xiaomi account login status has expired, Please login again through [integrated configuration]({lnk}).\n'
f'你的小米账号登陆状态已失效,但本次需要手动验证,请通过[集成配置]({lnk})重新登陆。',
'Xiaomi Miot Warning',
nid,
)
_LOGGER.error(
'Xiaomi account: %s auth failed, Please update option for this integration to refresh token.\n%s',
self.user_id,
rdt,
)
elif need_verify:
raise need_verify
else:
_LOGGER.warning('Retry login xiaomi account failed: %s', self.username)
return False
def is_token_expired(self, data: dict):
eno = data.get('code', 0)
msg = data.get('message', '')
if eno in [2, 3]:
return True
elif 'auth err' in msg:
return True
elif msg in ['invalid signature', 'SERVICETOKEN_EXPIRED']:
return True
return False
async def async_request_api(self, api, data, method='POST', crypt=True, debug=True, **kwargs):
if not self.service_token:
await self.async_login()
params = {}
if data is not None:
params['data'] = self.json_encode(data)
raise_timeout = kwargs.pop('raise_timeout', None)
raw = kwargs.pop('raw', self.sid != 'xiaomiio')
rsp = None
try:
if raw:
rsp = await self.hass.async_add_executor_job(
partial(self.request_raw, api, data, method, **kwargs)
)
elif crypt:
rsp = await self.async_request_rc4_api(api, params, method, **kwargs)
else:
rsp = await self.hass.async_add_executor_job(
partial(self.request, self.get_api_url(api), params, **kwargs)
)
rdt = json.loads(rsp)
if debug:
_LOGGER.debug(
'Request miot api: %s %s result: %s',
api, data, rsp,
)
self.attrs['timeouts'] = 0
except asyncio.TimeoutError as exc:
if raise_timeout:
raise exc
rdt = None
self.attrs.setdefault('timeouts', 0)
self.attrs['timeouts'] += 1
if 5 < self.attrs['timeouts'] <= 10:
_LOGGER.error('Request xiaomi api: %s %s timeout, exception: %s', api, data or {}, exc)
elif self.attrs['timeouts'] <= 5:
_LOGGER.warning('Request xiaomi api: %s timeout (%s times)', api, self.attrs['timeouts'])
except asyncio.exceptions.CancelledError as exc:
rdt = None
_LOGGER.warning('Request xiaomi api: %s was cancelled, likely due to timeout: %s', api, exc)
except (TypeError, ValueError):
rdt = None
code = rdt.get('code') if rdt else None
if code == 3:
self._logout()
_LOGGER.warning('Unauthorized while request to %s, response: %s, logged out.', api, rsp)
elif code or not rdt:
fun = _LOGGER.info if rdt else _LOGGER.warning
fun('Request xiaomi api: %s %s failed, response: %s', api, data, rsp)
return rdt
async def async_get_device(self, mac=None, host=None):
dvs = await self.async_get_devices() or []
for d in dvs:
if not isinstance(d, dict):
continue
if mac and mac == d.get('mac'):
return d
if host and host == d.get('localip'):
return d
return None
async def get_device_list(self):
rdt = await self.async_request_api('home/device_list', {
'getVirtualModel': True,
'getHuamiDevices': 1,
'get_split_device': False,
'support_smart_home': True,
}, debug=False, timeout=60) or {}
result = rdt.get('result')
if result:
return result['list']
_LOGGER.warning('Got xiaomi devices for %s failed: %s', self.username, rdt)
return None
async def get_all_devices(self, homes=None):
devices = {
d['did']: d
for d in await self.get_device_list() or []
}
if not isinstance(homes, list):
return await self.get_device_list() or []
for home in homes:
hid = int(home.get('id', 0))
uid = int(home.get('uid', 0))
start_did = ''
has_more = True
while has_more:
rdt = await self.async_request_api('v2/home/home_device_list', {
'home_owner': uid,
'home_id': hid,
'limit': 300,
'start_did': start_did,
'get_split_device': False,
'support_smart_home': True,
'get_cariot_device': True,
'get_third_device': True,
}, debug=False, timeout=20) or {}
result = rdt.get('result') or {}
if not result:
_LOGGER.warning('Got xiaomi devices for %s failed: %s', self.username, rdt)
for d in result.get('device_info') or []:
did = d.get('did')
devices.setdefault(did, {}).update(d)
start_did = result.get('max_did') or ''
has_more = result.get('has_more') and start_did
return list(devices.values())
async def get_home_devices(self):
rdt = await self.async_request_api('v2/homeroom/gethome_merged', {
'fg': True,
'fetch_share': True,
'fetch_share_dev': True,
'fetch_cariot': True,
'limit': 300,
'app_ver': 7,
'plat_form': 0,
}, debug=False, timeout=60) or {}
result = rdt.get('result') or {}
if not result:
_LOGGER.warning('Got xiaomi home devices for %s failed: %s', self.username, rdt)
devices = result.setdefault('devices', {})
for h in result.get('homelist', []):
for r in h.get('roomlist', []):
for did in r.get('dids', []):
devices[did] = {
'home_id': h.get('id'),
'room_id': r.get('id'),
'home_name': h.get('name'),
'room_name': r.get('name'),
}
return result
async def async_get_devices(self, renew=False, return_all=False):
if not self.user_id:
return None
fnm = f'xiaomi_miot/devices-{self.user_id}-{self.default_server}.json'
store = Store(self.hass, 1, fnm)
now = time.time()
cds = []
dvs = []
try:
dat = await store.async_load() or {}
except ValueError:
await store.async_remove()
dat = {}
if isinstance(dat, dict):
cds = dat.get('devices') or []
if not renew and dat.get('update_time', 0) > (now - 86400):
dvs = cds
if not dvs:
try:
hls = await self.get_home_devices()
dvs = await self.get_all_devices(hls.get('homelist', []))
if dvs:
if hls:
hds = hls.get('devices') or {}
dvs = [
{**d, **(hds.get(d.get('did')) or {})}
for d in dvs
]
dat = {
'update_time': now,
'devices': dvs,
'homes': hls.get('homelist', []),
}
await store.async_save(dat)
_LOGGER.info('Got %s devices from xiaomi cloud', len(dvs))
except requests.exceptions.ConnectionError as exc:
if not cds:
raise exc
dvs = cds
_LOGGER.warning('Get xiaomi devices filed: %s, use cached %s devices.', exc, len(cds))
if return_all:
return dat
return dvs
async def async_renew_devices(self):
return await self.async_get_devices(renew=True)
async def async_get_devices_by_key(self, key, renew=False, filters=None):
dat = {}
if filters is None:
filters = {}
fls = ['ssid', 'bssid', 'home_id', 'model', 'did']
dvs = await self.async_get_devices(renew=renew) or []
for d in dvs:
if not isinstance(d, dict):
continue
if self.is_hide(d):
continue
if not d.get('mac'):
d['mac'] = d.get('did')
k = d.get(key)
for f in fls:
ft = filters.get(f'filter_{f}')
if not ft:
continue
ex = ft != 'include'
fl = filters.get(f'{f}_list') or {}
fv = d.get(f)
if ex:
ok = fv not in fl
else:
ok = fv in fl
if not ok:
k = None
if k:
dat[k] = d
return dat
async def async_get_homerooms(self, renew=False):
dat = await self.async_get_devices(renew=renew, return_all=True) or {}
return dat.get('homes') or []
async def async_get_beaconkey(self, did):
dat = {'did': did, 'pdid': 1}
rdt = await self.async_request_api('v2/device/blt_get_beaconkey', dat) or {}
return rdt.get('result', rdt)
@staticmethod
def is_hide(d):
did = d.get('did', '')
pid = d.get('pid', '')
if pid == '21':
prt = d.get('parent_id')
if prt and prt in did:
# issues/263
return True
return False
async def async_login(self, login_data=None):
if self.login_times > 5:
await self.async_stored_auth(remove=True)
if self.login_times > 10:
raise MiCloudException(
'Too many failures when login to Xiaomi, '
'please reload/config xiaomi_miot component.'
)
self.login_times += 1
ret = await self.hass.async_add_executor_job(self._login_request, login_data)
if ret:
self.hass.data[DOMAIN]['sessions'][self.unique_id] = self
await self.async_stored_auth(save=True)
self.login_times = 0
return ret
async def async_relogin(self):
self._logout()
return await self.async_login()
def _logout(self):
self.service_token = None
self.async_session = None
def _login_request(self, login_data=None):
self._init_session(True)
location = ''
auth = self.attrs.pop('login_data', {})
if not login_data:
pass
elif ticket := login_data.get('verify_ticket'):
resp = self.verify_ticket(ticket)
location = resp.get('location', '')
if location:
self.account_get(location, allow_redirects=True)
auth = self._login_step1()
location = auth.get('location', '')
elif auth:
auth.update(login_data)
else:
auth = self._login_step1()
if not location:
location = self._login_step2(**auth)
response = self._login_step3(location)
http_code = response.status_code
if http_code == 200:
return True
elif http_code == 403:
raise MiCloudAccessDenied(f'Login to xiaomi error: {response.text} ({http_code})')
else:
_LOGGER.error(
'Xiaomi login request returned status %s, reason: %s, content: %s',
http_code, response.reason, response.text,
)
raise MiCloudException(f'Login to xiaomi error: {response.text} ({http_code})')
def _login_step1(self):
self.cookies.update({'sdkVersion': '3.8.6', 'deviceId': self.client_id})
try:
auth = self.account_get(
'/pass/serviceLogin',
params={'sid': self.sid, '_json': 'true'},
headers={'User-Agent': self.useragent},
)
except Exception as exc:
raise MiCloudException(f'Error getting xiaomi login sign. Cannot parse response. {exc}')
if auth.get('code') == 0:
self.user_id = auth.get('userId', self.user_id)
self.cuser_id = auth.get('cUserId', self.cuser_id)
self.ssecurity = auth.get('ssecurity', self.ssecurity)
self.pass_token = auth.get('passToken', self.pass_token)
self.async_session = None
return auth
def _login_step2(self, captcha=None, **kwargs):
url = '/pass/serviceLoginAuth2'
post = {
'user': self.username,
'hash': hashlib.md5(self.password.encode()).hexdigest().upper(),
'callback': kwargs.get('callback') or '',
'sid': kwargs.get('sid') or self.sid,
'qs': kwargs.get('qs') or '',
'_sign': kwargs.get('_sign') or '',
}
params = {'_json': 'true'}
cookies = {}
if captcha:
post['captCode'] = captcha
params['_dc'] = int(time.time() * 1000)
cookies['ick'] = self.attrs.pop('captchaIck', '')
response = self.account_post(url, data=post, params=params, cookies=cookies, response=True)
auth = self.json_decode(response.text) or {}
code = auth.get('code')
# 20003 InvalidUserNameException
# 22009 PackageNameDeniedException
# 70002 InvalidCredentialException
# 70016 InvalidCredentialException with captchaUrl / Password error
# 81003 NeedVerificationException
# 87001 InvalidResponseException captCode error
# other NeedCaptchaException
location = auth.get('location')
if not location:
if ntf := auth.get('notificationUrl'):
if ntf[:4] != 'http':
ntf = f'{ACCOUNT_BASE}{ntf}'
self.attrs['verify_url'] = ntf
raise MiCloudNeedVerify('need_verify').with_url(ntf)
if cap := auth.get('captchaUrl'):
if cap[:4] != 'http':
cap = f'{ACCOUNT_BASE}{cap}'
if self._get_captcha(cap):
self.attrs['login_data'] = kwargs
_LOGGER.error(
'Xiaomi serviceLoginAuth2: %s' %
[url, self.login_times, {**post, 'hash': '*'}, cookies, response.text],
)
raise MiCloudAccessDenied(f'Login to xiaomi error: {response.text}')
self.user_id = str(auth.get('userId', ''))
self.cuser_id = auth.get('cUserId')
self.ssecurity = auth.get('ssecurity')
self.pass_token = auth.get('passToken')
if self.sid != 'xiaomiio':
sign = f'nonce={auth.get("nonce")}&{auth.get("ssecurity")}'
sign = hashlib.sha1(sign.encode()).digest()
sign = base64.b64encode(sign).decode()
location += '&clientSign=' + parse.quote(sign)
_LOGGER.info('Xiaomi serviceLoginAuth2: %s', [auth, self.cookies])
return location
def _login_step3(self, location):
self.session.headers.update({'content-type': 'application/x-www-form-urlencoded'})
response = self.account_get(location, response=True)
cookies = response.cookies
service_token = cookies.get('serviceToken')
if service_token:
self.service_token = service_token
self.user_id = cookies.get('userId', self.user_id)
self.cuser_id = cookies.get('cUserId', self.cuser_id)
self.async_session = None
else:
err = {
'location': location,
'status_code': response.status_code,
'cookies': cookies.get_dict(),
'response': response.text,
}
raise MiCloudAccessDenied(f'Login to xiaomi error: {err}')
return response
def _get_captcha(self, url):
response = self.session.get(url)
if ick := response.cookies.get('ick'):
self.attrs['captchaIck'] = ick
self.attrs['captchaImg'] = base64.b64encode(response.content).decode()
return ick
def check_identity_list(self, url, path='fe/service/identity/authStart'):
if path not in url:
return None
resp = self.account_get(url.replace(path, 'identity/list'), response=True)
identity_session = resp.cookies.get('identity_session')
if not identity_session:
return False
data = self.json_decode(resp.text) or {}
flag = data.get('flag', 4)
options = data.get('options', [flag])
return options
def verify_ticket(self, ticket):
url = self.attrs.get('verify_url')
if not url:
return {}
options = self.check_identity_list(url) or []
for flag in options:
api = {
4: '/identity/auth/verifyPhone',
8: '/identity/auth/verifyEmail',
}.get(flag)
if not api:
continue
data = self.account_post(
api,
params={
'_dc': int(time.time() * 1000),
},
data={
'_flag': flag,
'ticket': ticket,
'trust': 'true',
'_json': 'true',
},
cookies={
'identity_session': self.attrs.get('identity_session'),
},
)
if data.get('code') == 0:
self.attrs.pop('identity_session', None)
return data
return {}
def account_get(self, url, method='GET', **kwargs):
return self.account_post(url, method, **kwargs)
def account_post(self, url, method='POST', **kwargs):
if url[:4] != 'http':
url = f'{ACCOUNT_BASE}{url}'
kwargs['cookies'] = {
**self.cookies,
**kwargs.get('cookies', {}),
}
kwargs.setdefault('headers', {'User-Agent': self.useragent})
response = kwargs.pop('response', None)
resp = self.session.request(method, url, **kwargs)
try:
data = self.json_decode(resp.text) or {}
except Exception:
data = {
'code': resp.status_code,
'response': resp.text,
}
cookies = resp.cookies.get_dict()
self.cookies.update(cookies)
log = _LOGGER.warning if data.get('code') else _LOGGER.info
log('Account request: %s' % [url, kwargs, resp.text, cookies])
if response:
return resp
return data
def json_decode(self, text):
return json.loads(text.replace('&&&START&&&', ''))
def to_config(self):
return {
CONF_USERNAME: self.username,
CONF_PASSWORD: self.password,
'server_country': self.default_server,
'user_id': self.user_id,
'service_token': self.service_token,
'ssecurity': self.ssecurity,
'sid': self.sid,
'device_id': self.client_id,
}
@staticmethod
async def from_token(hass, config: dict, login=None):
mic = await hass.async_add_executor_job(
MiotCloud,
hass,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get('server_country'),
config.get('sid'),
)
mic.user_id = str(config.get('user_id') or '')
if a := hass.data[DOMAIN].get('sessions', {}).get(mic.unique_id):
mic = a
mic.merger_config(config)
if not mic.service_token:
sdt = await mic.async_stored_auth(save=False)
config.update(sdt)
mic.service_token = config.get('service_token')
mic.ssecurity = config.get('ssecurity')
did = config.get('device_id') or ''
if did and len(did) <= 32:
mic.client_id = did
mic.useragent = UA % did
if login is None:
if not mic.service_token:
login = True
if login:
await mic.async_login()
else:
hass.data[DOMAIN]['sessions'][mic.unique_id] = mic
return mic
def merger_config(self, config: dict, changed=None):
if self.username != config.get(CONF_USERNAME):
self.username = config.get(CONF_USERNAME)
changed = True
if self.password != config.get(CONF_PASSWORD):
self.password = config.get(CONF_PASSWORD)
changed = True
if changed:
self.async_session = None
self.service_token = None
self.ssecurity = None
self.cookies = {}
self.attrs = {}
return self
async def async_change_sid(self, sid: str, login=None):
config = {
**self.to_config(),
'sid': sid,
'service_token': None,
'ssecurity': None,
}
mic = await self.from_token(self.hass, config, login)
return mic
async def async_stored_auth(self, uid=None, save=False, remove=False):
if not uid:
uid = self.user_id or self.username
fnm = f'xiaomi_miot/auth-{uid}-{self.default_server}.json'
if self.sid != 'xiaomiio':
fnm = fnm.replace('.json', f'-{self.sid}.json')
store = Store(self.hass, 1, fnm)
if remove:
await store.async_remove()
try:
old = await store.async_load() or {}
except ValueError:
if not remove:
await store.async_remove()
old = {}
if save:
cfg = self.to_config()
cfg.pop(CONF_PASSWORD, None)
if cfg.get('service_token') == old.get('service_token'):
cfg['update_at'] = old.get('update_at')
else:
cfg['update_at'] = f'{datetime.fromtimestamp(int(time.time()))}'
await store.async_save(cfg)
return cfg
return old
def api_session(self, **kwargs):
if not self.service_token or not self.user_id:
raise MiCloudException('Cannot execute request. service token or userId missing. Make sure to login.')
if kwargs.get('async'):
session = self.async_session
if not session or session.closed:
session = async_create_clientsession(
self.hass,
headers=self.api_headers(),
cookies=self.api_cookies(),
)
self.async_session = session
else:
session = requests.Session()
session.headers.update(self.api_headers())
session.cookies.update(self.api_cookies())
return session
def api_headers(self):
return {
'X-XIAOMI-PROTOCAL-FLAG-CLI': 'PROTOCAL-HTTP2',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': self.useragent,
}
def api_cookies(self):
return {
'userId': str(self.user_id),
'yetAnotherServiceToken': self.service_token,
'serviceToken': self.service_token,
'locale': str(self.locale),
'timezone': str(self.timezone),
'is_daylight': str(time.daylight),
'dst_offset': str(time.localtime().tm_isdst * 60 * 60 * 1000),
'channel': 'MI_APP_STORE',
}
def request(self, url, params, **kwargs):
self.session = self.api_session()
timeout = kwargs.get('timeout', self.http_timeout)
try:
nonce = miutils.gen_nonce()
signed_nonce = miutils.signed_nonce(self.ssecurity, nonce)
signature = miutils.gen_signature(url.replace('/app/', '/'), signed_nonce, nonce, params)
post_data = {
'signature': signature,
'_nonce': nonce,
'data': params['data'],
}
response = self.session.post(url, data=post_data, timeout=timeout)
return response.text
except requests.exceptions.HTTPError as exc:
_LOGGER.error('Error while executing request to %s: %s', url, exc)
except MiCloudException as exc:
_LOGGER.error('Error while decrypting response of request to %s: %s', url, exc)
async def async_request_rc4_api(self, api, params: dict, method='POST', **kwargs):
url = self.get_api_url(api)
session = self.api_session(**{'async': True})
timeout = aiohttp.ClientTimeout(total=kwargs.get('timeout', self.http_timeout))
headers = {
'MIOT-ENCRYPT-ALGORITHM': 'ENCRYPT-RC4',
'Accept-Encoding': 'identity',
}
params = self.rc4_params(method, url, params)
if method == 'GET':
response = await session.get(url, params=params, timeout=timeout, headers=headers)
else:
response = await session.post(url, data=params, timeout=timeout, headers=headers)
rsp = await response.text()
if not rsp or 'error' in rsp or 'invalid' in rsp:
_LOGGER.warning('Error while executing request to %s: %s', url, rsp or response.status)
elif 'message' not in rsp:
try:
signed_nonce = self.signed_nonce(params['_nonce'])
rsp = MiotCloud.decrypt_data(signed_nonce, rsp)
except ValueError:
_LOGGER.warning('Error while decrypting response of request to %s :%s', url, rsp)
return rsp
def request_raw(self, url, data=None, method='GET', **kwargs):
self.session = self.api_session()
url = self.get_api_url(url)
kwargs.setdefault('params' if method == 'GET' else 'data', data)
kwargs.setdefault('timeout', self.http_timeout)
try:
response = self.session.request(method, url, **kwargs)
if response.status_code == 401:
self._logout()
_LOGGER.warning('Unauthorized while executing request to %s, logged out.', url)
rsp = response.text
if not rsp or 'error' in rsp or 'invalid' in rsp:
log = _LOGGER.info if 'remote/ubus' in url else _LOGGER.warning
log('Error while executing request to %s: %s', url, rsp or response.status_code)
return rsp
except requests.exceptions.HTTPError as exc:
_LOGGER.warning('Error while executing request to %s: %s', url, exc)
return None
def get_api_by_host(self, host, api=''):
srv = self.default_server.lower()
if srv and srv != 'cn':
host = f'{srv}.{host}'
api = str(api).lstrip('/')
return f'https://{host}/{api}'
def get_api_url(self, api):
if api[:6] == 'https:' or api[:5] == 'http:':
url = api
else:
api = str(api).lstrip('/')
url = self._get_api_url(self.default_server) + '/' + api
return url
def rc4_params(self, method, url, params: dict):
nonce = miutils.gen_nonce()
signed_nonce = self.signed_nonce(nonce)
params['rc4_hash__'] = MiotCloud.sha1_sign(method, url, params, signed_nonce)
for k, v in params.items():
params[k] = MiotCloud.encrypt_data(signed_nonce, v)
params.update({
'signature': MiotCloud.sha1_sign(method, url, params, signed_nonce),
'ssecurity': self.ssecurity,
'_nonce': nonce,
})
return params
def signed_nonce(self, nonce):
return miutils.signed_nonce(self.ssecurity, nonce)
@staticmethod
def json_encode(data):
return json.dumps(data, separators=(',', ':'))
@staticmethod
def sha1_sign(method, url, dat: dict, nonce):
path = parse.urlparse(url).path
if path[:5] == '/app/':
path = path[4:]
arr = [str(method).upper(), path]
for k, v in dat.items():
arr.append(f'{k}={v}')
arr.append(nonce)
raw = hashlib.sha1('&'.join(arr).encode('utf-8')).digest()
return base64.b64encode(raw).decode()
@staticmethod
def encrypt_data(pwd, data):
return base64.b64encode(RC4(base64.b64decode(pwd)).init1024().crypt(data)).decode()
@staticmethod
def decrypt_data(pwd, data):
return RC4(base64.b64decode(pwd)).init1024().crypt(base64.b64decode(data))
@staticmethod
def all_clouds(hass):
cls = {}
for k, v in hass.data[DOMAIN].items():
if isinstance(v, dict):
v = v.get(CONF_XIAOMI_CLOUD)
if isinstance(v, MiotCloud):
cls[v.unique_id] = v
return list(cls.values())
@staticmethod
def get_random_string(length):
seq = string.ascii_uppercase + string.digits
return ''.join((random.choice(seq) for _ in range(length)))
class MiCloudNeedVerify(MiCloudException):
url = None
def with_url(self, url):
self.url = url
return self
@@ -0,0 +1,235 @@
"""Support for Curtain and Airer."""
import logging
from datetime import timedelta
from homeassistant.components.cover import (
DOMAIN as ENTITY_DOMAIN,
CoverEntity as BaseEntity,
CoverEntityFeature,
CoverDeviceClass,
)
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import MiotProperty
from .core.converters import MiotPropConv, MiotTargetPositionConv
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class CoverEntity(XEntity, BaseEntity):
_attr_is_closed = None
_attr_device_class = None
_attr_target_cover_position = None
_attr_supported_features = CoverEntityFeature(0)
_conv_status = None
_conv_motor: MiotPropConv = None
_conv_current_position = None
_conv_target_position = None
_current_range = None
_target_range = (0, 100)
_is_airer = None
_motor_reverse = None
_position_reverse = None
_open_texts = ['open', 'up']
_close_texts = ['close', 'down']
_closed_position = 0
_deviated_position = 0
_target_position_props = None
_cover_position_mapping = None
_target2current_position = None
def on_init(self):
self._attr_available = self.device.available
models = f'{self.device.model} {self.device.info.urn}'
self._is_airer = 'airer' in models
if self._is_airer:
self._position_reverse = True
elif 'curtain' in models:
self._attr_device_class = CoverDeviceClass.CURTAIN
elif 'wopener' in models or 'window-opener' in models:
self._attr_device_class = CoverDeviceClass.WINDOW
self._motor_reverse = self.custom_config_bool('motor_reverse', self._motor_reverse)
self._position_reverse = self.custom_config_bool('position_reverse', self._position_reverse)
self._open_texts = self.custom_config_list('open_texts', self._open_texts)
self._close_texts = self.custom_config_list('close_texts', self._close_texts)
if self._motor_reverse:
self._open_texts, self._close_texts = self._close_texts, self._open_texts
self._target_position_props = self.custom_config_list('target_position_props') or []
self._cover_position_mapping = self.custom_config_json('cover_position_mapping') or {}
if not self.conv.attrs:
self.conv.attrs.append(self.conv.full_name)
for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['status']):
self._conv_status = conv
elif prop.in_list(['motor_control']):
self._conv_motor = conv
self._attr_supported_features |= CoverEntityFeature.OPEN
self._attr_supported_features |= CoverEntityFeature.CLOSE
if prop.list_first('Stop', 'Pause') is not None:
self._attr_supported_features |= CoverEntityFeature.STOP
elif prop.in_list(['current_position']):
if prop.value_range:
self._conv_current_position = conv
self._current_range = (prop.range_min(), prop.range_max())
self.log.debug('current_position: %s', conv)
elif prop.value_list and self._cover_position_mapping and not self._conv_current_position:
self._conv_current_position = conv
self._current_range = (0, 100)
self.log.debug('current_position: %s', conv)
elif prop.value_range and isinstance(conv, MiotTargetPositionConv):
self._conv_target_position = conv
self._target_range = conv.ranged
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
elif prop.value_range and prop.in_list(self._target_position_props):
self._conv_target_position = conv
self._target_range = (prop.range_min(), prop.range_max())
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
if self.custom_config_bool('disable_target_position'):
self._conv_target_position = None
self._attr_supported_features &= ~CoverEntityFeature.SET_POSITION
self._deviated_position = self.custom_config_integer('deviated_position', 2)
if self._current_range:
pos = self._current_range[0] + self._deviated_position
self._closed_position = self.custom_config_integer('closed_position', pos)
self._target2current_position = self.custom_config_bool('target2current_position', not self._conv_current_position)
if self._motor_reverse or self._position_reverse:
self._attr_extra_state_attributes.update({
'motor_reverse': self._motor_reverse,
'position_reverse': self._position_reverse,
})
if self._closed_position:
self._attr_extra_state_attributes.update({
'closed_position': self._closed_position,
'deviated_position': self._deviated_position,
})
def set_state(self, data: dict):
prop_status = getattr(self._conv_status, 'prop', None) if self._conv_status else None
if prop_status:
val = self._conv_status.value_from_dict(data)
if val is not None:
self._attr_is_closed = None
self._attr_is_opening = None
self._attr_is_closing = None
if val in prop_status.list_search('Closed'):
self._attr_is_closed = True
elif val in prop_status.list_search('Opened'):
self._attr_is_closed = False
elif val in prop_status.list_search('Opening'):
self._attr_is_opening = True
elif val in prop_status.list_search('Closing'):
self._attr_is_closing = True
elif val in prop_status.list_search('Rising'):
self._attr_is_closing = self._position_reverse
elif val in prop_status.list_search('Falling', 'Dropping'):
self._attr_is_opening = self._position_reverse
elif val in prop_status.list_search('Stop Lower Limit', 'Stop At Lowest', 'Floor'):
self._attr_is_closed = not self._position_reverse
elif val in prop_status.list_search('Stop Upper Limit', 'Stop At Highest', 'Ceiling'):
self._attr_is_closed = self._position_reverse
elif self._is_airer and val in prop_status.list_search('Down'):
self._attr_is_closed = False
if self._conv_current_position:
val = self._conv_current_position.value_from_dict(data)
if val is not None:
val = int(val)
if self._cover_position_mapping:
val = self._cover_position_mapping.get(val, val)
if self._position_reverse:
val = self._current_range[1] - val
self._attr_current_cover_position = val
if self._conv_target_position:
val = self._conv_target_position.value_from_dict(data)
if val is not None:
val = int(val)
if self._position_reverse:
val = self._target_range[1] - val
self._attr_target_cover_position = val
if self._target2current_position:
self._attr_current_cover_position = self._attr_target_cover_position
self._attr_extra_state_attributes.update({
'target2current_position': True,
})
if (val := self._attr_current_cover_position) is not None:
closed = val <= self._closed_position
if self._deviated_position is None:
pass
elif val <= self._deviated_position:
self._attr_current_cover_position = 0
closed = not self._is_airer
elif val >= (100 - self._deviated_position):
self._attr_current_cover_position = 100
closed = self._is_airer
if self._attr_is_closed is None or not prop_status:
self._attr_is_closed = closed
self._attr_extra_state_attributes.update({
'state_is_closed': self._attr_is_closed,
})
async def async_open_cover(self, **kwargs):
if conv := self._conv_motor:
val = conv.prop.list_first(*self._open_texts)
if val is not None:
await self.device.async_write({conv.full_name: val})
return
self.log.warning('No open command found in motor control property: %s', self._open_texts)
await self.async_set_cover_position(0 if self._position_reverse else 100)
async def async_close_cover(self, **kwargs):
if conv := self._conv_motor:
val = conv.prop.list_first(*self._close_texts)
if val is not None:
await self.device.async_write({conv.full_name: val})
return
self.log.warning('No close command found in motor control property: %s', [self._close_texts, conv.prop.value_list])
await self.async_set_cover_position(100 if self._position_reverse else 0)
async def async_stop_cover(self, **kwargs):
if not self._conv_motor:
return
val = self._conv_motor.prop.list_first('Stop', 'Pause')
if val is not None:
await self.device.async_write({self._conv_motor.full_name: val})
async def async_set_cover_position(self, position, **kwargs):
if not self._conv_target_position:
return
if self._position_reverse:
position = self._target_range[1] - position
await self.device.async_write({self._conv_target_position.full_name: position})
XEntity.CLS[ENTITY_DOMAIN] = CoverEntity
@@ -0,0 +1,248 @@
"""Support for Xiaomi device tracker."""
import logging
import time
from datetime import timedelta
from homeassistant.components.device_tracker import (
DOMAIN as ENTITY_DOMAIN,
)
from homeassistant.components.device_tracker.const import SourceType
from homeassistant.components.device_tracker.config_entry import (
TrackerEntity as BaseTrackerEntity,
ScannerEntity as BaseScannerEntity,
)
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
)
from .core.coord_transform import gcj02_to_wgs84, bd09_to_wgs84
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services('watch', 'rearview_mirror', 'head_up_display'):
if 'xiaoxun.watch.' in model:
entities.append(XiaoxunWatchTrackerEntity(config, srv))
elif srv.get_property('latitude', 'longitude'):
entities.append(MiotTrackerEntity(config, srv))
if not entities and ('xiaoxun.watch.' in model or 'xiaoxun.tracker.' in model):
# xiaoxun.watch.sw763
# xiaoxun.tracker.v1
entities.append(XiaoxunWatchTrackerEntity(config))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class ScannerEntity(XEntity, BaseScannerEntity, RestoreEntity):
@property
def device_info(self):
return self._attr_device_info
@property
def unique_id(self):
return self._attr_unique_id
@property
def is_connected(self):
"""Return true if the device is connected to the network."""
return True if self._attr_state else False
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
return SourceType.ROUTER
def get_state(self) -> dict:
return {self.attr: self._attr_state}
XEntity.CLS[ENTITY_DOMAIN] = ScannerEntity
XEntity.CLS['scanner'] = ScannerEntity
class MiotTrackerEntity(MiotEntity, BaseTrackerEntity):
_attr_latitude = None
_attr_longitude = None
_attr_location_name = None
_attr_location_accuracy = 0
_disable_location_name = False
def __init__(self, config, miot_service: MiotService = None):
super().__init__(miot_service, config=config, logger=_LOGGER)
async def async_added_to_hass(self):
await super().async_added_to_hass()
self._disable_location_name = self.custom_config_bool('disable_location_name')
async def async_update(self):
await super().async_update()
if not self._available or not self._miot_service:
return
if prop := self._miot_service.get_property('latitude'):
self._attr_latitude = prop.from_device(self.device)
if prop := self._miot_service.get_property('longitude'):
self._attr_longitude = prop.from_device(self.device)
if prop := self._miot_service.get_property('current_address'):
self._attr_location_name = prop.from_device(self.device)
await self.transform_coord()
async def transform_coord(self, default=None):
if not (self._attr_latitude or self._attr_longitude):
return
typ = self.custom_config('coord_type') or default
if not typ:
return
typ = f'{typ}'.lower()
if typ == 'gcj02':
self._attr_longitude, self._attr_latitude = gcj02_to_wgs84(self._attr_longitude, self._attr_latitude)
if typ == 'bd09':
self._attr_longitude, self._attr_latitude = bd09_to_wgs84(self._attr_longitude, self._attr_latitude)
@property
def should_poll(self):
"""No polling for entities that have location pushed."""
return True
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
return SourceType.GPS
@property
def latitude(self):
"""Return latitude value of the device."""
return self._attr_latitude
@property
def longitude(self):
"""Return longitude value of the device."""
return self._attr_longitude
@property
def location_name(self):
"""Return a location name for the current location of the device."""
if self._disable_location_name:
return None
return self._attr_location_name
@property
def location_accuracy(self):
"""Return the location accuracy of the device.
Value in meters.
"""
return self._attr_location_accuracy
@property
def battery_level(self):
"""Return the battery level of the device."""
if not self._miot_service:
return None
sls = [self._miot_service, *self._miot_service.spec.get_services('battery')]
for srv in sls:
prop = srv.get_property('battery_level')
if prop:
return prop.from_device(self.device)
return None
class XiaoxunWatchTrackerEntity(MiotTrackerEntity):
def __init__(self, config, miot_service: MiotService = None, miot_spec: MiotSpec = None):
self._miot_spec = miot_spec
super().__init__(config=config, miot_service=miot_service)
@property
def device_eid(self):
did = f'{self.miot_did}'
return did.replace('xiaoxun.', '')
async def async_update(self):
await super().async_update()
await self.update_location()
async def update_location(self):
did = f'{self.miot_did}'
mic = self.xiaomi_cloud
if not did or not mic:
return
pms = {
'app_id': '10025',
'dids': [did],
'params': {
'CID': 50031,
'model': self.model,
'SN': int(time.time() / 1000),
'PL': {
'Size': 1,
'Key': '78999898989898998',
'EID': self.device_eid,
},
},
}
rdt = await mic.async_request_api('third/api', pms) or {}
loc = {}
for v in (rdt.get('result') or {}).get('PL', {}).get('List', {}).values():
if loc := v.get('result') or {}:
loc.setdefault('device', v)
loc.setdefault('timestamp', v.get('timestamp', ''))
break
if not loc:
self.logger.warning('%s: Got xiaoxun watch location faild: %s', self.name_model, rdt)
return
self.logger.debug('%s: Got xiaoxun watch location: %s', self.name_model, rdt)
dvc = loc.get('device') or {}
gps = f"{loc.get('location', '')},".split(',')
self._attr_latitude = float(gps[1])
self._attr_longitude = float(gps[0])
self._attr_location_name = loc.get('desc')
self._attr_location_accuracy = int(loc.get('radius') or 0)
await self.transform_coord(default='gcj02')
self.update_attrs({
'sos': dvc.get('SOS', 0),
'steps': dvc.get('steps', 0),
'home_wifi': dvc.get('home_wifi', 0),
'imei': dvc.get('imei'),
'adcode': loc.get('adcode'),
'country': loc.get('country'),
'province': loc.get('province'),
'city': loc.get('province'),
'district': loc.get('district'),
'township': loc.get('township'),
'road': loc.get('road'),
'street': loc.get('street'),
})
if tim := loc.get('timestamp', ''):
self.update_attrs({
'timestamp': f'{tim[0:4]}-{tim[4:6]}-{tim[6:8]} {tim[8:10]}:{tim[10:12]}:{tim[12:14]}',
})
+278
View File
@@ -0,0 +1,278 @@
"""Support for Xiaomi fans."""
import logging
from homeassistant.components.fan import (
DOMAIN as ENTITY_DOMAIN,
FanEntity as BaseEntity,
FanEntityFeature,
)
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiirToggleEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
MiotProperty,
)
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
ranged_value_to_percentage,
percentage_to_ranged_value,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
SPEED_OFF = 'off'
OFF_SPEED_VALUES = [SPEED_OFF, None]
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services('ir_fan_control'):
entities.append(MiirFanEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class FanEntity(XEntity, BaseEntity):
_attr_device_class = None
_conv_power = None
_conv_mode = None
_conv_speed = None
_speed_list = None
_speed_range = None
_conv_oscillate = None
_prop_speed = None
_prop_percentage = None
def on_init(self):
speed_property = self.custom_config('speed_property', 'speed')
percentage_property = self.custom_config('percentage_property', 'fan_percent')
for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['on']):
self._conv_power = conv
if hasattr(FanEntityFeature, 'TURN_ON'): # v2024.8
self._attr_supported_features |= FanEntityFeature.TURN_ON
if hasattr(FanEntityFeature, 'TURN_OFF'):
self._attr_supported_features |= FanEntityFeature.TURN_OFF
elif prop.in_list(['mode']):
self._conv_mode = conv
self._attr_preset_modes = prop.list_descriptions()
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
elif prop.in_list(['fan_level', 'speed_level', 'stepless_fan_level', speed_property, percentage_property]):
if prop.value_range:
self.set_percentage_property(prop)
elif prop.value_list and not self._conv_speed:
self.set_speeds_property(prop)
else:
continue
self._conv_speed = conv
elif prop.in_list(['horizontal_swing', 'vertical_swing']) and not self._conv_oscillate:
self._conv_oscillate = conv
self._attr_supported_features |= FanEntityFeature.OSCILLATE
# issues/617
if self.custom_config_bool('disable_preset_modes'):
self._attr_supported_features &= ~FanEntityFeature.PRESET_MODE
self._attr_preset_modes = []
elif dpm := self.custom_config_list('disable_preset_modes'):
self._attr_preset_modes = [
mode
for mode in self._attr_preset_modes
if mode not in dpm
]
def set_percentage_property(self, prop):
if not prop or not prop.value_range:
return
self._prop_percentage = prop
_min = prop.range_min()
_max = prop.range_max()
_stp = prop.range_step()
self._speed_range = (_min, _max)
self._attr_speed_count = (_max - _min) / _stp + 1
self._attr_supported_features |= FanEntityFeature.SET_SPEED
def set_speeds_property(self, prop):
if not prop:
return
self._speed_list = prop.list_descriptions()
self._attr_speed_count = len(self._speed_list)
self._attr_extra_state_attributes['speed_list'] = self._speed_list
self._attr_supported_features |= FanEntityFeature.SET_SPEED
def set_state(self, data: dict):
if self._conv_speed:
val = self._conv_speed.value_from_dict(data)
if val is not None:
des = self._conv_speed.prop.list_description(val)
if self._speed_range:
self._attr_percentage = ranged_value_to_percentage(self._speed_range, float(val))
if self._speed_list and val in self._speed_list:
self._attr_percentage = ordered_list_item_to_percentage(self._speed_list, val)
elif self._speed_list and des in self._speed_list:
self._attr_percentage = ordered_list_item_to_percentage(self._speed_list, des)
if self._conv_power:
val = self._conv_power.value_from_dict(data)
if val is not None:
self._attr_is_on = bool(val)
if not self._attr_is_on:
self._attr_percentage = 0
if self._conv_mode:
val = self._conv_mode.value_from_dict(data)
if val is not None:
self._attr_preset_mode = val
if self._conv_oscillate:
val = self._conv_oscillate.value_from_dict(data)
if val is not None:
self._attr_oscillating = bool(val)
@property
def is_on(self):
if self._conv_power:
return self._attr_is_on
return self.percentage is not None and self.percentage > 0
async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs):
dat = {}
if self._conv_power and not self.is_on:
dat[self._conv_power.full_name] = True
if percentage is not None:
if self._speed_range:
dat[self._conv_speed.full_name] = percentage_to_ranged_value(self._speed_range, percentage)
elif self._speed_list:
des = percentage_to_ordered_list_item(self._speed_list, percentage)
dat[self._conv_speed.full_name] = des
if preset_mode is not None:
dat[self._conv_mode.full_name] = preset_mode
if dat:
await self.device.async_write(dat)
async def async_turn_off(self, **kwargs):
if not self._conv_power:
return
await self.device.async_write({self._conv_power.full_name: False})
async def async_set_percentage(self, percentage: int):
if percentage == 0 and self._conv_power:
await self.async_turn_off()
return
await self.async_turn_on(percentage=percentage)
async def async_set_preset_mode(self, preset_mode: str):
if not self._conv_mode:
return
await self.device.async_write({self._conv_mode.full_name: preset_mode})
async def async_oscillate(self, oscillating: bool):
if not self._conv_oscillate:
return
await self.device.async_write({self._conv_oscillate.full_name: oscillating})
XEntity.CLS[ENTITY_DOMAIN] = FanEntity
class MiirFanEntity(MiirToggleEntity, FanEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
if self._act_turn_on and hasattr(FanEntityFeature, 'TURN_ON'): # v2024.8
self._supported_features |= FanEntityFeature.TURN_ON
if self._act_turn_off and hasattr(FanEntityFeature, 'TURN_OFF'):
self._supported_features |= FanEntityFeature.TURN_OFF
self._attr_percentage = 50
self._act_speed_up = miot_service.get_action('fan_speed_up')
self._act_speed_dn = miot_service.get_action('fan_speed_down')
if self._act_speed_up or self._act_speed_dn:
self._supported_features |= FanEntityFeature.SET_SPEED
self._act_swing_on = miot_service.get_action('horizontal_swing_on')
self._act_swing_off = miot_service.get_action('horizontal_swing_off')
if self._act_swing_on or self._act_swing_off:
self._supported_features |= FanEntityFeature.OSCILLATE
self._supported_features |= FanEntityFeature.PRESET_MODE
self._attr_preset_mode = None
self._attr_preset_modes = []
for a in miot_service.actions.values():
if a.ins:
continue
self._attr_preset_modes.append(a.friendly_desc)
async def async_added_to_hass(self):
await super().async_added_to_hass()
# issues/617
if self.custom_config_bool('disable_preset_modes'):
self._supported_features &= ~FanEntityFeature.PRESET_MODE
self._attr_preset_modes = []
elif dpm := self.custom_config_list('disable_preset_modes'):
self._attr_preset_modes = [
mode
for mode in self._attr_preset_modes
if mode not in dpm
]
async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs):
"""Turn the entity on."""
if percentage is None:
pass
elif percentage > self._attr_percentage and self._act_speed_up:
return await self.async_call_action(self._act_speed_up)
elif percentage < self._attr_percentage and self._act_speed_dn:
return await self.async_call_action(self._act_speed_dn)
if preset_mode is None:
pass
elif act := self._miot_service.get_action(preset_mode):
return await self.async_call_action(act)
return await super().async_turn_on(**kwargs)
async def async_set_percentage(self, percentage: int):
"""Set the speed of the fan, as a percentage."""
return await self.async_turn_on(percentage=percentage)
async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode."""
return await self.async_turn_on(preset_mode=preset_mode)
async def async_oscillate(self, oscillating):
"""Oscillate the fan."""
ret = None
if not oscillating and self._act_swing_off:
ret = await self.async_call_action(self._act_swing_off)
elif oscillating and self._act_swing_on:
ret = await self.async_call_action(self._act_swing_on)
if ret:
self._attr_oscillating = oscillating
return ret
@@ -0,0 +1,150 @@
"""Support for humidifier and dehumidifier."""
import logging
from bisect import bisect
from homeassistant.components.humidifier import (
DOMAIN as ENTITY_DOMAIN,
HumidifierEntity as BaseEntity,
HumidifierEntityFeature, # v2022.5
HumidifierDeviceClass,
)
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import MiotProperty
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
MODE_OFF = 'Off'
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
bind_services_to_entries(hass, {})
class HumidifierEntity(XEntity, BaseEntity):
_attr_mode = None
_attr_available_modes = None
_conv_power = None
_conv_mode = None
_conv_target_humidity = None
_conv_current_humidity = None
_target_humidity_ratio = 1
_target_humidity_step = None
def on_init(self):
self._attr_available_modes = []
self._target_humidity_ratio = self.custom_config_number('target_humidity_ratio', 1)
for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['on']):
self._conv_power = conv
self._attr_available_modes.append(MODE_OFF)
self._attr_supported_features |= HumidifierEntityFeature.MODES
elif prop.in_list(['mode', 'fan_level']) and not self._conv_mode:
self._conv_mode = conv
self._attr_available_modes.extend(prop.list_descriptions())
self._attr_supported_features |= HumidifierEntityFeature.MODES
elif prop.in_list(['relative_humidity', 'humidity']):
self._conv_current_humidity = conv
elif prop.in_list(['target_humidity']):
self._conv_target_humidity = conv
if prop.value_range:
self._attr_min_humidity = prop.range_min()
self._attr_max_humidity = prop.range_max()
self._target_humidity_step = prop.range_step()
elif prop.value_list:
vls = prop.list_value(None)
vls.sort()
self._attr_min_humidity = vls[0]
self._attr_max_humidity = vls[-1]
if self._target_humidity_ratio:
self._attr_min_humidity = round(self._attr_min_humidity * self._target_humidity_ratio)
self._attr_max_humidity = round(self._attr_max_humidity * self._target_humidity_ratio)
typ = f'{self.model} {self._miot_service.spec.type}'
if 'dehumidifier' in typ or '.derh.' in typ:
self._attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER
else:
self._attr_device_class = HumidifierDeviceClass.HUMIDIFIER
def set_state(self, data: dict):
if self._conv_mode:
val = self._conv_mode.value_from_dict(data)
if val is not None:
self._attr_mode = val
if self._conv_power:
val = self._conv_power.value_from_dict(data)
if val is not None:
self._attr_is_on = bool(val)
if not self._attr_is_on:
self._attr_mode = MODE_OFF
if self._conv_target_humidity:
val = self._conv_target_humidity.value_from_dict(data)
if val is not None:
self._attr_target_humidity = round(val * self._target_humidity_ratio)
if self._conv_current_humidity:
val = self._conv_current_humidity.value_from_dict(data)
if val is not None:
self._attr_current_humidity = val
async def async_turn_on(self):
if not self._conv_power:
return
await self.device.async_write({self._conv_power.full_name: True})
async def async_turn_off(self):
if not self._conv_power:
return
await self.device.async_write({self._conv_power.full_name: False})
async def async_set_mode(self, mode: str):
if mode == MODE_OFF:
await self.async_turn_off()
return
data = {}
if not self._attr_is_on and self._conv_power:
data[self._conv_power.full_name] = True
if self._conv_mode:
data[self._conv_mode.full_name] = mode
if data:
await self.device.async_write(data)
async def async_set_humidity(self, humidity: int):
if not self._conv_target_humidity:
return
data = {}
if not self._attr_is_on and self._conv_power:
data[self._conv_power.full_name] = True
prop = getattr(self._conv_target_humidity, 'prop', None)
if self._target_humidity_step:
humidity = round(humidity / self._target_humidity_step) * self._target_humidity_step
elif prop and prop.value_list:
vls = prop.list_value(None)
vls.sort()
idx = bisect(vls, humidity)
humidity = vls[idx - 1] if idx > 0 else vls[0]
if self._target_humidity_ratio:
humidity = round(humidity / self._target_humidity_ratio)
data[self._conv_target_humidity.full_name] = humidity
await self.device.async_write(data)
XEntity.CLS[ENTITY_DOMAIN] = HumidifierEntity
@@ -0,0 +1,58 @@
{
"entity": {
"climate": {
"heater": {
"state_attributes": {
"preset_mode": {
"state": {
"warm": "mdi:weather-sunny"
}
}
}
}
},
"fan": {
"fan": {
"state_attributes": {
"preset_mode": {
"state": {
"auto": "mdi:fan-auto"
}
}
}
}
}
},
"services": {
"send_command": {
"service": "mdi:powershell"
},
"get_properties": {
"service": "mdi:magnify"
},
"set_property": {
"service": "mdi:gamepad"
},
"set_miot_property": {
"service": "mdi:gamepad-up"
},
"call_action": {
"service": "mdi:gesture-tap"
},
"intelligent_speaker": {
"service": "mdi:speaker-message"
},
"xiaoai_wakeup": {
"service": "mdi:chat-sleep"
},
"renew_devices": {
"service": "mdi:autorenew"
},
"get_token": {
"service": "mdi:key-chain"
},
"request_xiaomi_api": {
"service": "mdi:cloud"
}
}
}
@@ -0,0 +1,138 @@
import logging
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform, ATTR_ENTITY_ID
from homeassistant.helpers import intent
_LOGGER = logging.getLogger(__name__)
async def async_setup_intents(hass: HomeAssistant):
"""Set up the intents."""
intent.async_register(hass, XiaoaiPlayText())
intent.async_register(hass, XiaoaiExecuteCommand())
def match_states_from_slots(intent_obj, slots, domains=None):
match_constraints = intent.MatchTargetsConstraints(
name=slots.get('name', {}).get('value'),
area_name=slots.get('area', {}).get('value'),
floor_name=slots.get('floor', {}).get('value'),
domains=domains,
assistant=intent_obj.assistant,
single_target=True,
)
match_preferences = intent.MatchTargetsPreferences(
area_id=slots.get('preferred_area_id', {}).get('value'),
floor_id=slots.get('preferred_floor_id', {}).get('value'),
)
match_result = intent.async_match_targets(
intent_obj.hass, match_constraints, match_preferences
)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
assert match_result.states
return match_result.states
class XiaoaiPlayText(intent.IntentHandler):
intent_type = 'XiaoaiPlayText'
description = 'Convert text to sound and play on the Xiaoai speaker or Xiaomi TV'
slot_schema = {
vol.Required('message', description='Message content'): intent.non_empty_string,
vol.Optional('name', description='Name of the Xiaoai speaker or Xiaomi TV'): intent.non_empty_string,
vol.Optional('area'): intent.non_empty_string,
vol.Optional('floor'): intent.non_empty_string,
vol.Optional('preferred_area_id'): str,
vol.Optional('preferred_floor_id'): str,
}
platforms = {Platform.MEDIA_PLAYER}
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = match_states_from_slots(intent_obj, slots, [Platform.MEDIA_PLAYER])
state = states[0]
for sta in states:
if 'xiaomi' in state.entity_id:
state = sta
break
message = slots.get('message', {}).get('value')
await hass.services.async_call(
'xiaomi_miot',
'intelligent_speaker',
target={ATTR_ENTITY_ID: state.entity_id},
service_data={
'text': message,
'execute': False,
},
blocking=True,
)
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.ACTION_DONE
response.async_set_results(
success_results=[
intent.IntentResponseTarget(
name=str(state.name),
type=intent.IntentResponseTargetType.ENTITY,
id=state.entity_id,
),
],
)
return response
class XiaoaiExecuteCommand(intent.IntentHandler):
intent_type = 'XiaoaiExecuteCommand'
description = 'Execute commands on the Xiaoai speaker or Xiaomi TV'
slot_schema = {
vol.Required('command', description='Command content'): intent.non_empty_string,
vol.Optional('name', description='Name of the Xiaoai speaker or Xiaomi TV'): intent.non_empty_string,
vol.Optional('area'): intent.non_empty_string,
vol.Optional('floor'): intent.non_empty_string,
vol.Optional('preferred_area_id'): str,
vol.Optional('preferred_floor_id'): str,
}
platforms = {Platform.MEDIA_PLAYER}
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
states = match_states_from_slots(intent_obj, slots, [Platform.MEDIA_PLAYER])
state = states[0]
for sta in states:
if 'xiaomi' in state.entity_id:
state = sta
break
command = slots.get('command', {}).get('value')
await hass.services.async_call(
'xiaomi_miot',
'intelligent_speaker',
target={ATTR_ENTITY_ID: state.entity_id},
service_data={
'text': command,
'execute': True,
},
blocking=True,
)
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.ACTION_DONE
response.async_set_results(
success_results=[
intent.IntentResponseTarget(
name=str(state.name),
type=intent.IntentResponseTargetType.ENTITY,
id=state.entity_id,
),
],
)
return response
@@ -0,0 +1,200 @@
"""Support for Xiaomi lights."""
import logging
from homeassistant.components.light import (
DOMAIN as ENTITY_DOMAIN,
LightEntity as BaseEntity,
LightEntityFeature, # v2022.5
ColorMode,
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ATTR_EFFECT,
)
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiirToggleEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.converters import (
MiotBrightnessConv,
MiotColorTempConv,
MiotRgbColorConv,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
MiotProperty,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
ATTR_COLOR_TEMP = 'color_temp'
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services('ir_light_control'):
if srv.name in ['ir_light_control']:
entities.append(MiirLightEntity(config, srv))
continue
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class LightEntity(XEntity, BaseEntity, RestoreEntity):
_attr_names = None
_brightness_for_on = None
_brightness_for_off = None
def on_init(self):
self._attr_names = {}
self._brightness_for_on = self.custom_config_number('brightness_for_on')
self._brightness_for_off = self.custom_config_number('brightness_for_off')
mode_property = self.custom_config('mode_property', 'mode')
color_property = self.custom_config('color_property', 'color')
color_temp_property = self.custom_config('color_temp_property', 'color_temp')
brightness_property = self.custom_config('brightness_property', 'brightness')
self._attr_color_mode = ColorMode.ONOFF
modes = set()
for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
if prop.in_list(['brightness', brightness_property]) or isinstance(conv, MiotBrightnessConv):
self._attr_names[ATTR_BRIGHTNESS] = attr
self._attr_color_mode = ColorMode.BRIGHTNESS
elif prop.in_list(['color_temperature', color_temp_property]) or isinstance(conv, MiotColorTempConv):
self._attr_color_mode = ColorMode.COLOR_TEMP
modes.add(ColorMode.COLOR_TEMP)
# percentage may be incorrectly declared as kelvin
# e.g. https://home.miot-spec.com/spec/mrbond.airer.m33a
if prop.unit == 'percentage' or prop.range_max() == 100:
self._attr_min_color_temp_kelvin = MiotColorTempConv.percentage_to_kelvin(prop.range_max())
self._attr_max_color_temp_kelvin = MiotColorTempConv.percentage_to_kelvin(prop.range_min())
self._attr_names[ATTR_COLOR_TEMP_KELVIN] = attr
elif prop.unit in ['kelvin']:
self._attr_min_color_temp_kelvin = prop.range_min()
self._attr_max_color_temp_kelvin = prop.range_max()
self._attr_names[ATTR_COLOR_TEMP_KELVIN] = attr
else:
self._attr_min_mireds = prop.range_min()
self._attr_max_mireds = prop.range_max()
self._attr_names[ATTR_COLOR_TEMP] = attr
elif prop.in_list(['color', color_property]) or isinstance(conv, MiotRgbColorConv):
self._attr_names[ATTR_RGB_COLOR] = attr
modes.add(ColorMode.RGB)
elif prop.in_list(['mode', 'color_mode', mode_property]):
self._attr_names[ATTR_EFFECT] = attr
self._attr_effect_list = prop.list_descriptions()
self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_supported_color_modes = modes if modes else {self._attr_color_mode}
def get_state(self) -> dict:
return {
self.attr: self._attr_is_on,
ATTR_BRIGHTNESS: self._attr_brightness,
ATTR_COLOR_TEMP: self._attr_color_temp,
}
def set_state(self, data: dict):
val = data.get(self.attr)
if val is not None:
self._attr_is_on = bool(val)
if (val := data.get(self._attr_names.get(ATTR_BRIGHTNESS))) is not None:
self._attr_brightness = val
if self._brightness_for_on is not None:
self._attr_is_on = val >= self._brightness_for_on
if (val := data.get(self._attr_names.get(ATTR_COLOR_TEMP_KELVIN))) is not None:
if val != self._attr_color_temp_kelvin:
self._attr_color_temp_kelvin = val
self._attr_color_mode = ColorMode.COLOR_TEMP
elif (val := data.get(self._attr_names.get(ATTR_COLOR_TEMP))) is not None:
if val != self._attr_color_temp:
self._attr_color_temp = val
self._attr_color_mode = ColorMode.COLOR_TEMP
if (val := data.get(self._attr_names.get(ATTR_RGB_COLOR))) is not None:
if val != self._attr_rgb_color:
self._attr_rgb_color = val
self._attr_color_mode = ColorMode.RGB
if (val := data.get(self._attr_names.get(ATTR_EFFECT))) is not None:
self._attr_effect = val
async def async_turn_on(self, **kwargs):
dat = {self.attr: True}
if self._brightness_for_on is not None:
dat[self.attr] = self._brightness_for_on
for k, v in kwargs.items():
if attr := self._attr_names.get(k):
dat[attr] = v
await self.device.async_write(dat)
async def async_turn_off(self, **kwargs):
dat = {self.attr: False}
if self._brightness_for_off is not None:
dat[self.attr] = self._brightness_for_off
await self.device.async_write(dat)
XEntity.CLS[ENTITY_DOMAIN] = LightEntity
class MiirLightEntity(MiirToggleEntity, BaseEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
self._act_bright_up = miot_service.get_action('brightness_up')
self._act_bright_dn = miot_service.get_action('brightness_down')
if self._act_bright_up or self._act_bright_dn:
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_brightness = 127
self._supported_features = LightEntityFeature.EFFECT
self._attr_effect_list = []
for a in miot_service.actions.values():
if a.ins:
continue
self._attr_effect_list.append(a.friendly_desc)
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
bright = kwargs.get(ATTR_BRIGHTNESS)
if bright is None:
pass
elif bright > self._attr_brightness and self._act_bright_up:
return await self.async_call_action(self._act_bright_up)
elif bright < self._attr_brightness and self._act_bright_dn:
return await self.async_call_action(self._act_bright_dn)
effect = kwargs.get(ATTR_EFFECT)
if act := self._miot_service.get_action(effect):
return await self.async_call_action(act)
return await super().async_turn_on(**kwargs)
@@ -0,0 +1,22 @@
{
"domain": "xiaomi_miot",
"name": "Xiaomi Miot",
"after_dependencies": ["homekit"],
"codeowners": ["@al-one"],
"config_flow": true,
"dependencies": [
"http",
"intent",
"persistent_notification",
"ffmpeg"
],
"documentation": "https://github.com/al-one/hass-xiaomi-miot",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/al-one/hass-xiaomi-miot/issues",
"requirements": [
"construct>=2.10.68",
"python-miio>=0.5.12",
"micloud>=0.5"
],
"version": "1.1.2"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
"""Support number entity for Xiaomi Miot."""
import logging
from homeassistant.components.number import (
DOMAIN as ENTITY_DOMAIN,
RestoreNumber,
NumberMode,
)
from homeassistant.helpers.event import async_call_later
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
async_setup_config_entry,
bind_services_to_entries,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class NumberEntity(XEntity, RestoreNumber):
_attr_mode = NumberMode.AUTO
def on_init(self):
if self._miot_property:
self._attr_native_step = self._miot_property.range_step()
self._attr_native_max_value = self._miot_property.range_max()
self._attr_native_min_value = self._miot_property.range_min()
self._attr_native_unit_of_measurement = self._miot_property.unit_of_measurement
def get_state(self) -> dict:
return {self.attr: self._attr_native_value}
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
self._attr_native_value = val
async def async_set_native_value(self, value: float):
await self.device.async_write({self.attr: value})
if self._miot_action:
self._attr_native_value = None
async_call_later(self.hass, 0.5, self.schedule_update_ha_state)
XEntity.CLS[ENTITY_DOMAIN] = NumberEntity
@@ -0,0 +1,238 @@
"""Support remote entity for Xiaomi Miot."""
import logging
import asyncio
from functools import partial
from homeassistant.const import (
CONF_HOST,
CONF_TOKEN,
)
from homeassistant.components import remote, persistent_notification
from homeassistant.components.remote import (
DOMAIN as ENTITY_DOMAIN,
RemoteEntity,
RemoteEntityFeature,
)
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
MiotEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.utils import get_translations, DeviceException
from .core.miot_spec import (
MiotSpec,
)
from .core.xiaomi_cloud import (
MiotCloud,
MiCloudException,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
host = config.get(CONF_HOST)
token = config.get(CONF_TOKEN)
if spec.name in ['remote_control', 'ir_remote_control']:
if 'chuangmi.remote.' in model or 'chuangmi.ir.' in model:
ChuangmiIr = await hass.async_add_executor_job(import_miio_chuangmi_remote)
device = ChuangmiIr(host, token)
entities.append(MiotRemoteEntity(config, spec, device))
elif model in [
'xiaomi.wifispeaker.l05c',
'xiaomi.wifispeaker.lx5a',
'xiaomi.wifispeaker.lx06',
'xiaomi.wifispeaker.oh2p',
'lumi.acpartner.v1',
'lumi.acpartner.v2',
'lumi.acpartner.v3',
'lumi.acpartner.mcn04',
]:
entities.append(MiotRemoteEntity(config, spec))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
def import_miio_chuangmi_remote():
try:
from miio import ChuangmiIr
except (ModuleNotFoundError, ImportError):
from miio.integrations.chuangmi.remote import ChuangmiIr
return ChuangmiIr
class MiotRemoteEntity(MiotEntity, RemoteEntity):
def __init__(self, config, miot_spec: MiotSpec, device=None):
self._miot_spec = miot_spec
super().__init__(miot_service=None, device=device, config=config, logger=_LOGGER)
self._attr_should_poll = False
self._supported_features = RemoteEntityFeature.LEARN_COMMAND
self._translations = get_translations('ir_devices')
async def async_added_to_hass(self):
await super().async_added_to_hass()
did = self.miot_did
mic = self.miot_cloud
irs = []
if did and isinstance(mic, MiotCloud):
rdt = await mic.async_request_api('v2/irdevice/controllers', {'parent_id': did}) or {}
rds = (rdt.get('result') or {}).get('controllers') or []
if not rdt:
dls = await mic.async_get_devices() or []
for d in dls:
if did == d.get('parent_id'):
rds.append(d)
for d in rds:
ird = d.get('did')
rdt = await mic.async_request_api('v2/irdevice/controller/keys', {'did': ird}) or {}
kys = (rdt.get('result') or {}).get('keys', {})
irs.append({
'did': ird,
'name': d.get('name'),
'keys': kys,
})
add_selects = self._add_entities.get('select')
if not kys:
self.logger.info('%s: IR device %s(%s) have no keys: %s', self.name_model, ird, d.get('name'), rdt)
elif add_selects and ird not in self._subs:
from .select import SelectSubEntity
ols = []
for k in kys:
nam = k.get('display_name') or k.get('name')
if not nam:
continue
nam = self._translations.get(nam, nam)
ols.append(nam)
self._subs[ird] = SelectSubEntity(self, ird, option={
'name': d.get('name'),
'entity_id': f'remote_{ird}'.replace('.', '_'),
'options': ols,
'async_select_option': self.async_press_ir_key,
})
add_selects([self._subs[ird]], update_before_add=False)
if irs:
self._state_attrs['ir_devices'] = irs
def is_on(self):
return True
async def async_send_command(self, command, **kwargs):
"""Send commands to a device."""
repeat = kwargs.get(remote.ATTR_NUM_REPEATS, remote.DEFAULT_NUM_REPEATS)
delays = kwargs.get(remote.ATTR_DELAY_SECS, remote.DEFAULT_DELAY_SECS)
did = kwargs.get(remote.ATTR_DEVICE)
for _ in range(repeat):
for cmd in command:
try:
if f'{cmd}'[:4] == 'key:':
ret = await self.async_send_cloud_command(did, cmd)
elif f'{cmd}'.startswith('FE'):
ret = await self.miot_device.async_send('send_ir_code', [cmd])
else:
ret = self.hass.async_add_executor_job(self.miot_device.play, cmd)
self.logger.info('%s: Send IR command %s(%s) result: %s', self.name_model, cmd, kwargs, ret)
except (DeviceException, MiCloudException) as exc:
self.logger.error('%s: Send IR command %s(%s) failed: %s', self.name_model, cmd, kwargs, exc)
await asyncio.sleep(delays)
async def async_send_cloud_command(self, did, command):
key = f'{command}'
if key[:4] == 'key:':
key = key[4:]
try:
key = int(key)
except (TypeError, ValueError):
key = None
if not did or not key:
self.logger.warning('%s: IR command %s to %s invalid for cloud.', self.name_model, command, did)
return False
mic = self.miot_cloud
if not mic:
return False
res = await mic.async_request_api('v2/irdevice/controller/key/click', {
'did': did,
'key_id': key,
}) or {}
if res.get('code'):
self.logger.warning('%s: Send IR command %s(%s) failed: %s', self.name_model, command, did, res)
return res
async def async_learn_command(self, **kwargs):
"""Learn a command from a device."""
if not self.miot_device:
return {'error': f'Not support for {self.model}'}
timeout = int(kwargs.get(remote.ATTR_TIMEOUT) or 30)
res = {}
key = int(kwargs.get(remote.ATTR_DEVICE, 999999))
try:
for idx in range(timeout):
if hasattr(self.miot_device, 'read'):
learn = self.hass.async_add_executor_job(self.miot_device.learn, key)
read = self.hass.async_add_executor_job(self.miot_device.read, key)
else:
learn = self.miot_device.async_send('start_ir_learn', [key])
read = self.miot_device.async_send('get_ir_learn_result')
if idx == 0:
await learn
await asyncio.sleep(1)
res = await read
if isinstance(res, dict) and res.get('code'):
break
if isinstance(res, list) and res and str(res[0]).startswith('FE'):
# lumi.acpartner.v*
res = {'result': res}
break
except Exception as exc:
res = {'error': f'{exc}'}
self.logger.warning(
'%s: Learn command failed, the device ID must between 1 and 1000000. %s',
self.name_model, exc,
)
persistent_notification.async_create(
self.hass,
f'{res}',
'Remote learn result',
f'{DOMAIN}-remote-learn',
)
return res
def delete_command(self, **kwargs):
"""Delete commands from the database."""
raise NotImplementedError()
async def async_press_ir_key(self, select, **kwargs):
key = None
did = kwargs.get('attr')
for d in self._state_attrs.get('ir_devices', []):
if did and did != d.get('did'):
continue
for k in d.get('keys', []):
if select not in [
k.get('display_name'),
k.get('name'),
self._translations.get(k.get('display_name') or k.get('name')),
]:
continue
key = k.get('id')
if key:
return await self.async_send_cloud_command(did, key)
return False
@@ -0,0 +1,32 @@
#!/bin/bash
pwd
url=$1
mp4=${2:-latest.mp4}
m3u8=$(curl -s "$url")
KEY=$(echo "$m3u8" | grep EXT-X-KEY | cut -d'"' -f2 | xargs curl -sL | xxd -p -c 32)
KIV=$(echo "$m3u8" | grep -oP 'IV=0x\w+' | sed 's/IV=0x//')
whitelist="pipe,file,https,tls,tcp,crypto,concat"
temp="temp_segs.txt"
i=0
for line in $(echo "$m3u8" | grep -v '^#'); do
echo "$i $line"
seg="temp_$i.mp4"
ffmpeg -loglevel warning -y \
-protocol_whitelist "$whitelist" \
-decryption_key "$KEY" -decryption_iv "$KIV" \
-i "crypto+$line" \
-c copy "$seg"
if [ -f "$seg" ]; then
echo "file '$seg'" >> $temp
fi
((i++))
done
ffmpeg -loglevel warning -y \
-protocol_whitelist "$whitelist" \
-f concat -safe 0 \
-i "$temp" \
-c copy "$mp4"
rm -vf temp_*
@@ -0,0 +1,116 @@
"""Support select entity for Xiaomi Miot."""
import logging
from homeassistant.components.select import (
DOMAIN as ENTITY_DOMAIN,
SelectEntity as BaseEntity,
)
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.event import async_call_later
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
BaseSubEntity,
async_setup_config_entry,
bind_services_to_entries,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class SelectEntity(XEntity, BaseEntity, RestoreEntity):
_attr_current_option = None
_attr_options = []
def on_init(self):
if self._miot_property:
self._attr_options = self._miot_property.list_descriptions()
if self._miot_action:
self._attr_options.insert(0, '')
if lst := getattr(self.conv, 'options', None):
self._attr_options = lst
self._attr_available = True
def get_state(self) -> dict:
return {self.attr: self._attr_current_option}
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
self._attr_current_option = val
async def async_select_option(self, option: str):
if self._miot_action and option == '':
return
await self.device.async_write({self.attr: option})
if self._miot_action:
self._attr_current_option = ''
async_call_later(self.hass, 0.5, self.schedule_update_ha_state)
XEntity.CLS[ENTITY_DOMAIN] = SelectEntity
class SelectSubEntity(BaseEntity, BaseSubEntity):
def __init__(self, parent, attr, option=None):
BaseSubEntity.__init__(self, parent, attr, option)
self._available = True
self._attr_current_option = None
self._attr_options = self._option.get('options') or []
self._select_option = self._option.get('select_option')
self._async_select_option = self._option.get('async_select_option')
def update(self, data=None):
super().update(data)
self._attr_current_option = self._attr_state
self.schedule_update_ha_state()
def select_option(self, option):
"""Change the selected option."""
if self._select_option:
kws = {
'attr': self._attr,
'option': self._option,
}
if ret := self._select_option(option, **kws):
self._attr_current_option = option
self.schedule_update_ha_state()
return ret
raise NotImplementedError()
async def async_select_option(self, option: str):
"""Change the selected option."""
if self._async_select_option:
kws = {
'attr': self._attr,
'option': self._option,
}
if ret := await self._async_select_option(option, **kws):
self._attr_current_option = option
self.schedule_update_ha_state()
return ret
raise NotImplementedError()
def update_options(self, options: list):
self._attr_options = options
@@ -0,0 +1,636 @@
"""Support for Xiaomi sensors."""
import logging
import time
import json
from typing import cast
from datetime import datetime, timedelta
from functools import cmp_to_key, cached_property
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.sensor import (
DOMAIN as ENTITY_DOMAIN,
SensorEntity as BaseEntity,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.helpers.restore_state import RestoreEntity, RestoredExtraData
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotEntity,
BaseSubEntity,
MiCoordinatorEntity,
MiotCloud,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
)
from .core.utils import local_zone, get_translation
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
entry = HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
cloud = await entry.get_cloud()
if cloud:
if not entry.get_config('disable_message'):
hass.data[DOMAIN]['accounts'].setdefault(cloud.user_id, {})
if not hass.data[DOMAIN]['accounts'][cloud.user_id].get('messenger'):
entity = MihomeMessageSensor(hass, cloud)
await entity.coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN]['accounts'][cloud.user_id]['messenger'] = entity
async_add_entities([entity], update_before_add=False)
if not entry.get_config('disable_scene_history'):
homes = await cloud.async_get_homerooms()
for home in homes:
home_id = home.get('id')
if hass.data[DOMAIN]['accounts'][cloud.user_id].get(f'scene_history_{home_id}'):
continue
entity = MihomeSceneHistorySensor(hass, cloud, home_id, home.get('uid'))
await entity.coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN]['accounts'][cloud.user_id][f'scene_history_{home_id}'] = entity
async_add_entities([entity], update_before_add=False)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services(
'switch_sensor',
'lock', 'door', 'bed', 'walking_pad', 'treadmill',
'fridge_chamber', 'germicidal_lamp', 'vital_signs',
'sterilizer', 'steriliser', 'chair', 'dryer', 'clothes_dryer',
):
if srv.name in ['lock']:
if not srv.get_property('operation_method', 'operation_id'):
continue
elif srv.name in ['door']:
if spec.get_service('lock'):
continue
elif not srv.mapping():
continue
entities.append(MiotSensorEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=False)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
def datetime_with_tzinfo(value):
if isinstance(value, datetime):
pass
elif isinstance(value, str):
value = datetime.fromisoformat(value)
elif isinstance(value, (int, float)):
value = datetime.fromtimestamp(value)
else:
value = cast(datetime, value)
if value.tzinfo is None:
value = value.replace(tzinfo=local_zone())
return value
class SensorEntity(XEntity, BaseEntity, RestoreEntity):
def on_init(self):
self._attr_state_class = self.custom_config('state_class')
self._attr_native_unit_of_measurement = self.custom_config('unit_of_measurement')
if self._miot_property:
if not self._attr_icon:
self._attr_icon = self._miot_property.entity_icon
if not self._attr_device_class:
self._attr_device_class = self._miot_property.device_class
if not self._attr_state_class:
self._attr_state_class = self._miot_property.state_class
if not self._attr_native_unit_of_measurement:
self._attr_native_unit_of_measurement = self._miot_property.unit_of_measurement
def get_state(self) -> dict:
return {self.attr: self._attr_native_value}
def set_state(self, data: dict):
value = self.conv.value_from_dict(data)
prop = self._miot_property
if prop and prop.value_range:
if not prop.range_valid(value):
value = None
if value is not None:
try:
if ratio := self.custom_value_ratio:
value = round(float(value) * ratio, 3)
elif self.state_class:
value = round(float(value), 3)
except (TypeError, ValueError):
value = None
if self.device_class == SensorDeviceClass.TIMESTAMP:
value = datetime_with_tzinfo(value)
self._attr_native_value = value
@cached_property
def custom_value_ratio(self):
return self.custom_config_number('value_ratio') or 0
XEntity.CLS[ENTITY_DOMAIN] = SensorEntity
class MiotSensorEntity(MiotEntity, BaseEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
first_property = None
if len(miot_service.properties) > 0:
first_property = list(miot_service.properties.values() or [])[0].name
self._prop_state = miot_service.get_property(
'status', 'fault', first_property or 'status',
)
if miot_service.name in ['lock']:
self._prop_state = miot_service.get_property('operation_method') or self._prop_state
self._attr_icon = self._miot_service.entity_icon
self._attr_state_class = None
self._attr_native_unit_of_measurement = None
if self._prop_state:
self._name = f'{self.device_name} {self._prop_state.friendly_desc}'
self._attr_icon = self._prop_state.entity_icon or self._attr_icon
self._attr_state_class = self._prop_state.state_class
async def async_added_to_hass(self):
await super().async_added_to_hass()
if prop := self.custom_config('state_property'):
self._prop_state = self._miot_service.get_property_by_full_name(prop) or self._prop_state
if self._prop_state:
self._attr_icon = self._prop_state.entity_icon
self._attr_device_class = self._prop_state.device_class
self._attr_native_unit_of_measurement = self._prop_state.unit_of_measurement
self._state_attrs.update({
'state_property': self._prop_state.full_name,
})
cls = self.custom_config('state_class')
if cls and cls in SensorStateClass:
self._attr_state_class = SensorStateClass(cls)
elif cls in ['', False]:
self._attr_state_class = None
if uom := self.custom_config('unit_of_measurement'):
self._attr_native_unit_of_measurement = uom
async def async_update(self):
await super().async_update()
if not self._available or not self._prop_state:
return
if self._miot_service.name in ['lock'] and self._prop_state.full_name not in self._state_attrs:
if how := self._state_attrs.get('lock_method'):
await self.async_update_attrs({
self._prop_state.full_name: get_translation(how, ['lock_method']),
})
elif edt := self._state_attrs.get('event.11', {}):
if isinstance(edt, dict):
await self.async_update_attrs({
self._prop_state.full_name: edt.get('method'),
})
self._prop_state.description_to_dict(self._state_attrs)
@property
def device_class(self):
"""Return the class of this entity."""
return self.get_device_class(SensorDeviceClass)
@property
def native_value(self):
prop = self._prop_state
if not prop:
return None
key = f'{prop.full_name}_desc'
if key in self._state_attrs:
return f'{self._state_attrs[key]}'.lower()
val = prop.from_device(self.device)
if not prop.range_valid(val):
val = None
return val
class BaseSensorSubEntity(BaseSubEntity, BaseEntity):
def __init__(self, parent, attr, option=None, **kwargs):
kwargs.setdefault('domain', ENTITY_DOMAIN)
self._attr_state_class = None
super().__init__(parent, attr, option, **kwargs)
@property
def native_value(self):
value = self._attr_state
if hasattr(self, '_attr_native_value') and self._attr_native_value is not None:
value = self._attr_native_value
value = get_translation(value, [self._attr])
if self.device_class == SensorDeviceClass.TIMESTAMP:
value = datetime_with_tzinfo(value)
return value
@property
def state_class(self):
return self._attr_state_class
async def async_added_to_hass(self):
await super().async_added_to_hass()
cls = self.custom_config('state_class')
if cls and cls in SensorStateClass:
self._attr_state_class = SensorStateClass(cls)
elif cls in ['', False]:
self._attr_state_class = None
if uom := self.custom_config('unit_of_measurement'):
self._attr_native_unit_of_measurement = uom
def update(self, data=None):
old_state = self._attr_state
super().update(data)
now = datetime.now(tz=local_zone(self.hass))
if self.state_class in ['total_increasing'] and old_state not in [None, '', STATE_UNKNOWN]:
ptm = self._extra_attrs.get('updated_time') or now
if now.strftime('%Y-%m-%d') == ptm.strftime('%Y-%m-%d'):
try:
if (self._attr_state or 0) < old_state:
self._attr_state = old_state
except (TypeError, ValueError) as exc:
_LOGGER.warning(
'%s: Total increasing sensor state error: %s',
self.name_model, [exc, self._attr_state, old_state],
)
if self._attr_state != old_state:
self._extra_attrs['updated_time'] = now
class MihomeMessageSensor(MiCoordinatorEntity, BaseEntity, RestoreEntity):
_filter_homes = None
_exclude_types = None
_has_none_message = False
def __init__(self, hass, cloud: MiotCloud):
self.hass = hass
self.cloud = cloud
self.message = {}
self.entity_id = f'{ENTITY_DOMAIN}.mi_{cloud.user_id}_message'
self._attr_unique_id = f'{DOMAIN}-mihome-message-{cloud.user_id}'
self._attr_name = f'Xiaomi {cloud.user_id} message'
self._attr_icon = 'mdi:message'
self._attr_should_poll = False
self._attr_native_value = None
self._attr_extra_state_attributes = {
'entity_class': self.__class__.__name__,
}
self.coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=self._attr_unique_id,
update_method=self.fetch_latest_message,
update_interval=timedelta(seconds=15),
)
super().__init__(self.coordinator)
async def async_added_to_hass(self):
await super().async_added_to_hass()
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
self._filter_homes = self.custom_config_list('filter_home') or []
self._exclude_types = list(map(lambda x: int(x), self.custom_config_list('exclude_type', [13]) or []))
if sec := self.custom_config_integer('interval_seconds'):
self.coordinator.update_interval = timedelta(seconds=sec)
if restored := await self.async_get_last_extra_data():
self._attr_native_value = restored.as_dict().get('state')
self._attr_extra_state_attributes.update(restored.as_dict().get('attrs', {}))
self._attr_extra_state_attributes['filter_homes'] = self._filter_homes
self._attr_extra_state_attributes['exclude_types'] = self._exclude_types
async def async_will_remove_from_hass(self):
"""Run when entity will be removed from hass.
To be extended by integrations.
"""
await super().async_will_remove_from_hass()
self.hass.data[DOMAIN]['accounts'].get(self.cloud.user_id, {}).pop('messenger', None)
@property
def extra_restore_state_data(self):
"""Return entity specific state data to be restored."""
return RestoredExtraData({
'state': self.native_value,
'attrs': self._attr_extra_state_attributes,
})
async def async_set_message(self, msg):
if msg == self.message:
return
if old := self._attr_native_value:
self._attr_extra_state_attributes['prev_message'] = old
con = msg.get('content')
if tit := msg.get('title'):
if con:
self._attr_native_value = f'{con}: {tit}'
else:
self._attr_native_value = tit
logger = _LOGGER.info if old != self._attr_native_value else _LOGGER.debug
logger('New xiaomi message for %s: %s', self.cloud.user_id, self._attr_native_value)
tim = msg.get('ctime')
params = msg.get('params', {})
body = params.get('body', {})
self._attr_entity_picture = msg.get('img_url') or None
self._attr_extra_state_attributes.update({
'msg_id': msg.get('msg_id'),
'is_new': msg.get('is_new'),
'type': msg.get('type'),
'title': tit,
'content': con,
'user_id': msg.get('uid'),
'ctime': tim,
'timestamp': datetime.fromtimestamp(tim, local_zone()) if tim else None,
'model': params.get('model', body.get('model')),
'device_id': msg.get('did', body.get('did')),
'home_name': msg.get('homeName'),
'room_name': msg.get('roomName'),
'event': body.get('event'),
'event_data': body.get('extra', body.get('value')),
})
async def fetch_latest_message(self):
res = await self.cloud.async_request_api('v2/message/v2/typelist', data={}) or {}
mls = (res.get('result') or {}).get('messages') or []
mls.sort(key=lambda x: x.get('ctime', 0), reverse=False)
prev_time = self._attr_extra_state_attributes.get('ctime')
prev_mid = self._attr_extra_state_attributes.get('msg_id')
msg = {}
for m in mls:
hre = m.get('params', {}).get('body', {}).get('homeRoomExtra', {})
home = hre.get('homeName')
if self._filter_homes and home and home not in self._filter_homes:
continue
typ = m.get('type', 0)
if self._exclude_types and typ in self._exclude_types:
continue
tim = m.get('ctime', 0)
mid = m.get('msg_id', 0)
if prev_time and tim < prev_time:
continue
if prev_mid and mid <= prev_mid:
continue
m['homeName'] = home
m['roomName'] = hre.get('roomName')
msg = m
break
if not mls:
if not self._has_none_message:
# Only raise a warning if there was a failure obtaining the xiaomi message
# Otherwise, a warning will show anytime that there are simply no messages waiting
code = res.get('code')
message = res.get('message')
if code == 0 and message == 'ok':
_LOGGER.debug('Get xiaomi message for %s failed: %s', self.cloud.user_id, res)
else:
_LOGGER.warning('Get xiaomi message for %s failed: %s', self.cloud.user_id, res)
self._has_none_message = True
if msg:
await self.async_set_message(msg)
self.message = msg
self._has_none_message = False
return msg
class MihomeSceneHistorySensor(MiCoordinatorEntity, BaseEntity, RestoreEntity):
MESSAGE_TIMEOUT = 60
UPDATE_INTERVAL = 15
_has_none_message = False
def __init__(self, hass, cloud: MiotCloud, home_id, owner_user_id):
self.hass = hass
self.cloud = cloud
self.home_id = int(home_id)
self.owner_user_id = int(owner_user_id)
self.entity_id = f'{ENTITY_DOMAIN}.mi_{cloud.user_id}_{home_id}_scene_history'
self._attr_unique_id = f'{DOMAIN}-mihome-scene-history-{cloud.user_id}_{home_id}'
self._attr_name = f'Xiaomi {cloud.user_id}_{home_id} Scene History'
self._attr_icon = 'mdi:message'
self._attr_should_poll = False
self._attr_native_value = None
self._attr_extra_state_attributes = {
'entity_class': self.__class__.__name__,
}
self.coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=self._attr_unique_id,
update_method=self.fetch_latest_message,
update_interval=timedelta(seconds=self.UPDATE_INTERVAL),
)
super().__init__(self.coordinator)
async def async_added_to_hass(self):
await super().async_added_to_hass()
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
if sec := self.custom_config_integer('interval_seconds'):
self.coordinator.update_interval = timedelta(seconds=sec)
if restored := await self.async_get_last_extra_data():
restored_dict = restored.as_dict()
attrs = restored_dict.get('attrs', {})
if ts := attrs.get('ts'):
attrs['timestamp'] = datetime.fromtimestamp(ts, local_zone()) if ts else None
_LOGGER.debug(
'xiaomi scene history %s %d, async_added_to_hass restore state: state= %s attrs= %s',
self.cloud.user_id, self.home_id, restored_dict.get('state'), attrs,
)
self._attr_native_value = restored_dict.get('state')
self._attr_extra_state_attributes.update(attrs)
async def async_will_remove_from_hass(self):
"""Run when entity will be removed from hass.
To be extended by integrations.
"""
await super().async_will_remove_from_hass()
self.hass.data[DOMAIN]['accounts'].get(self.cloud.user_id, {}).pop(f'scene_history_{self.home_id}', None)
@property
def extra_restore_state_data(self):
"""Return entity specific state data to be restored."""
return RestoredExtraData({
'state': self.native_value,
'attrs': self._attr_extra_state_attributes,
})
def trim_message(self, msg):
ts = msg.get('time') or int(time.time())
return {
"from": msg.get('from'),
"name": msg.get('name'),
"ts": ts,
'timestamp': datetime.fromtimestamp(ts, local_zone()),
"scene_id": str(msg.get('userSceneId')),
"targets": msg.get('msg', []),
}
@staticmethod
def _cmp_message(a, b):
a_ts, b_ts = a.get('ts', 0), b.get('ts', 0)
if a_ts != b_ts:
return a_ts - b_ts
a_scene_id, b_scene_id = a.get('scene_id', 0), b.get('scene_id', 0)
if a_scene_id < b_scene_id:
return -1
if a_scene_id > b_scene_id:
return 1
return 0
async def async_set_message(self, msg):
self._attr_native_value = msg.get('name')
_LOGGER.debug('New xiaomi scene history for %s %d: %s', self.cloud.user_id, self.home_id, self._attr_native_value)
old = self._attr_extra_state_attributes or {}
self._attr_extra_state_attributes.update({**msg, 'prev_value': old.get('name'), 'prev_scene_id': old.get('scene_id')})
async def fetch_latest_message(self):
res = await self.cloud.async_request_api('scene/history', data={
"home_id": self.home_id,
"uid": int(self.cloud.user_id),
"owner_uid": self.owner_user_id,
"command": "history",
"limit": 15,
}) or {}
messages = [self.trim_message(msg) for msg in (res.get('result') or {}).get('history') or []]
if not messages:
if not self._has_none_message:
_LOGGER.info('Get xiaomi scene history for %s %d failed: %s', self.cloud.user_id, self.home_id, res)
self._has_none_message = True
return {}
messages.sort(key=cmp_to_key(self._cmp_message), reverse=False)
_LOGGER.debug(
'Get xiaomi scene history for %s %d success: prev_timestamp= %d prev_scene_id= %s messages= %s,',
self.cloud.user_id, self.home_id,
self._attr_extra_state_attributes.get('ts') or 0,
self._attr_extra_state_attributes.get('scene_id') or '',
messages,
)
must_after = int(time.time()) - self.MESSAGE_TIMEOUT
for msg in messages:
if msg.get('ts') < must_after:
continue
if self._cmp_message(msg, self._attr_extra_state_attributes) <= 0:
continue
await self.async_set_message(msg)
self._has_none_message = False
return msg
return {}
class XiaoaiConversationSensor(MiCoordinatorEntity, BaseSensorSubEntity):
def __init__(self, parent, hass, option=None):
BaseSensorSubEntity.__init__(self, parent, 'conversation', option)
self.hass = hass
self.conversation = {}
self._available = True
self._attr_native_value = None
self._option.setdefault('icon', 'mdi:account-voice')
self.coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=self.unique_id,
update_method=self.fetch_latest_message,
update_interval=timedelta(seconds=5),
)
super().__init__(self.coordinator)
async def async_added_to_hass(self):
await super().async_added_to_hass()
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
await self.coordinator.async_refresh()
if sec := self.custom_config_integer('interval_seconds'):
self.coordinator.update_interval = timedelta(seconds=sec)
async def fetch_latest_message(self):
mic = self._parent.xiaoai_cloud
dvc = self._parent.xiaoai_device or {}
aid = dvc.get('deviceID')
if not isinstance(mic, MiotCloud) or not aid:
self._available = False
return
api = 'https://userprofile.mina.mi.com/device_profile/v2/conversation'
dat = {
'hardware': dvc.get('hardware', ''),
'timestamp': int(time.time() * 1000),
'limit': 3,
}
cks = {
'deviceId': aid,
}
try:
res = await mic.async_request_api(api, data=dat, method='GET', cookies=cks) or {}
rdt = res.get('data', {})
if not isinstance(rdt, dict):
rdt = json.loads(rdt) or {}
except (TypeError, ValueError, Exception) as exc:
rdt = {}
_LOGGER.warning(
'%s: Got exception while fetch xiaoai conversation: %s',
self.name_model, [aid, exc],
)
mls = rdt.get('records') or []
msg = mls.pop(0) if mls else {}
self.conversation = msg
old = self._attr_native_value
if con := msg.get('query'):
self._state = con
self._attr_native_value = con
logger = _LOGGER.info if old != self._attr_native_value else _LOGGER.debug
logger('%s: New xiaoai conversation: %s', self.name_model, self._attr_native_value)
tim = msg.get('time')
ans = []
for v in msg.get('answers', []):
if not isinstance(v, dict):
continue
typ = v.get('type', '').lower()
v.pop('bitSet', None)
v.get(typ, {}).pop('bitSet', None)
ans.append(v)
self._state_attrs.update({
'content': con,
'answers': ans,
'history': [
v.get('query')
for v in mls
],
'timestamp': datetime.fromtimestamp(tim / 1000, local_zone()) if tim else None,
})
return msg
@@ -0,0 +1,333 @@
send_command:
description: Send miio command.
fields:
entity_id:
description: ID of the entity.
example: light.yeelight_living_e92c
required: true
selector:
entity:
integration: xiaomi_miot
method:
description: Method of the command.
example: set_power
required: true
selector:
text:
params:
description: Params for the method.
example: ['on']
required: true
selector:
object:
get_properties:
description: Get miot properties.
fields:
entity_id:
description: ID of the entity.
example: climate.xiaomi_mc5_374e
required: true
selector:
entity:
integration: xiaomi_miot
mapping:
description: Mapping for properties.
example: '[{"siid": 2, "piid": 1}, {"siid": 3, "piid": 1}]'
required: true
selector:
object:
update_entity:
description: Update to entity state attributes.
default: false
example: true
selector:
boolean:
set_property:
description: Set miot property.
fields:
entity_id:
description: ID of the entity.
example: climate.xiaomi_mc5_374e
required: true
selector:
entity:
integration: xiaomi_miot
field:
description: Field of property.
example: air_conditioner.on
required: true
selector:
text:
value:
description: Value of property.
example: true
required: true
selector:
object:
set_miot_property:
description: Set miot property by siid/piid.
fields:
entity_id:
description: ID of the entity.
example: climate.xiaomi_mc5_374e
required: true
selector:
entity:
integration: xiaomi_miot
siid:
description: Miot service ID.
example: 2
required: true
selector:
number:
min: 1
max: 9999
mode: box
piid:
description: Miot property ID.
example: 1
required: true
selector:
number:
min: 1
max: 9999
mode: box
value:
description: Miot property value.
example: true
required: true
selector:
object:
call_action:
description: Call miot action.
fields:
entity_id:
description: ID of the entity.
example: media_player.xiaoai_x08c
required: true
selector:
entity:
integration: xiaomi_miot
siid:
description: Miot service ID.
example: 3
required: true
selector:
number:
min: 1
max: 9999
mode: box
aiid:
description: Miot action ID.
example: 5
required: true
selector:
number:
min: 1
max: 9999
mode: box
params:
description: Miot action params.
example: "['Turn on light', 1]"
selector:
object:
intelligent_speaker:
description: Play text on Xiaoai speaker / Execute text directive / 小爱TTS及执行语音命令
fields:
entity_id:
description: ID of the entity.
example: media_player.xiaoai_x08c
required: true
selector:
entity:
integration: xiaomi_miot
domain: media_player
text:
description: Text content.
example: Turn on light
required: true
selector:
text:
execute:
description: Execute text directive.
default: false
example: true
selector:
boolean:
silent:
description: Silent execution.
default: false
example: false
selector:
boolean:
xiaoai_wakeup:
description: Wake up the Xiaoai speaker / 唤醒小爱音箱
fields:
entity_id:
description: ID of the xiaoai entity.
example: media_player.xiaoai_x08c
required: true
selector:
entity:
integration: xiaomi_miot
domain: media_player
text:
description: Text content for wake up.
example: Livingroom light
selector:
text:
get_device_data:
description: Get xiaomi device data from cloud.
fields:
entity_id:
description: ID of the entity.
example: sensor.xiaomi_lock
required: true
selector:
entity:
integration: xiaomi_miot
type:
description: Data type.
default: prop
example: prop
selector:
select:
options:
- prop
- event
- prop_cal_day
- prop_cal_week
- prop_cal_month
- store
key:
description: Data key.
example: power
required: true
selector:
text:
time_start:
description: From the unix timestamp.
example: 0
selector:
number:
min: 0
max: 9999999999
mode: box
time_end:
description: To the unix timestamp.
example: 0
selector:
number:
min: 0
max: 9999999999
mode: box
limit:
description: Limit of results.
example: 10
selector:
number:
min: 1
max: 2000
mode: box
group:
description: Results grouping.
default: raw
example: raw
selector:
select:
options:
- raw
- hour
- day
- week
- month
renew_devices:
description: Renew xiaomi devices.
fields:
username:
description: Xiaomi Account ID / Email / Phone.
required: false
selector:
text:
get_token:
description: Get xiaomi device token.
fields:
name:
description: Keyword of device name in Mihome / IP / Model.
required: true
selector:
text:
get_bindkey:
description: Get bindkey for ble device from cloud.
fields:
entity_id:
description: ID of the entity.
example: sensor.lywsd03mmc_temperature
required: true
selector:
entity:
integration: xiaomi_miot
did:
description: Xiaomi device ID.
selector:
text:
request_xiaomi_api:
description: Request xiaomi cloud api.
fields:
entity_id:
description: ID of the entity.
example: sensor.xiaomi_device
required: true
selector:
entity:
integration: xiaomi_miot
api:
description: Xiaomi API path.
example: /home/device_list
required: true
selector:
text:
data:
description: Xiaomi API request data.
example: "{'getVirtualModel':true}"
selector:
object:
method:
description: Request method.
default: POST
example: POST
selector:
select:
options:
- POST
- GET
crypt:
description: Crypt data.
default: true
example: true
selector:
boolean:
sid:
description: Xiaomi service ID.
default: xiaomiio
example: xiaomiio
selector:
select:
options:
- label: Mihome API
value: xiaomiio
- label: Xiaoai API
value: micoapi
- label: i.mi.com
value: i.mi.com
@@ -0,0 +1,78 @@
"""Support for Xiaomi switches."""
import logging
from homeassistant.components.switch import (
DOMAIN as ENTITY_DOMAIN,
SwitchEntity as BaseEntity,
SwitchDeviceClass,
)
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotToggleEntity,
ToggleSubEntity,
async_setup_config_entry,
)
from .core.miot_spec import MiotService
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
class SwitchEntity(XEntity, BaseEntity, RestoreEntity):
def get_state(self) -> dict:
return {self.attr: self._attr_is_on}
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
self._attr_is_on = bool(val)
async def async_turn_on(self):
await self.device.async_write({self.attr: True})
async def async_turn_off(self):
await self.device.async_write({self.attr: False})
XEntity.CLS[ENTITY_DOMAIN] = SwitchEntity
class MiotSwitchEntity(MiotToggleEntity, BaseEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
self._attr_icon = self._miot_service.entity_icon
@property
def device_class(self):
if cls := self.get_device_class(SwitchDeviceClass):
return cls
typ = f'{self.model} {self._miot_service.spec.type}'
if 'outlet' in typ or '.plug.' in typ:
return SwitchDeviceClass.OUTLET
return SwitchDeviceClass.SWITCH
class SwitchSubEntity(ToggleSubEntity, BaseEntity):
def __init__(self, parent, attr='switch', option=None, **kwargs):
kwargs.setdefault('domain', ENTITY_DOMAIN)
super().__init__(parent, attr, option, **kwargs)
def update(self, data=None):
super().update(data)
@@ -0,0 +1,40 @@
"""Provide info to system health."""
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
from .core.utils import async_get_manifest
from .core.xiaomi_cloud import MiotCloud
@callback
def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegistration) -> None:
"""Register system health callbacks."""
register.async_register_info(system_health_info, '/config/integrations')
async def system_health_info(hass):
"""Get info for the info page."""
mic = None
uas = {}
uds = {}
all_devices = {}
for mic in MiotCloud.all_clouds(hass):
uas[mic.user_id] = mic
uds[mic.unique_id] = await mic.async_get_devices_by_key('did') or {}
all_devices.update(uds[mic.unique_id])
api = mic.get_api_url('') if mic else 'https://api.io.mi.com'
api_spec = 'https://miot-spec.org/miot-spec-v2/spec/services'
version = await async_get_manifest(hass, 'version', 'unknown')
data = {
'component_version': version,
'can_reach_server': system_health.async_check_can_reach_url(hass, api),
'can_reach_spec': system_health.async_check_can_reach_url(
hass, api_spec, 'https://home.miot-spec.com/?cant-reach',
),
'logged_accounts': len(uas),
'total_devices': len(all_devices),
}
return data
@@ -0,0 +1,96 @@
"""Support text entity for Xiaomi Miot."""
import logging
from homeassistant.components.text import (
DOMAIN as ENTITY_DOMAIN,
TextEntity as BaseEntity,
)
from . import (
DOMAIN,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
async_setup_config_entry,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
class TextEntity(XEntity, BaseEntity):
_attr_native_value = ''
def get_state(self) -> dict:
return {self.attr: self._attr_native_value}
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
if isinstance(val, list):
val = val[0] if val else None
if val is None:
val = ''
self._attr_native_value = val
async def async_set_value(self, value: str):
if self._miot_action and self._miot_action.name == 'execute_text_directive':
silent = self.custom_config_integer('silent_execution', 0)
silent_prop = self._miot_service.get_property('silent_execution')
if silent_prop:
"""
xiaomi.wifispeaker.07g bool
xiaomi.wifispeaker.16b bool
xiaomi.wifispeaker.l04m 0:On 1:Off
xiaomi.wifispeaker.l05b bool
xiaomi.wifispeaker.l05c bool
xiaomi.wifispeaker.l05g bool
xiaomi.wifispeaker.l06a 0:On 1:Off
xiaomi.wifispeaker.l09a 0:On 1:Off
xiaomi.wifispeaker.l09b bool
xiaomi.wifispeaker.l15a bool
xiaomi.wifispeaker.l16a bool
xiaomi.wifispeaker.l17a bool
xiaomi.wifispeaker.l7a 0:On 1:Off
xiaomi.wifispeaker.lx01 0:On 1:Off
xiaomi.wifispeaker.lx04 0:On 1:Off
xiaomi.wifispeaker.lx05 bool
xiaomi.wifispeaker.lx06 bool
xiaomi.wifispeaker.lx5a bool
xiaomi.wifispeaker.m03a bool
xiaomi.wifispeaker.s12 bool
xiaomi.wifispeaker.x08a 0:On 1:Off
xiaomi.wifispeaker.x08c 0:On 1:Off
xiaomi.wifispeaker.x08e bool
xiaomi.wifispeaker.x10a bool
xiaomi.wifispeaker.x6a bool
xiaomi.wifispeaker.x8f bool
xiaomi.wifispeaker.x8s bool
"""
if silent_prop.value_list:
val = silent_prop.list_value('On' if silent else 'Off')
if val is None:
val = 0 if silent else 1
silent = val
value = [value, silent]
await self.device.async_write({self.attr: value})
if self._miot_action:
self._attr_native_value = ''
self.schedule_update_ha_state()
XEntity.CLS[ENTITY_DOMAIN] = TextEntity
@@ -0,0 +1,135 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Vybrat akci",
"data": {
"action": "Akce"
}
},
"token": {
"description": "Konfigurovat informace o zařízení.",
"data": {
"host": "Místní IP",
"token": "Token",
"name": "Název zařízení",
"scan_interval": "Interval aktualizace stavu (v sekundách)"
}
},
"cloud": {
"description": "Konfigurace cloudového účtu Xiaomi.\n{tip}",
"data": {
"username": "ID účtu / e-mail Xiaomi",
"password": "Heslo",
"captcha": "Captcha",
"server_country": "Umístění serveru MiCloud",
"conn_mode": "Režim připojení zařízení",
"filter_models": "Filtrace zařízení přes model/domácnost/WiFi (pokročilé)"
}
},
"cloud_filter": {
"title": "Filtrovat cloudová zařízení",
"description": "Můžete zahrnout, nebo vyloučit zařízení a integrovat pouze zařízení, která chcete.\n{tip}",
"data": {
"filter_model": "Model filtrovaného zařízení",
"model_list": "Seznam modelů zařízení",
"filter_home_id": "Filtr Domů",
"home_id_list": "Domácí seznam",
"filter_ssid": "Filtr WiFi SSID",
"ssid_list": "Seznam WiFi SSID",
"filter_bssid": "Filtr WiFi BSSID",
"bssid_list": "Seznam WiFi BSSID",
"filter_did": "Filtrované zařízení",
"did_list": "Seznam zařízení"
}
},
"customizing": {
"title": "Přizpůsobení entit / zařízení",
"description": "{tip}",
"data": {
"domain": "Vybrat doménu entity",
"entity": "Vybrat entitu",
"model": "Vybrat model zařízení",
"model_specified": "Zadaný model zařízení (volitelné)",
"only_main_entity": "Pouze hlavní (nadřazené) entity",
"reset_customizes": "Obnovit výchozí přizpůsobení",
"bool2selects": "Přizpůsobte možnosti"
}
}
},
"error": {
"cannot_connect": "Připojení zařízení se nezdařilo",
"cannot_login": "Přihlášení k účtu Mi se nezdařilo, zkontrolujte prosím heslo účtu. Pokud se přihlášení po několika pokusech nezdaří, použijte k přihlášení na oficiální webové stránky Xiaomi (mi.com) jiné zařízení ve stejné síti LAN jako hostitel HA a zkuste to znovu.",
"cannot_reach": "Nelze získat přístup k Xiaomi API",
"tzinfo_error": "Viz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Žádné zařízení v cloudovém účtu Xiaomi"
},
"abort": {
"config_saved": "Konfigurace uložena! [Přizpůsobit znovu](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nNěkteré přizpůsobitelné možnosti vyžadují [obnovení integrace](/developer-tools/yaml), aby se projevily.",
"already_configured": "Zařízení je již nakonfigurováno",
"not_xiaomi_miio": "Nejedná se o zařízení Xiaomi miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Konfigurovat informace o zařízení.",
"data": {
"host": "Místní IP",
"token": "Token",
"scan_interval": "Interval aktualizace stavu (v sekundách)",
"miot_cloud": "Povolit miot cloud (konfigurace účtu v YAML)"
}
},
"cloud": {
"description": "Konfigurace cloudového účtu Xiaomi.\n{tip}",
"data": {
"username": "ID účtu / e-mail Xiaomi",
"password": "Heslo",
"captcha": "Captcha",
"server_country": "Umístění serveru MiCloud",
"conn_mode": "Režim připojení pro zařízení",
"renew_devices": "Vynutit obnovení zařízení",
"disable_message": "Deaktivovat senzor upozornění Mihome"
}
},
"cloud_filter": {
"title": "Filtrovat zařízení v cloudu",
"description": "Můžete zahrnout nebo vyloučit zařízení a integrovat pouze ta, která chcete. Pokud v režimu vyloučení nezaškrtnete žádnou možnost v seznamu, je to ekvivalentní zahrnutí všech možností.\n{tip}",
"data": {
"filter_model": "Model filtrovaného zařízení",
"model_list": "Seznam modelů zařízení",
"filter_home_id": "Filtr Domů",
"home_id_list": "Domácí seznam",
"filter_ssid": "Filtr WiFi SSID",
"ssid_list": "Seznam WiFi SSID",
"filter_bssid": "Filtr WiFi BSSID",
"bssid_list": "Seznam WiFi BSSID",
"filter_did": "Filtrovat zařízení",
"did_list": "Seznam zařízení"
}
}
},
"error": {
"cannot_connect": "Připojení zařízení se nezdařilo",
"cannot_login": "Přihlášení k účtu Mi se nezdařilo, zkontrolujte heslo účtu. ",
"cannot_reach": "Nelze získat přístup k Xiaomi API",
"tzinfo_error": "Viz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Žádné zařízení v cloudovém účtu Xiaomi"
},
"abort": {
"show_customizes": "[![Přidat novou integraci](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Verze komponenty",
"can_reach_server": "Dosažení Xiaomi API Serveru",
"can_reach_spec": "Dosažení MIoT-Spec Serveru",
"logged_accounts": "Přihlášené účty",
"total_devices": "Celkový počet zařízení MiHome"
}
}
}
@@ -0,0 +1,135 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Aktion wählen",
"data": {
"action": "Aktion"
}
},
"token": {
"description": "Geräteinfo konfigurieren.",
"data": {
"host": "Lokale IP",
"token": "Token",
"name": "Gerätename",
"scan_interval": "Status Update Interval (Sekunden)"
}
},
"cloud": {
"description": "Xiaomi Cloud Konto Konfigurieren.\n{tip}",
"data": {
"username": "Xiaomi Konto ID / Email",
"password": "Passwort",
"captcha": "Captcha",
"server_country": "Server Standort der MiCloud",
"conn_mode": "Verbindungsmodus für Gerät",
"filter_models": "Geräte filern nach Modell/Zuhause/WiFi (Erweitert)"
}
},
"cloud_filter": {
"title": "Cloud-Geräte filtern",
"description": "Sie können Geräte ein- oder ausschließen, um nur die gewünschten Geräte zu integrieren. Wenn Sie im Ausschlussmodus keine Option in der Liste aktivieren, entspricht dies dem Einschließen aller Optionen.\n{tip}",
"data": {
"filter_model": "Gerätemodell-Filter",
"model_list": "Gerätemodell-Liste",
"filter_home_id": "Zuhause Filtern",
"home_id_list": "Home-Liste",
"filter_ssid": "WiFi SSID filtern",
"ssid_list": "WiFi SSID Liste",
"filter_bssid": "WiFi BSSID filtern",
"bssid_list": "WiFi BSSID Liste",
"filter_did": "Gerätefilter",
"did_list": "Geräte-Liste"
}
},
"customizing": {
"title": "Entitäten / Geräte anpassen",
"description": "{tip}",
"data": {
"domain": "Entitäts-Domain wählen",
"entity": "Entität wählen",
"model": "Gerätemodell wählen",
"model_specified": "Gerätemodell spezifizieren (Optional)",
"only_main_entity": "Nur Haupt-Entitäten (parent)",
"reset_customizes": "Zurücksetzen auf Standardanpassungen",
"bool2selects": "Optionen anpassen"
}
}
},
"error": {
"cannot_connect": "Verbindung zum Gerät fehlgeschlagen",
"cannot_login": "Login in das Xiaomi Cloud-Konto fehlgeschlagen. Bitte überprüfe das Passwort. Wenn die Anmeldung nach mehreren Versuchen fehlschlägt, verwenden Sie bitte ein anderes Gerät im selben LAN wie der HA-Host, um sich auf der offiziellen Xiaomi-Website (mi.com) anzumelden, und versuchen Sie es erneut.",
"cannot_reach": "Zugriff auf Xiaomi-API nicht möglich",
"tzinfo_error": "Weitere Informationen: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Keine Geräte im Xiaomi Cloud-Konto"
},
"abort": {
"config_saved": "Konfiguration gespeichert! [Erneut anpassen](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nEinige anpassbare Einstellungen müssen [neu geladen werden](/developer-tools/yaml) um wirksam zu werden.",
"already_configured": "Gerät wurde bereits eingerichtet",
"not_xiaomi_miio": "Kein Xiaomi miio/miot-Gerät."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Geräteinfo konfigurieren.",
"data": {
"host": "Lokale IP",
"token": "Token",
"scan_interval": "Status Update Interval (Sekunden)",
"miot_cloud": "Aktiviere miot-Cloud (Konfiguriere Konto in YAML)"
}
},
"cloud": {
"description": "Konfiguriere Xiaomi-Cloud-Konto.\n{tip}",
"data": {
"username": "Xiaomi Konto ID / Email",
"password": "Passwort",
"captcha": "Captcha",
"server_country": "Server-Standort der MiCloud",
"conn_mode": "Verbindungsmodus für Gerät",
"renew_devices": "Geräteerneuerung erzwingen",
"disable_message": "Deaktiviere Mihome-Benachrichtigungssensor"
}
},
"cloud_filter": {
"title": "Cloud-Geräte filtern",
"description": "Sie können Geräte ein- oder ausschließen, um nur die gewünschten Geräte zu integrieren. Wenn Sie im Ausschlussmodus keine Option in der Liste aktivieren, entspricht dies dem Einschließen aller Optionen.\n{tip}",
"data": {
"filter_model": "Gerätemodell-Filter",
"model_list": "Gerätemodell-Liste",
"filter_home_id": "Zuhause filtern",
"home_id_list": "Zuhause-Liste",
"filter_ssid": "WiFi SSID filtern",
"ssid_list": "WiFi SSID-Liste",
"filter_bssid": "WiFi BSSID filtern",
"bssid_list": "WiFi BSSID-Liste",
"filter_did": "Gerät filtern",
"did_list": "Geräteliste"
}
}
},
"error": {
"cannot_connect": "Verbindung zum Gerät fehlgeschlagen",
"cannot_login": "Login in das Xiaomi Cloud-Konto fehlgeschlagen. Bitte überprüfe das Passwort. Wenn die Anmeldung nach mehreren Versuchen fehlschlägt, verwenden Sie bitte ein anderes Gerät im selben LAN wie der HA-Host, um sich auf der offiziellen Xiaomi-Website (mi.com) anzumelden, und versuchen Sie es erneut.",
"cannot_reach": "Zugriff auf Xiaomi-API nicht möglich",
"tzinfo_error": "See: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Keine Geräte im Xiaomi Cloud Konto"
},
"abort": {
"show_customizes": "[![Neue Integration hinzufügen](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Neue Integration hinzufügen](/_my_redirect/config_flow_start?domain=xiaomi_miot) um Anpassungen zu bearbeiten.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Komponenten-Version",
"can_reach_server": "Reach Xiaomi API Server",
"can_reach_spec": "Reach MIoT-Spec Server",
"logged_accounts": "Eingeloggte Konten",
"total_devices": "Gesamtzahl der MiHome-Geräte"
}
}
}
@@ -0,0 +1,135 @@
{
"config":{
"flow_title":"Xiaomi Miot",
"step":{
"user":{
"title":"Επιλέξτε Ενέργεια",
"data":{
"action":"Ενέργεια"
}
},
"token":{
"description":"Ρύθμιση παραμέτρων πληροφοριών συσκευής.",
"data":{
"host":"Τοπική IP",
"token":"Σημείο",
"name":"Όνομα συσκευής",
"scan_interval":"Διάστημα κατάστασης ενημέρωσης (δευτερόλεπτα)"
}
},
"cloud":{
"description":"Διαμορφώστε τον λογαριασμό cloud της Xiaomi.\n{tip}",
"data":{
"username":"Αναγνωριστικό λογαριασμού / email Xiaomi",
"password":"Κωδικός πρόσβασης",
"captcha":"Captcha",
"server_country":"Τοποθεσία διακομιστή του MiCloud",
"conn_mode":"Λειτουργία σύνδεσης για τη συσκευή",
"filter_models":"Φιλτράρισμα συσκευών μέσω μοντέλου/σπιτιού/WiFi (Για προχωρημένους)"
}
},
"cloud_filter":{
"title":"Φιλτράρισμα συσκευών cloud",
"description":"Μπορείτε να συμπεριλάβετε ή να εξαιρέσετε συσκευές για να ενσωματώσετε μόνο τις συσκευές που θέλετε. Στη λειτουργία εξαίρεσης, εάν δεν επιλέξετε καμία επιλογή στη λίστα, είναι ισοδύναμη με τη συμπερίληψη όλων των επιλογών.\n{tip}",
"data":{
"filter_model":"Φιλτράρισμα μοντέλου συσκευής",
"model_list":"Λίστα μοντέλων συσκευών",
"filter_home_id":"Αρχική σελίδα φίλτρου",
"home_id_list":"Αρχική Λίστα",
"filter_ssid":"Φίλτρο WiFi SSID",
"ssid_list":"Λίστα SSID WiFi",
"filter_bssid":"Φιλτράρισμα WiFi BSSID",
"bssid_list":"Λίστα BSSID WiFi",
"filter_did":"Συσκευή φίλτρου",
"did_list":"Λίστα συσκευών"
}
},
"customizing":{
"title":"Προσαρμογή οντοτήτων / συσκευών",
"description":"{tip}",
"data":{
"domain":"Επιλέξτε τομέα οντότητας",
"entity":"Επιλέξτε οντότητα",
"model":"Επιλέξτε μοντέλο συσκευής",
"model_specified":"Καθορισμένο μοντέλο συσκευής (Προαιρετικό)",
"only_main_entity":"Μόνο κύριες (μητρικές) οντότητες",
"reset_customizes":"Επαναφορά στις προεπιλεγμένες προσαρμογές",
"bool2selects":"Προσαρμογή επιλογών"
}
}
},
"error":{
"cannot_connect":"Αποτυχία σύνδεσης της συσκευής",
"cannot_login":"Αποτυχία σύνδεσης στον λογαριασμό cloud xiaomi, ελέγξτε τον κωδικό πρόσβασης. Εάν αποτύχετε πάντα να συνδεθείτε, συνδεθείτε για να mi.com στο ίδιο περιβάλλον με το τρέχον δίκτυο και προσπαθήστε ξανά.",
"cannot_reach":"Δεν είναι δυνατή η πρόσβαση στο API της Xiaomi",
"tzinfo_error":"Βλ.: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices":"Καμία συσκευή στον λογαριασμό cloud xiaomi"
},
"abort":{
"config_saved":"Η διαμόρφωση αποθηκεύτηκε! [Προσαρμογή ξανά] (/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nΟρισμένες προσαρμόσιμες επιλογές απαιτούν [επαναφόρτωση ενοποίησης] (/εργαλεία προγραμματιστή/yaml) για να τεθούν σε ισχύ.",
"already_configured":"Η συσκευή έχει ήδη ρυθμιστεί",
"not_xiaomi_miio":"Δεν είναι συσκευή Xiaomi miio / miot."
}
},
"options":{
"step":{
"user":{
"title":"Xiaomi Miot",
"description":"Ρύθμιση παραμέτρων πληροφοριών συσκευής.",
"data":{
"host":"Τοπική IP",
"token":"Σημείο",
"scan_interval":"Διάστημα κατάστασης ενημέρωσης (δευτερόλεπτα)",
"miot_cloud":"Ενεργοποίηση του miot cloud (Λογαριασμός Config σε YAML)"
}
},
"cloud":{
"description":"Διαμορφώστε τον λογαριασμό cloud της Xiaomi.\n{tip}",
"data":{
"username":"Αναγνωριστικό λογαριασμού / email Xiaomi",
"password":"Κωδικός πρόσβασης",
"captcha":"Κάπτσα",
"server_country":"Τοποθεσία διακομιστή του MiCloud",
"conn_mode":"Λειτουργία σύνδεσης για τη συσκευή",
"renew_devices":"Αναγκαστική ανανέωση συσκευών",
"disable_message":"Απενεργοποιήστε τον αισθητήρα ειδοποιήσεων Mihome"
}
},
"cloud_filter":{
"title":"Φιλτράρισμα συσκευών cloud",
"description":"Μπορείτε να συμπεριλάβετε ή να εξαιρέσετε συσκευές για να ενσωματώσετε μόνο τις συσκευές που θέλετε. Στη λειτουργία εξαίρεσης, εάν δεν επιλέξετε καμία επιλογή στη λίστα, είναι ισοδύναμη με τη συμπερίληψη όλων των επιλογών.\n{tip}",
"data":{
"filter_model":"Φιλτράρισμα μοντέλου συσκευής",
"model_list":"Λίστα μοντέλων συσκευών",
"filter_home_id":"Αρχική σελίδα φίλτρου",
"home_id_list":"Αρχική Λίστα",
"filter_ssid":"Φίλτρο WiFi SSID",
"ssid_list":"Λίστα SSID WiFi",
"filter_bssid":"Φιλτράρισμα WiFi BSSID",
"bssid_list":"Λίστα BSSID WiFi",
"filter_did":"Συσκευή φίλτρου",
"did_list":"Λίστα συσκευών"
}
}
},
"error":{
"cannot_connect":"Αποτυχία σύνδεσης της συσκευής",
"cannot_login":"Η σύνδεση στο λογαριασμό Mi απέτυχε, ελέγξτε τον κωδικό πρόσβασης του λογαριασμού. Εάν η σύνδεση αποτύχει μετά από αρκετές προσπάθειες, χρησιμοποιήστε μια άλλη συσκευή στο ίδιο LAN με τον κεντρικό υπολογιστή HA για να συνδεθείτε στον επίσημο ιστότοπο της Xiaomi (mi.com) και να προσπαθήσετε ξανά.",
"cannot_reach":"Δεν είναι δυνατή η πρόσβαση στο API της Xiaomi",
"tzinfo_error":"Βλ.: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices":"Καμία συσκευή στον λογαριασμό cloud xiaomi"
},
"abort":{
"show_customizes":"[![Προσθήκη νέας ενσωμάτωσης] (https://my.home-assistant.io/badges/config_flow_start.svg)] (/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Προσθήκη νέας ενσωμάτωσης] (/_my_redirect/config_flow_start?domain=xiaomi_miot) για να τροποποιήσετε την προσαρμογή.\n{tip}"
}
},
"system_health":{
"info":{
"component_version":"Έκδοση στοιχείου",
"can_reach_server":"Επικοινωνήστε με τον διακομιστή API της Xiaomi",
"can_reach_spec":"Επικοινωνήστε με τον διακομιστή προδιαγραφών MIoT",
"logged_accounts":"Συνδεδεμένοι λογαριασμοί",
"total_devices":"Σύνολο συσκευών MiHome"
}
}
}
@@ -0,0 +1,536 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Select Action",
"data": {
"action": "Action"
}
},
"token": {
"description": "Config device info.",
"data": {
"host": "Local IP",
"token": "Token",
"name": "Device name",
"scan_interval": "Update state interval (seconds)"
}
},
"cloud": {
"description": "Config xiaomi cloud account.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Password",
"captcha": "Captcha",
"verify_ticket": "Two-factor verification code",
"server_country": "Server location of MiCloud",
"conn_mode": "Connection mode for device",
"trans_options": "Translation property value description",
"filter_models": "Filter devices via model/home/WiFi (Advanced)"
}
},
"cloud_filter": {
"title": "Filter cloud devices",
"description": "You can include or exclude devices to integrate only the devices you want.\n{tip}",
"data": {
"filter_model": "Filter Device Model",
"model_list": "Device Model List",
"filter_home_id": "Filter Home",
"home_id_list": "Home List",
"filter_ssid": "Filter WiFi SSID",
"ssid_list": "WiFi SSID List",
"filter_bssid": "Filter WiFi BSSID",
"bssid_list": "WiFi BSSID List",
"filter_did": "Filter Device",
"did_list": "Device List",
"home_ids": "Filter Home (Leave blank to save)"
},
"data_description": {
"filter_did": "In the exclude mode, if you do not check any option in the list, it is equivalent to including all devices.",
"home_ids": "After checking this option and submitting, you will return to the current page,\nand the device list will refresh and filter out the devices of the checked homes."
}
},
"customizing": {
"title": "Customizing entities / devices",
"description": "{tip}",
"data": {
"domain": "Select entity domain",
"entity": "Select entity",
"model": "Select device model",
"model_specified": "Specified device model (Optional)",
"only_main_entity": "Only main (parent) entities",
"yaml_mode": "YAML Mode",
"yaml_customizes": "Customizes YAML Code",
"reset_customizes": "Reset to default customizes",
"binary_sensor_attributes": "binary_sensor_attributes",
"binary_sensor_properties": "binary_sensor_properties",
"bind_sensor": "bind_sensor",
"bind_xiaoai": "bind_xiaoai",
"brightness_for_off": "brightness_for_off",
"brightness_for_on": "brightness_for_on",
"button_actions": "button_actions",
"button_properties": "button_properties",
"chunk_properties": "chunk_properties",
"close_texts": "close_texts",
"closed_position": "closed_position",
"cloud_delay_update": "cloud_delay_update",
"configuration_entities": "configuration_entities",
"coord_type": "coord_type",
"current_temp_property": "current_temp_property",
"descriptions_for_on": "descriptions_for_on",
"descriptions_for_off": "descriptions_for_off",
"deviated_position": "deviated_position",
"device_class": "device_class",
"diagnostic_entities": "diagnostic_entities",
"disable_location_name": "disable_location_name",
"disable_preset_modes": "disable_preset_modes",
"entity_category": "entity_category",
"exclude_miot_properties": "exclude_miot_properties",
"exclude_miot_services": "exclude_miot_services",
"exclude_type": "exclude_type",
"feeding_measure": "feeding_measure",
"filter_home": "filter_home",
"interval_seconds": "interval_seconds",
"main_miot_services": "main_miot_services",
"miot_did": "miot_did",
"mitv_lan_host": "mitv_lan_host",
"motion_stream_slice": "motion_stream_slice",
"motion_timeout": "motion_timeout",
"number_properties": "number_properties",
"open_texts": "open_texts",
"percentage_property": "percentage_property",
"scanner_properties": "scanner_properties",
"screenshot_compress": "screenshot_compress",
"select_actions": "select_actions",
"select_properties": "select_properties",
"sensor_attributes": "sensor_attributes",
"sensor_properties": "sensor_properties",
"sources_via_apps": "sources_via_apps",
"sources_via_keycodes": "sources_via_keycodes",
"speed_property": "speed_property",
"stat_power_cost_key": "stat_power_cost_key",
"stat_power_cost_type": "stat_power_cost_type",
"state_class": "state_class",
"state_property": "state_property",
"switch_properties": "switch_properties",
"target_position_properties": "target_position_properties",
"television_name": "television_name",
"text_actions": "text_actions",
"turn_on_hvac": "turn_on_hvac",
"unit_of_measurement": "unit_of_measurement",
"value_ratio": "value_ratio",
"video_attribute": "video_attribute",
"yeelight_smooth_off": "yeelight_smooth_off",
"yeelight_smooth_on": "yeelight_smooth_on",
"bool2selects": "Customize options"
}
}
},
"error": {
"cannot_connect": "Failed to connect to the device",
"cannot_login": "Failed to login to the Xiaomi account. Please check your account credentials. If login attempts fail multiple times, please try logging in again on the Xiaomi official website (mi.com) using the same network environment.",
"cannot_reach": "Unable to access the Xiaomi API interface",
"tzinfo_error": "There seems to be an issue with your Hass environment. Please refer to: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "No devices found under this Xiaomi account",
"need_verify": "This login requires security verification. Please open the verification page to send the mobile/email verification code. After receiving the verification code, do not verify it on the webpage. Go back to HomeAssistant and enter it in the input box below."
},
"abort": {
"config_saved": "Configuration saved! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nSome custom options may require [reloading the integration](/developer-tools/yaml) to take effect.",
"already_configured": "This device has already been configured.",
"not_xiaomi_miio": "This device does not support the miio/miot protocol."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Config device info.",
"data": {
"host": "Device IP",
"token": "Token",
"scan_interval": "Status update interval (seconds)",
"miot_cloud": "Enable miot cloud (Config account in YAML first)"
}
},
"cloud": {
"description": "Config xiaomi cloud account.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Password",
"captcha": "Captcha",
"verify_ticket": "Two-factor verification code",
"server_country": "Server location of MiCloud",
"conn_mode": "Connection mode for device",
"renew_devices": "Force renew devices",
"trans_options": "Translation property value description",
"disable_message": "Disable Mihome notification sensor",
"disable_scene_history": "Disable Mihome scene history sensor"
}
},
"cloud_filter": {
"title": "Filter cloud devices",
"description": "You can include or exclude devices to integrate only the devices you want.\n{tip}",
"data": {
"filter_model": "Filter Device Model",
"model_list": "Device Model List",
"filter_home_id": "Filter Home",
"home_id_list": "Home List",
"filter_ssid": "Filter WiFi SSID",
"ssid_list": "WiFi SSID List",
"filter_bssid": "Filter WiFi BSSID",
"bssid_list": "WiFi BSSID List",
"filter_did": "Filter Device",
"did_list": "Device List",
"home_ids": "Filter Home (Leave blank to save)"
},
"data_description": {
"filter_did": "In the exclude mode, if you do not check any option in the list, it is equivalent to including all devices.",
"home_ids": "After checking this option and submitting, you will return to the current page,\nand the device list will refresh and filter out the devices of the checked homes."
}
}
},
"error": {
"cannot_connect": "Failed to connect to the device",
"cannot_login": "Failed to login to the Xiaomi account. Please check your account credentials. If login attempts fail multiple times, please try logging in again on the Xiaomi official website (mi.com) using the same network environment.",
"cannot_reach": "Unable to access the Xiaomi API interface",
"tzinfo_error": "There seems to be an issue with your Hass environment. Please refer to: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "No devices found under this Xiaomi account",
"need_verify": "This login requires security verification. Please open the verification page to send the mobile/email verification code. After receiving the verification code, do not verify it on the webpage. Go back to HomeAssistant and enter it in the input box below."
},
"abort": {
"show_customizes": "[![Add new integration](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Custom component version",
"can_reach_server": "Reach Xiaomi API server",
"can_reach_spec": "Reach MIoT-Spec Server",
"logged_accounts": "Number of logged-in accounts",
"total_devices": "Total number of MiHome devices"
}
},
"entity": {
"button": {
"info": {"name": "Info"}
},
"sensor": {
"clean_area": {"name": "Clean Area"},
"clean_time": {"name": "Clean Time"},
"power_cost_today": {"name": "Energy Today"},
"power_cost_month": {"name": "Energy Month"},
"power_cost_today_2": {"name": "Energy Today"},
"power_cost_month_2": {"name": "Energy Month"},
"props-clean_area": {"name": "Clean Area"},
"props-clean_time": {"name": "Clean Time"},
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Password",
"biological": "Biological",
"key": "Key",
"turntable": "Turntable",
"nfc": "NFC",
"one_time_password": "One-time Password",
"two_step_verification": "Two-step Verification",
"coercion": "Coercion",
"homekit": "Homekit",
"manual": "Manual",
"automatic": "Automatic"
}
},
"lock_action": {
"state": {
"outside_unlock": "Unlock from outside",
"lock": "Lock",
"anti_lock_on": "Enable anti-lock",
"anti_lock_off": "Disable anti-lock",
"inside_unlock": "Unlock from inside",
"lock_inside": "Lock from inside",
"child_lock_on": "Enable child lock",
"child_lock_off": "Disable child lock",
"lock_outside": "Lock from outside"
}
},
"door_state": {
"state": {
"open": "Open",
"close": "Close",
"close_timeout": "Close timeout",
"knock": "Knock",
"breaking": "Break-in",
"stuck": "Stuck"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Auto",
"low": "Low",
"medium": "Medium",
"middle": "Medium",
"high": "High",
"quiet": "Quiet",
"turbo": "Turbo",
"level1": "Level 1",
"level2": "Level 2",
"level3": "Level 3",
"level4": "Level 4",
"level5": "Level 5",
"level6": "Level 6",
"level7": "Level 7"
}
},
"swing_mode": {
"state": {
"off": "Off",
"vertical": "Vertical",
"horizontal": "Horizontal",
"both": "Both"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Send command",
"description": "Send miio command",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"method": {
"name": "Method",
"description": ""
},
"params": {
"name": "Params",
"description": ""
}
}
},
"get_properties": {
"name": "Get properties",
"description": "Get miot properties",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"mapping": {
"name": "Mapping",
"description": ""
},
"update_entity": {
"name": "Update entity",
"description": ""
}
}
},
"set_property": {
"name": "Set property",
"description": "Set miot property",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"field": {
"name": "Field",
"description": ""
},
"value": {
"name": "Value",
"description": ""
}
}
},
"set_miot_property": {
"name": "Set miot property",
"description": "Set miot property by siid/piid",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"piid": {
"name": "",
"description": ""
},
"value": {
"name": "",
"description": ""
}
}
},
"call_action": {
"name": "Call action",
"description": "Call miot action",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"aiid": {
"name": "",
"description": ""
},
"params": {
"name": "",
"description": ""
}
}
},
"intelligent_speaker": {
"name": "Intelligent speaker",
"description": "Intelligent speaker for XiaoAi",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": "Xiaoai Entity ID"
},
"text": {
"name": "Text",
"description": ""
},
"execute": {
"name": "Execute",
"description": ""
},
"silent": {
"name": "Silent",
"description": ""
}
}
},
"xiaoai_wakeup": {
"name": "XiaoAi Wakeup",
"description": "Wakeup the XiaoAi speaker",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": "Xiaoai Entity ID"
},
"text": {
"name": "Text",
"description": ""
}
}
},
"get_device_data": {
"name": "Get device data",
"description": "Get xiaomi device data from cloud",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"type": {
"name": "Type",
"description": ""
},
"key": {
"name": "Key",
"description": ""
},
"time_start": {
"name": "Time start",
"description": ""
},
"time_end": {
"name": "Time end",
"description": ""
},
"limit": {
"name": "Limit",
"description": ""
},
"group": {
"name": "Group",
"description": ""
}
}
},
"renew_devices": {
"name": "Renew devices",
"description": "Renew cached xiaomi devices",
"fields": {
"username": {
"name": "Username",
"description": ""
}
}
},
"get_token": {
"name": "Get token",
"description": "Get xiaomi device token",
"fields": {
"name": {
"name": "Name",
"description": ""
}
}
},
"get_bindkey": {
"name": "Get bindkey",
"description": "Get bindkey for ble device",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"did": {
"name": "DID",
"description": ""
}
}
},
"request_xiaomi_api": {
"name": "Request xiaomi API",
"description": "Request xiaomi API",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"api": {
"name": "API",
"description": ""
},
"data": {
"name": "Data",
"description": ""
},
"method": {
"name": "Method",
"description": ""
},
"crypt": {
"name": "Crypt",
"description": ""
},
"sid": {
"name": "",
"description": ""
}
}
}
}
}
@@ -0,0 +1,210 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Seleccionar Acción",
"data": {
"action": "Acción"
}
},
"token": {
"description": "Configurar información del dispositivo.",
"data": {
"host": "IP Local",
"token": "Token",
"name": "Nombre del dispositivo",
"scan_interval": "Intervalo de actualización del estado (segundos)"
}
},
"cloud": {
"description": "Configurar cuenta en la nube de Xiaomi.\n{tip}",
"data": {
"username": "ID de cuenta / Email de Xiaomi",
"password": "Contraseña",
"captcha": "Captcha",
"server_country": "Ubicación del servidor de MiCloud",
"conn_mode": "Modo de conexión para el dispositivo",
"filter_models": "Filtrar dispositivos por modelo/hogar/WiFi (Avanzado)"
}
},
"cloud_filter": {
"title": "Filtrar dispositivos en la nube",
"description": "Puedes incluir o excluir dispositivos para integrar solo los que desees. En el modo de exclusión, si no seleccionas ninguna opción en la lista, será equivalente a incluir todas las opciones.\n{tip}",
"data": {
"filter_model": "Filtrar modelo de dispositivo",
"model_list": "Lista de modelos de dispositivo",
"filter_home_id": "Filtrar hogar",
"home_id_list": "Lista de hogares",
"filter_ssid": "Filtrar SSID de WiFi",
"ssid_list": "Lista de SSID de WiFi",
"filter_bssid": "Filtrar BSSID de WiFi",
"bssid_list": "Lista de BSSID de WiFi",
"filter_did": "Filtrar dispositivo",
"did_list": "Lista de dispositivos"
}
},
"customizing": {
"title": "Personalizar entidades / dispositivos",
"description": "{tip}",
"data": {
"domain": "Seleccionar dominio de la entidad",
"entity": "Seleccionar entidad",
"model": "Seleccionar modelo de dispositivo",
"model_specified": "Especificar modelo de dispositivo (Opcional)",
"only_main_entity": "Solo entidades principales (padre)",
"reset_customizes": "Restaurar personalizaciones predeterminadas",
"bool2selects": "Opciones personalizadas"
}
}
},
"error": {
"cannot_connect": "No se pudo conectar al dispositivo",
"cannot_login": "No se pudo iniciar sesión en la cuenta de Xiaomi. Por favor, verifica tus credenciales de cuenta. Si los intentos de inicio de sesión fallan varias veces, intenta iniciar sesión nuevamente en el sitio web oficial de Xiaomi (mi.com) usando el mismo entorno de red.",
"cannot_reach": "No se puede acceder a la interfaz de la API de Xiaomi",
"tzinfo_error": "Parece que hay un problema con tu entorno de Hass. Por favor, consulta: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "No se encontraron dispositivos bajo esta cuenta de Xiaomi"
},
"abort": {
"config_saved": "¡Configuración guardada! [Personalizar de nuevo](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlgunas opciones personalizadas pueden requerir [recargar la integración](/developer-tools/yaml) para que surtan efecto.",
"already_configured": "Este dispositivo ya ha sido configurado.",
"not_xiaomi_miio": "Este dispositivo no es compatible con el protocolo miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Configurar información del dispositivo.",
"data": {
"host": "IP del dispositivo",
"token": "Token",
"scan_interval": "Intervalo de actualización de estado (segundos)",
"miot_cloud": "Habilitar la nube de MIoT (Configurar la cuenta en YAML primero)"
}
},
"cloud": {
"description": "Configurar cuenta en la nube de Xiaomi.\n{tip}",
"data": {
"username": "ID de cuenta / Email de Xiaomi",
"password": "Contraseña",
"captcha": "Captcha",
"server_country": "Ubicación del servidor de MiCloud",
"conn_mode": "Modo de conexión para el dispositivo",
"renew_devices": "Forzar renovación de dispositivos",
"disable_message": "Desactivar el sensor de notificaciones de MiHome"
}
},
"cloud_filter": {
"title": "Filtrar dispositivos en la nube",
"description": "Puedes incluir o excluir dispositivos para integrar solo los que desees. En el modo de exclusión, si no seleccionas ninguna opción en la lista, será equivalente a incluir todas las opciones.\n{tip}",
"data": {
"filter_model": "Filtrar modelo de dispositivo",
"model_list": "Lista de modelos de dispositivo",
"filter_home_id": "Filtrar hogar",
"home_id_list": "Lista de hogares",
"filter_ssid": "Filtrar SSID de WiFi",
"ssid_list": "Lista de SSID de WiFi",
"filter_bssid": "Filtrar BSSID de WiFi",
"bssid_list": "Lista de BSSID de WiFi",
"filter_did": "Filtrar dispositivo",
"did_list": "Lista de dispositivos"
}
}
},
"error": {
"cannot_connect": "No se pudo conectar al dispositivo",
"cannot_login": "No se pudo iniciar sesión en la cuenta de Xiaomi. Por favor, verifica tus credenciales de cuenta. Si los intentos de inicio de sesión fallan varias veces, intenta iniciar sesión nuevamente en el sitio web oficial de Xiaomi (mi.com) usando el mismo entorno de red.",
"cannot_reach": "No se puede acceder a la interfaz de la API de Xiaomi",
"tzinfo_error": "Parece que hay un problema con tu entorno de Hass. Por favor, consulta: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "No se encontraron dispositivos bajo esta cuenta de Xiaomi"
},
"abort": {
"show_customizes": "[![Agregar nueva integración](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Agregar una nueva integración](/_my_redirect/config_flow_start?domain=xiaomi_miot) para modificar la personalización.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Versión del componente personalizado",
"can_reach_server": "Acceso al servidor de la API de Xiaomi",
"can_reach_spec": "Acceso al servidor MIoT-Spec",
"logged_accounts": "Número de cuentas iniciadas de sesión",
"total_devices": "Número total de dispositivos MiHome"
}
},
"entity": {
"sensor": {
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Contraseña",
"biological": "Biológico",
"key": "Llave",
"turntable": "Plataforma giratoria",
"nfc": "NFC",
"one_time_password": "Contraseña de un solo uso",
"two_step_verification": "Verificación de dos pasos",
"coercion": "Coacción",
"homekit": "Homekit",
"manual": "Manual",
"automatic": "Automático"
}
},
"lock_action": {
"state": {
"outside_unlock": "Desbloquear desde el exterior",
"lock": "Bloquear",
"anti_lock_on": "Activar bloqueo anti-cierre",
"anti_lock_off": "Desactivar bloqueo anti-cierre",
"inside_unlock": "Desbloquear desde el interior",
"lock_inside": "Bloquear desde el interior",
"child_lock_on": "Activar bloqueo para niños",
"child_lock_off": "Desactivar bloqueo para niños",
"lock_outside": "Bloquear desde el exterior"
}
},
"door_state": {
"state": {
"open": "Abierto",
"close": "Cerrado",
"close_timeout": "Tiempo de espera para cerrar",
"knock": "Golpe",
"breaking": "Intento de intrusión",
"stuck": "Atascado"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Automático",
"low": "Bajo",
"medium": "Medio",
"middle": "Medio",
"high": "Alto",
"quiet": "Silencioso",
"turbo": "Turbo",
"level1": "Nivel 1",
"level2": "Nivel 2",
"level3": "Nivel 3",
"level4": "Nivel 4",
"level5": "Nivel 5",
"level6": "Nivel 6",
"level7": "Nivel 7"
}
},
"swing_mode": {
"state": {
"off": "Desactivado",
"vertical": "Vertical",
"horizontal": "Horizontal",
"both": "Ambos"
}
}
}
}
}
}
}
@@ -0,0 +1,135 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Sélectionner une action",
"data": {
"action": "Action"
}
},
"token": {
"description": "Configurer les informations de l'appareil.",
"data": {
"host": "IP locale",
"token": "Jeton",
"name": "Nom de l'appareil",
"scan_interval": "Intervalle de mise à jour (en secondes)"
}
},
"cloud": {
"description": "Configurer le compte cloud Xiaomi.\n{tip}",
"data": {
"username": "ID / e-mail du compte Xiaomi",
"password": "Mot de passe",
"captcha": "Captcha",
"server_country": "Emplacement du serveur MiCloud",
"conn_mode": "Mode de connexion pour l'appareil",
"filter_models": "Filtrer les appareils par modèle/maison/Wi-Fi (Avancé)"
}
},
"cloud_filter": {
"title": "Filtrer les appareils du cloud",
"description": "Vous pouvez inclure ou exclure des appareils pour n'intégrer que les appareils que vous souhaitez. En mode exclusion, si vous ne cochez aucune option dans la liste, cela équivaut à inclure toutes les options.\n{tip}",
"data": {
"filter_model": "Filtrer par modèle d'appareil",
"model_list": "Liste des modèles d'appareil",
"filter_home_id": "Filtrer par maison",
"home_id_list": "Liste des maisons",
"filter_ssid": "Filtrer par SSID Wi-Fi",
"ssid_list": "Liste des SSID Wi-Fi",
"filter_bssid": "Filtrer par BSSID Wi-Fi",
"bssid_list": "Liste des BSSID Wi-Fi",
"filter_did": "Filtrer par appareil",
"did_list": "Liste des appareils"
}
},
"customizing": {
"title": "Personnaliser les entités / appareils",
"description": "{tip}",
"data": {
"domain": "Sélectionner le domaine de l'entité",
"entity": "Sélectionner l'entité",
"model": "Sélectionner le modèle de l'appareil",
"model_specified": "Modèle d'appareil spécifié (facultatif)",
"only_main_entity": "Uniquement les entités principales (parent)",
"reset_customizes": "Réinitialiser les personnalisations par défaut",
"bool2selects": "Options de personnalisation"
}
}
},
"error": {
"cannot_connect": "Impossible de se connecter à l'appareil",
"cannot_login": "Impossible de se connecter au compte Cloud Xiaomi, veuillez vérifier le mot de passe. Si vous n'arrivez pas à vous connecter, connectez-vous sur mi.com dans le même environnement que le réseau actuel et réessayez.",
"cannot_reach": "Impossible d'accéder à l'API Xiaomi",
"tzinfo_error": "Voir : https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Aucun appareil dans le compte Xiaomi Cloud"
},
"abort": {
"config_saved": "Configuration enregistrée ! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nCertaines options personnalisables nécessitent un [rechargement de l'intégration ](/developer-tools/yaml) pour prendre effet.",
"already_configured": "L'appareil est déjà configuré",
"not_xiaomi_miio": "Pas un appareil Xiaomi miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Configurer les informations de l'appareil.",
"data": {
"host": "Adresse IP locale",
"token": "Jeton",
"scan_interval": "Intervalle de mise à jour (secondes)",
"miot_cloud": "Activer le cloud miot (Configurer le compte dans YAML)"
}
},
"cloud": {
"description": "Configurer le compte cloud Xiaomi.\n{tip}",
"data": {
"username": "Identifiant du compte Xiaomi / Email",
"password": "Mot de passe",
"captcha": "Captcha",
"server_country": "Emplacement du serveur MiCloud",
"conn_mode": "Mode de connexion pour l'appareil",
"renew_devices": "Forcer la mise à jour des appareils",
"disable_message": "Désactiver le capteur de notification Mihome"
}
},
"cloud_filter": {
"title": "Filtrer les appareils du cloud",
"description": "Vous pouvez inclure ou exclure les appareils pour n'intégrer que les appareils que vous souhaitez. En mode exclusion, si vous ne cochez aucune option dans la liste, cela équivaut à inclure toutes les options.\n{tip}",
"data": {
"filter_model": "Filtrer le modèle d'appareil",
"model_list": "Liste des modèles d'appareils",
"filter_home_id": "Filtrer le domicile",
"home_id_list": "Liste des domiciles",
"filter_ssid": "Filtrer le SSID WiFi",
"ssid_list": "Liste des SSID WiFi",
"filter_bssid": "Filtrer le BSSID WiFi",
"bssid_list": "Liste des BSSID WiFi",
"filter_did": "Filtrer l'appareil",
"did_list": "Liste des appareils"
}
}
},
"error": {
"cannot_connect": "Impossible de se connecter à l'appareil",
"cannot_login": "Échec de connexion au compte Xiaomi, veuillez vérifier le mot de passe. Si la connexion échoue plusieurs fois, connectez-vous au site web de mi.com sur un autre appareil du même réseau local que celui de HA et essayez à nouveau.",
"cannot_reach": "Impossible d'accéder à l'API Xiaomi",
"tzinfo_error": "Voir: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Aucun appareil dans le compte cloud Xiaomi"
},
"abort": {
"show_customizes": "[![Ajouter une nouvelle intégration](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Ajouter une nouvelle intégration](/_my_redirect/config_flow_start?domain=xiaomi_miot) pour modifier la personnalisation.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Version de composant",
"can_reach_server": "Connexion serveur Xiaomi API",
"can_reach_spec": "Connexion serveur MIoT-Spec",
"logged_accounts": "Comptes connectés",
"total_devices": "Total appareils MiHome"
}
}
}
@@ -0,0 +1,260 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Válasszon műveletet",
"data": {
"action": "Művelet"
}
},
"token": {
"description": "Konfigurálja az eszközinformációkat.",
"data": {
"host": "Helyi IP",
"token": "Token",
"name": "Eszköz neve",
"scan_interval": "Állapot frissítési időköz (másodperc)"
}
},
"cloud": {
"description": "Konfigurálja a Xiaomi felhőfiókot.\n{tip}",
"data": {
"username": "Xiaomi fiók azonosító / E-mail",
"password": "Jelszó",
"captcha": "Captcha",
"server_country": "MiCloud szerver régió",
"conn_mode": "Kapcsolat módja az eszközhöz",
"filter_models": "Eszközök szűrése modell/otthon/WiFi alapján (speciális)"
}
},
"cloud_filter": {
"title": "Felhő eszközök szűrése",
"description": "Beleveheti vagy kizárhatja az eszközöket, hogy csak azokat az eszközöket integrálja, amelyekiet szeretné. A kizárás módban, ha nem jelöl be semmilyen lehetőséget a listán, az egyenértékű az összes lehetőség beillesztésével. \n{tip}",
"data": {
"filter_model": "Eszközmodell szűrése",
"model_list": "Eszközmodell lista",
"filter_home_id": "Otthon szűrése",
"home_id_list": "Otthon lista",
"filter_ssid": "WiFi SSID szűrése",
"ssid_list": "WiFi SSID lista",
"filter_bssid": "WiFi BSSID szűrése",
"bssid_list": "WiFi BSSID lista",
"filter_did": "Eszköz szűrése",
"did_list": "Eszköz lista"
}
},
"customizing": {
"title": "Entitások / eszközök testreszabása",
"description": "{tip}",
"data": {
"domain": "Válasszon entitás tartományt",
"entity": "Válassza ki az entitást",
"model": "Válassza ki az eszközmodellt",
"model_specified": "Specifikus eszközmodell (opcionális)",
"only_main_entity": "Csak fő (szülő) entitások",
"reset_customizes": "Alapértelmezett testreszabások visszaállítása",
"bool2selects": "Testreszabási lehetőségek"
}
}
},
"error": {
"cannot_connect": "Nem sikerült kapcsolódni az eszközhöz",
"cannot_login": "Nem sikerült bejelentkezni a Xiaomi fiókba. Kérjük, ellenőrizze a fiók adatait. Ha a bejelentkezési kísérletek többször is sikertelenek, próbálkozzon újra a Xiaomi hivatalos weboldalán (mi.com) ugyanazon hálózati környezetben.",
"cannot_reach": "Nem sikerült elérni a Xiaomi API felületet",
"tzinfo_error": "Probléma van a Hass környezetével. Kérjük, olvassa el a következő leírást: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Nincsenek eszközök ezen Xiaomi fiók alatt"
},
"abort": {
"config_saved": "Konfiguráció mentve! [Testreszabás újra](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nNéhány testreszabási lehetőség érvénybe léptetéséhez [újra kell tölteni az integrációt](/developer-tools/yaml).",
"already_configured": "Ez az eszköz már konfigurálva van.",
"not_xiaomi_miio": "Ez az eszköz nem támogatja a miio/miot protokolt."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Konfigurálja az eszközinformációkat.",
"data": {
"host": "Eszköz IP",
"token": "Token",
"scan_interval": "Állapot frissítési időköz (másodperc)",
"miot_cloud": "Miot felhő engedélyezése (Először konfigurálja a fiókot a YAML-ben)"
}
},
"cloud": {
"description": "Konfigurálja a Xiaomi felhőfiókot.\n{tip}",
"data": {
"username": "Xiaomi fiók azonosító / E-mail",
"password": "Jelszó",
"captcha": "Captcha",
"server_country": "MiCloud szerver régió",
"conn_mode": "Kapcsolat módja az eszközhöz",
"renew_devices": "Eszközök kényszerített frissítése",
"disable_message": "Mihome értesítés szenzor letiltása"
}
},
"cloud_filter": {
"title": "Felhő eszközök szűrése",
"description": "Beleveheti vagy kizárhatja az eszközöket, hogy csak azokat az eszközöket integrálja, amelyekiet szeretné. A kizárás módban, ha nem jelöl be semmilyen lehetőséget a listán, az egyenértékű az összes lehetőség beillesztésével. \n{tip}",
"data": {
"filter_model": "Eszközmodell szűrése",
"model_list": "Eszközmodell lista",
"filter_home_id": "Otthon szűrése",
"home_id_list": "Otthon lista",
"filter_ssid": "WiFi SSID szűrése",
"ssid_list": "WiFi SSID lista",
"filter_bssid": "WiFi BSSID szűrése",
"bssid_list": "WiFi BSSID lista",
"filter_did": "Eszköz szűrése",
"did_list": "Eszköz lista"
}
}
},
"error": {
"cannot_connect": "Nem sikerült kapcsolódni az eszközhöz",
"cannot_login": "Nem sikerült bejelentkezni a Xiaomi fiókba. Kérjük, ellenőrizze a fiók adatait. Ha a bejelentkezési kísérletek többször is sikertelenek, próbálkozzon újra a Xiaomi hivatalos weboldalán (mi.com) ugyanazon hálózati környezetben.",
"cannot_reach": "Nem sikerült elérni a Xiaomi API felületet",
"tzinfo_error": "Probléma van a Hass környezetével. Kérjük, olvassa el a következő leírást: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Nincsenek eszközök ezen Xiaomi fiók alatt"
},
"abort": {
"show_customizes": "[![Új integráció hozzáadása](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Új integráció hozzáadása](/_my_redirect/config_flow_start?domain=xiaomi_miot) a testreszabás módosításához.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Egyedi komponens verzió",
"can_reach_server": "Elérheti a Xiaomi API szerverét",
"can_reach_spec": "Elérheti a MIoT-Spec szerverét",
"logged_accounts": "Bejelentkezett fiókok száma",
"total_devices": "Összes MiHome eszköz száma"
}
},
"entity": {
"sensor": {
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Jelszó",
"biological": "Biometrikus",
"key": "Kulcs",
"turntable": "Forgómechanizmus",
"nfc": "NFC",
"one_time_password": "Egyszer használatos jelszó",
"two_step_verification": "Kétlépcsős azonosítás",
"coercion": "Kényszerítés",
"homekit": "Homekit",
"manual": "Kézi",
"automatic": "Automatikus"
}
},
"lock_action": {
"state": {
"outside_unlock": "Kívülről kinyitás",
"lock": "Zárás",
"anti_lock_on": "Anti-lock engedélyezése",
"anti_lock_off": "Anti-lock letiltása",
"inside_unlock": "Belső oldalról kinyitás",
"lock_inside": "Belső oldalról zárás",
"child_lock_on": "Gyermekzár engedélyezése",
"child_lock_off": "Gyermekzár letiltása",
"lock_outside": "Kívülről zárás"
}
},
"door_state": {
"state": {
"open": "Nyitott",
"close": "Zárt",
"close_timeout": "Zárás időkorlátja",
"knock": "Kopogás",
"breaking": "Betörés",
"stuck": "Beragadt"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Automatikus",
"low": "Alacsony",
"medium": "Közepes",
"middle": "Közepes",
"high": "Magas",
"quiet": "Csendes",
"turbo": "Turbo",
"level1": "Szint 1",
"level2": "Szint 2",
"level3": "Szint 3",
"level4": "Szint 4",
"level5": "Szint 5",
"level6": "Szint 6",
"level7": "Szint 7"
}
},
"swing_mode": {
"state": {
"off": "Kikapcsolva",
"vertical": "Függőleges",
"horizontal": "Vízszintes",
"both": "Mindkettő"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Parancs küldése",
"description": "Miio parancs küldése"
},
"get_properties": {
"name": "Tulajdonságok lekérése",
"description": "Miot tulajdonságok lekérése"
},
"set_property": {
"name": "Tulajdonság beállítása",
"description": "Miot tulajdonság beállítása"
},
"set_miot_property": {
"name": "Miot tulajdonság beállítása",
"description": "Miot tulajdonság beállítása siid/piid alapján"
},
"call_action": {
"name": "Művelet végrehajtása",
"description": "Miot művelet végrehajtása"
},
"intelligent_speaker": {
"name": "Intelligens hangszóró",
"description": "Intelligens hangszóró XiaoAi számára"
},
"xiaoai_wakeup": {
"name": "XiaoAi felébresztése",
"description": "XiaoAi hangszóró felébresztése"
},
"get_device_data": {
"name": "Eszközadatok lekérése",
"description": "Xiaomi eszközadatok lekérése a felhőből"
},
"renew_devices": {
"name": "Eszközök megújítása",
"description": "Gyorsítótárban lévő Xiaomi eszközök megújítása"
},
"get_token": {
"name": "Token lekérése",
"description": "Xiaomi eszköz token lekérése"
},
"get_bindkey": {
"name": "Kötési kulcs lekérése",
"description": "Kötési kulcs lekérése BLE eszközhöz"
},
"request_xiaomi_api": {
"name": "Xiaomi API lekérése",
"description": "Xiaomi API lekérése"
}
}
}
@@ -0,0 +1,445 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Seleziona Azione",
"data": {
"action": "Azione"
}
},
"token": {
"description": "Configurazione informazioni dispositivo.",
"data": {
"host": "IP Locale",
"token": "Token",
"name": "Nome Dispositivo",
"scan_interval": "Intervallo (secondi)"
}
},
"cloud": {
"description": "Configurazione account xiaomi cloud.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Password",
"captcha": "Captcha",
"server_country": "Posizione server MiCloud",
"conn_mode": "Modalità di connessione del dispostivo",
"filter_models": "Filtra dispositivi per modello/famiglia/WiFi (Avanzato)"
}
},
"cloud_filter": {
"title": "Filtro dispositivi cloud",
"description": "Puoi includere o escludere dispositivi per integrare solo i dipositivi preferiti. In modalità esclusione, se non si seleziona nessuna opzione, vengono considerate tutte le opzioni come incluse.\n{tip}",
"data": {
"filter_model": "Filtro Modello Dispositivo",
"model_list": "Lista Modelli Dispositivi",
"filter_home_id": "Filtra per casa",
"home_id_list": "Lista case",
"filter_ssid": "Filtro WiFi SSID",
"ssid_list": "WiFi SSID List",
"filter_bssid": "Filtro WiFi BSSID",
"bssid_list": "WiFi BSSID List",
"filter_did": "Filtro Dispositivo",
"did_list": "Lista Dispositivi"
}
},
"customizing": {
"title": "Presonalizzazione entità / dispositivi",
"description": "{tip}",
"data": {
"domain": "Seleziona dominio entità",
"entity": "Seleziona entità",
"model": "Seleziona modello dispositivo",
"model_specified": "Modello dispositivo specificato (opzionale)",
"only_main_entity": "Solo entità principali (parent)",
"reset_customizes": "Ripristina le opzioni predefinite",
"bool2selects": "Presonalizza opzioni"
}
}
},
"error": {
"cannot_connect": "Connessione al dispositivo fallita",
"cannot_login": "Login all'account xiaomi cloud fallito, per favore controlla che la password sia corretta. Seil login continua a fallrie, effettua il login a mi.com inello stesso ambiente del corrente network e riprova.",
"cannot_reach": " Impossibile accedere all'API Xiaomi",
"tzinfo_error": "Guardare: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Nessun dispositivo nell'account xiaomi cloud"
},
"abort": {
"config_saved": "Configurazione salvata! [Personalizza di nuovo](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlcune opzioni di personalizzazione richiedono [reload integration](/developer-tools/yaml) per avere effetto.",
"already_configured": "Il dispositivo è già configurato",
"not_xiaomi_miio": "Non è un dispositivo Xiaomi miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Configurazione informazioni dispositivo.",
"data": {
"host": "IP Locale",
"token": "Token",
"scan_interval": "Intervallo aggiornamento stato (secondi)",
"miot_cloud": "Abilita miot cloud (Configura account in YAML)"
}
},
"cloud": {
"description": "Configurazione xiaomi cloud account.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Password",
"captcha": "Captcha",
"server_country": "Posizione server di MiCloud",
"conn_mode": "Modalità di connessione dispositivo",
"renew_devices": "Forza rinnovo dispositivi",
"disable_message": "Disattiva il sensore di notifica di Mihome",
"disable_scene_history": "Disabilita sensore cronologia scene Mihome"
}
},
"cloud_filter": {
"title": "Filtro dispositivi cloud",
"description": "Puoi includere o escludere dispositivi per integrare solo i dipositivi preferiti. In modalità esclusione, se non si seleziona nessuna opzione, vengono considerate tutte le opzioni come incluse.\n{tip}",
"data": {
"filter_model": "Filtro Modello Dispositivo",
"model_list": "Lista Modelli Dispositivi",
"filter_home_id": "Filtra per casa",
"home_id_list": "Lista case",
"filter_ssid": "Filtro WiFi SSID",
"ssid_list": "WiFi SSID List",
"filter_bssid": "Filtro WiFi BSSID",
"bssid_list": "WiFi BSSID List",
"filter_did": "Filtro Dispositivo",
"did_list": "Lista Dispositivi"
}
}
},
"error": {
"cannot_connect": "Connessione al dispositivo fallita",
"cannot_login": "Login all'account xiaomi cloud fallito, per favore controlla che la password sia corretta. Seil login continua a fallrie, effettua il login a mi.com inello stesso ambiente del corrente network e riprova.",
"cannot_reach": " Impossibile accedere all'API Xiaomi",
"tzinfo_error": "Guardare: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Nessun dispositivo nell'account xiaomi cloud"
},
"abort": {
"show_customizes": "[![Aggiungi nuova integrazione](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Aggiungi nuova integrazione](/_my_redirect/config_flow_start?domain=xiaomi_miot) per modificare la personalizzazione.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Versione Componente",
"can_reach_server": "Raggiunge Server Xiaomi API",
"can_reach_spec": "Raggiunge Server MIoT-Spec",
"logged_accounts": "Accaount Loggati",
"total_devices": "Dispositivi MiHome Totali"
}
},
"entity": {
"sensor": {
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Password",
"biological": "Biologica",
"key": "Chiave",
"turntable": "Giradischi",
"nfc": "NFC",
"one_time_password": "Password monouso",
"two_step_verification": "Verifica in due passaggi",
"coercion": "Coercizione",
"homekit": "Homekit",
"manual": "Manuale",
"automatic": "Automatica"
}
},
"lock_action": {
"state": {
"outside_unlock": "Sblocco dall'esterno",
"lock": "Blocca",
"anti_lock_on": "Attiva anti-lock",
"anti_lock_off": "Disattiva anti-lock",
"inside_unlock": "Sblocco dall'interno",
"lock_inside": "Blocco dall'interno",
"child_lock_on": "Attiva blocco bambini",
"child_lock_off": "Disattiva blocco bambini",
"lock_outside": "Blocco dall'esterno"
}
},
"door_state": {
"state": {
"open": "Aperta",
"close": "Chiusa",
"close_timeout": "Chiusura timeout",
"knock": "Bussata",
"breaking": "Tentativo di effrazione",
"stuck": "Bloccata"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Auto",
"low": "Bassa",
"medium": "Media",
"middle": "Media",
"high": "Alta",
"quiet": "Silenziosa",
"turbo": "Turbo",
"level1": "Livello 1",
"level2": "Livello 2",
"level3": "Livello 3",
"level4": "Livello 4",
"level5": "Livello 5",
"level6": "Livello 6",
"level7": "Livello 7"
}
},
"swing_mode": {
"state": {
"off": "Spento",
"vertical": "Verticale",
"horizontal": "Orizontale",
"both": "Entrambi"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Invia comando",
"description": "Invia comando miio",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"method": {
"name": "Metodo",
"description": ""
},
"params": {
"name": "Parametro",
"description": ""
}
}
},
"get_properties": {
"name": "Ottieni proprietà",
"description": "Ottieni proprietà miot",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"mapping": {
"name": "Mappatura",
"description": ""
},
"update_entity": {
"name": "Update entity",
"description": ""
}
}
},
"set_property": {
"name": "Imposta proprietà",
"description": "Imposta proprietà miot",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"field": {
"name": "Campo",
"description": ""
},
"value": {
"name": "Valore",
"description": ""
}
}
},
"set_miot_property": {
"name": "Imposta proprietà miot",
"description": "Imposta proprietà miot tramite siid/piid",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"piid": {
"name": "",
"description": ""
},
"value": {
"name": "",
"description": ""
}
}
},
"call_action": {
"name": "Chiamata azione",
"description": "Chiama azione miot",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"aiid": {
"name": "",
"description": ""
},
"params": {
"name": "",
"description": ""
}
}
},
"intelligent_speaker": {
"name": "Altoparlante intelligente",
"description": "Altoparlante intelligente per XiaoAi",
"fields": {
"entity_id": {
"name": "ID entità",
"description": "ID entità Xiaoai"
},
"text": {
"name": "Text",
"description": ""
},
"execute": {
"name": "Execute",
"description": ""
},
"silent": {
"name": "Silent",
"description": ""
}
}
},
"xiaoai_wakeup": {
"name": "Sveglia XiaoAi",
"description": "Sveglia l'altoparlante XiaoAi",
"fields": {
"entity_id": {
"name": "ID entità",
"description": "ID entità Xiaoai"
},
"text": {
"name": "Testo",
"description": ""
}
}
},
"get_device_data": {
"name": "Ottieni dati del dispositivo",
"description": "Ottieni dati del dispositivo Xiaomi dal cloud",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"type": {
"name": "Tipo",
"description": ""
},
"key": {
"name": "Chiave",
"description": ""
},
"time_start": {
"name": "Tempo di avvio",
"description": ""
},
"time_end": {
"name": "Tempo di fine",
"description": ""
},
"limit": {
"name": "Limite",
"description": ""
},
"group": {
"name": "Groupo",
"description": ""
}
}
},
"renew_devices": {
"name": "Rinnova dispositivi",
"description": "Rinnova dispositivi Xiaomi in cache",
"fields": {
"username": {
"name": "Username",
"description": ""
}
}
},
"get_token": {
"name": "Ottieni token",
"description": "Ottieni token del dispositivo Xiaomi",
"fields": {
"name": {
"name": "Nome",
"description": ""
}
}
},
"get_bindkey": {
"name": "Ottieni bindkey",
"description": "Ottieni bindkey per dispositivo BLE",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"did": {
"name": "DID",
"description": ""
}
}
},
"request_xiaomi_api": {
"name": "Richiedi API Xiaomi",
"description": "Richiedi API Xiaomi",
"fields": {
"entity_id": {
"name": "ID entità",
"description": ""
},
"api": {
"name": "API",
"description": ""
},
"data": {
"name": "Dati",
"description": ""
},
"method": {
"name": "Metodo",
"description": ""
},
"crypt": {
"name": "Cifratura",
"description": ""
},
"sid": {
"name": "",
"description": ""
}
}
}
}
}
@@ -0,0 +1,135 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Wybierz akcję",
"data": {
"action": "Akcja"
}
},
"token": {
"description": "Skonfiguruj dane urządzenia.",
"data": {
"host": "Lokalny adres IP",
"token": "Token",
"name": "Nazwa urządzenia",
"scan_interval": "Interwał aktualizacji stanu (sekundy)"
}
},
"cloud": {
"description": "Skonfiguruj konto w chmurze Xiaomi.\n{tip}",
"data": {
"username": "Identyfikator konta Xiaomi / Email",
"password": "Hasło",
"captcha": "Kod Captcha",
"server_country": "Lokalizacja serwera MiCloud",
"conn_mode": "Tryb połączenia dla urządzenia",
"filter_models": "Filtruj urządzenia według modelu/domu/WiFi (zaawansowane)"
}
},
"cloud_filter": {
"title": "Filtruj urządzenia w chmurze",
"description": "Możesz uwzględnić lub wykluczyć urządzenia, aby zintegrować tylko wybrane urządzenia. W trybie wykluczania, jeśli nie zaznaczysz żadnej opcji na liście, jest to równoznaczne z włączeniem wszystkich opcji.\n{tip}",
"data": {
"filter_model": "Filtruj model urządzenia",
"model_list": "Lista modeli urządzeń",
"filter_home_id": "Filtruj domy",
"home_id_list": "Lista domów",
"filter_ssid": "Filtruj SSID Wi-Fi",
"ssid_list": "Lista identyfikatorów SSID Wi-Fi",
"filter_bssid": "Filtruj BSSID Wi-Fi",
"bssid_list": "Lista identyfikatorów BSSID Wi-Fi",
"filter_did": "Filtruj urządzenia",
"did_list": "Lista urządzeń"
}
},
"customizing": {
"title": "Dostosowywanie encji / urządzeń",
"description": "{tip}",
"data": {
"domain": "Wybierz domenę encji",
"entity": "Wybierz encję",
"model": "Wybierz model urządzenia",
"model_specified": "Określony model urządzenia (opcjonalnie)",
"only_main_entity": "Wyłącznie encje główne (nadrzędne).",
"reset_customizes": "Zresetuj do ustawień domyślnych",
"bool2selects": "Dostosuj opcje"
}
}
},
"error": {
"cannot_connect": "Nie udało się podłączyć urządzenia",
"cannot_login": "Nie udało się zalogować do konta w chmurze Xiaomi, sprawdź hasło. Jeśli nigdy nie uda Ci się zalogować, zaloguj się do mi.com w tym samym środowisku co bieżąca sieć i spróbuj ponownie.",
"cannot_reach": "Nie można uzyskać dostępu do interfejsu API Xiaomi",
"tzinfo_error": "Zobacz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Brak urządzeń na koncie w chmurze Xiaomi"
},
"abort": {
"config_saved": "Konfiguracja zapisana! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nSome customizable options require [reload integration](/developer-tools/yaml) to take effect.",
"already_configured": "Urządzenie jest już skonfigurowane",
"not_xiaomi_miio": "To nie jest urządzenie Xiaomi miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Skonfiguruj dane urządzenia.",
"data": {
"host": "Lokalny adres IP",
"token": "Token",
"scan_interval": "Interwał aktualizacji stanu (sekundy)",
"miot_cloud": "Włącz chmurę miot (Konfiguruj konto w YAML)"
}
},
"cloud": {
"description": "Skonfiguruj konto w chmurze Xiaomi.\n{tip}",
"data": {
"username": "Identyfikator konta Xiaomi / Email",
"password": "Hasło",
"captcha": "Kod Captcha",
"server_country": "Lokalizacja serwera MiCloud",
"conn_mode": "Tryb połączenia dla urządzenia",
"renew_devices": "Wymuś odnowienie urządzeń",
"disable_message": "Wyłącz sensor powiadomień Mihome"
}
},
"cloud_filter": {
"title": "Filtruj urządzenia w chmurze",
"description": "Możesz uwzględnić lub wykluczyć urządzenia, aby zintegrować tylko wybrane urządzenia. W trybie wykluczania, jeśli nie zaznaczysz żadnej opcji na liście, jest to równoznaczne z włączeniem wszystkich opcji.\n{tip}",
"data": {
"filter_model": "Filtruj model urządzenia",
"model_list": "Lista modeli urządzeń",
"filter_home_id": "Filtruj domy",
"home_id_list": "Lista domów",
"filter_ssid": "Filtruj SSID Wi-Fi",
"ssid_list": "Lista identyfikatorów SSID Wi-Fi",
"filter_bssid": "Filtruj BSSID Wi-Fi",
"bssid_list": "Lista identyfikatorów BSSID Wi-Fi",
"filter_did": "Filtruj urządzenia",
"did_list": "Lista urządzeń"
}
}
},
"error": {
"cannot_connect": "Nie udało się podłączyć urządzenia",
"cannot_login": "Jeśli logowanie nie powiedzie się po kilku próbach, użyj innego urządzenia w tej samej sieci LAN, co host HA, aby zalogować się na oficjalnej stronie Xiaomi (mi.com) i spróbuj ponownie.",
"cannot_reach": "Nie można uzyskać dostępu do interfejsu API Xiaomi",
"tzinfo_error": "Zobacz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Brak urządzeń na koncie w chmurze Xiaomi"
},
"abort": {
"show_customizes": "[![Add new integration](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Wersja komponentu",
"can_reach_server": "Serwer API Xiaomi dostępny",
"can_reach_spec": "Serwer MIoT-Spec dostępny",
"logged_accounts": "Zalogowane konta",
"total_devices": "Całkowita liczba urządzeń MiHome"
}
}
}
@@ -0,0 +1,445 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Selecione a forma de integração",
"data": {
"action": "Opções"
}
},
"token": {
"description": "Config infor do dispositivo.",
"data": {
"host": "IP Local",
"token": "Token",
"name": "Nome do dispositivo",
"scan_interval": "Intervalo de update do estado (segundos)"
}
},
"cloud": {
"description": "Configurar pela conta xiaomi.\n{tip}",
"data": {
"username": "ID de conta Xiaomi / Email",
"password": "Senha",
"captcha": "Captcha",
"server_country": "Região da MiCloud",
"conn_mode": "Modo de conexão com o dispositivo",
"filter_models": "Filtro de dispositivo via model/home/WiFi (Advanced)"
}
},
"cloud_filter": {
"title": "Filtro de dispositivos da nuvem",
"description": "Você pode incluir ou excluir dispositivos para a integrar só o que quiser. No modo de exclusão, se você não marcar nenhuma opção da lista, equivale a incluir todas as opções.\n{tip}",
"data": {
"filter_model": "Filtro de Dispositivos",
"model_list": "Lista de Dispositivos",
"filter_home_id": "Filtro de Casa",
"home_id_list": "Lista de Casa",
"filter_ssid": "Filtro por WiFi SSID",
"ssid_list": "Lista de WiFi SSID",
"filter_bssid": "Filtro WiFi BSSID",
"bssid_list": "Lista de WiFi BSSID",
"filter_did": "Filtrar dispositivos",
"did_list": "Lista de dispositivos"
}
},
"customizing": {
"title": "Customizando entidades / dispositivos",
"description": "{tip}",
"data": {
"domain": "Selecione o domomínio da entidade",
"entity": "Selecione a entidade",
"model": "Selecione o modelo do dispositivo",
"model_specified": "Modelo do dispositivo especificaco (Opcional)",
"only_main_entity": "Apenas entidades principais (pais)",
"reset_customizes": "Reiniciar para customização padrão",
"bool2selects": "Opções de customização"
}
}
},
"error": {
"cannot_connect": "Falha em conectar o dispositivo",
"cannot_login": "Falha no login com a conta xiaomi, verifique a senha. Se está tendo falhas ao fazer o login, faça login em mi.com na mesma rede atual e tente novamente.",
"cannot_reach": "Incapaz de accessar a API Xiaomi",
"tzinfo_error": "Aparentemente há um problema no seu ambiente Hass. Acesse: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Sem dispositivos na conta xiaomi"
},
"abort": {
"config_saved": "Configuração guardada! [Customize novamente](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlgumas opções de customização podem requerir [recarregar a integração](/developer-tools/yaml) para ter efeito.",
"already_configured": "O dispositivo já está configurado",
"not_xiaomi_miio": "Não é um dispositivo Xiaomi miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Configurar info do dispositivo.",
"data": {
"host": "IP Local",
"token": "Token",
"scan_interval": "Intervalo de update do estado (segundos)",
"miot_cloud": "Habilitar miot cloud (Configurar conta no YAML)"
}
},
"cloud": {
"description": "Config conta xiaomi.\n{tip}",
"data": {
"username": "ID de conta Xiaomi / Email",
"password": "Senha",
"captcha": "Captcha",
"server_country": "Região da MiCloud",
"conn_mode": "Modo de conexão com o dispositivo",
"renew_devices": "Forçar renovar dispositivos",
"disable_message": "Desabilitar o sensor de notificação Mihome",
"disable_scene_history": "Desabilitar o histórico do sensor de cena"
}
},
"cloud_filter": {
"title": "Filtro de dispositivos da nuvem",
"description": "Você pode incluir ou excluir dispositivos para a integrar só o que quiser. No modo de exclusão, se você não marcar nenhuma opção da lista, equivale a incluir todas as opções.\n{tip}",
"data": {
"filter_model": "Filtro de Dispositivos",
"model_list": "Lista de Dispositivos",
"filter_home_id": "Filtrar Casa",
"home_id_list": "Lista Casa",
"filter_ssid": "Filtro por WiFi SSID",
"ssid_list": "Lista de WiFi SSID",
"filter_bssid": "Filtro por WiFi BSSID",
"bssid_list": "Lista de WiFi BSSID",
"filter_did": "Filtrar Dispositivos",
"did_list": "Lista de Dispositivos"
}
}
},
"error": {
"cannot_connect": "Falha em conectar o dispositivo",
"cannot_login": "Falha no login com a conta xiaomi, verifique a senha. Se está tendo falhas ao fazer o login, faça login em mi.com na mesma rede atual e tente novamente.",
"cannot_reach": "Incapaz de accessar a API Xiaomi",
"tzinfo_error": "Acesse: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Sem dispositivos na conta xiaomi"
},
"abort": {
"show_customizes": "[![Adicionar nova integração](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Adicionar nova integração](/_my_redirect/config_flow_start?domain=xiaomi_miot) para modificar a customização.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Versião do componente",
"can_reach_server": "Acessar server Xiaomi API",
"can_reach_spec": "Acessar server MIoT-Spec",
"logged_accounts": "Contas logadas",
"total_devices": "Total de dispositivos MiHome"
}
},
"entity": {
"sensor": {
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Senha",
"biological": "Biologico",
"key": "Chave",
"turntable": "Giratória",
"nfc": "NFC",
"one_time_password": "Senha de Única Vez",
"two_step_verification": "Verificação de Dois Passos",
"coercion": "Coerção",
"homekit": "Homekit",
"manual": "Manual",
"automatic": "Automatico"
}
},
"lock_action": {
"state": {
"outside_unlock": "Desbloquear de fora",
"lock": "Bloquear",
"anti_lock_on": "Habilitar anti-bloqueio",
"anti_lock_off": "Desabilitar anti-bloqueio",
"inside_unlock": "Desbloquear de dentro",
"lock_inside": "Bloquear de dentro",
"child_lock_on": "Habilitar bloqueio por criança",
"child_lock_off": "Desabilitar bloqueio por criança",
"lock_outside": "Bloquear de fora"
}
},
"door_state": {
"state": {
"open": "Aberto",
"close": "Fechado",
"close_timeout": "Timeout de Fechamento",
"knock": "Batido",
"breaking": "Arrombado",
"stuck": "Travado"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Auto",
"low": "Baixo",
"medium": "Medio",
"middle": "Meio",
"high": "Alto",
"quiet": "Silencioso",
"turbo": "Turbo",
"level1": "Nível 1",
"level2": "Nível 2",
"level3": "Nível 3",
"level4": "Nível 4",
"level5": "Nível 5",
"level6": "Nível 6",
"level7": "Nível 7"
}
},
"swing_mode": {
"state": {
"off": "Desligado",
"vertical": "Vertical",
"horizontal": "Horizontal",
"both": "Ambos"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Enviar comando",
"description": "Enviar comando miio",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"method": {
"name": "Metodo",
"description": ""
},
"params": {
"name": "Parâmetros",
"description": ""
}
}
},
"get_properties": {
"name": "Obter propriedades",
"description": "Obtem as propriedades miot",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"mapping": {
"name": "Mapeamento",
"description": ""
},
"update_entity": {
"name": "Atualizar entidade",
"description": ""
}
}
},
"set_property": {
"name": "Definir propriedade",
"description": "Define a propriedade miot",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"field": {
"name": "Field",
"description": ""
},
"value": {
"name": "Value",
"description": ""
}
}
},
"set_miot_property": {
"name": "Definir propriedade miot",
"description": "Define a propriedade miot por siid/piid",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"piid": {
"name": "",
"description": ""
},
"value": {
"name": "",
"description": ""
}
}
},
"call_action": {
"name": "Chamar ação",
"description": "Chama uma ação miot",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"aiid": {
"name": "",
"description": ""
},
"params": {
"name": "",
"description": ""
}
}
},
"intelligent_speaker": {
"name": "Caixa de som inteligenter",
"description": "Caixa de som inteligente para XiaoAi",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": "ID da Entidade Xiaoai"
},
"text": {
"name": "Texto",
"description": ""
},
"execute": {
"name": "Executar",
"description": ""
},
"silent": {
"name": "Silenciar",
"description": ""
}
}
},
"xiaoai_wakeup": {
"name": "Despertar XiaoA",
"description": "Despertar a caixa de som XiaoAi",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": "ID da Entidade Xiaoai"
},
"text": {
"name": "Texto",
"description": ""
}
}
},
"get_device_data": {
"name": "Obter dados do dispositivo",
"description": "Obtem dados do dispositivo pela cloud",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"type": {
"name": "Tipo",
"description": ""
},
"key": {
"name": "Chave",
"description": ""
},
"time_start": {
"name": "Hora de início",
"description": ""
},
"time_end": {
"name": "Hora de finalização",
"description": ""
},
"limit": {
"name": "Limite",
"description": ""
},
"group": {
"name": "Gropo",
"description": ""
}
}
},
"renew_devices": {
"name": "Renovar dispositivos",
"description": "Renovar dispositivos xiaomi do cache",
"fields": {
"username": {
"name": "Nome de usuário",
"description": ""
}
}
},
"get_token": {
"name": "Obter token",
"description": "Obtem o token do dispositivo xiaomi",
"fields": {
"name": {
"name": "Nome",
"description": ""
}
}
},
"get_bindkey": {
"name": "Obter bindkey",
"description": "Obtem o bindkey para dispositivo ble",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"did": {
"name": "DID",
"description": ""
}
}
},
"request_xiaomi_api": {
"name": "Solicitar API xiaomi",
"description": "Solicita API xiaomi",
"fields": {
"entity_id": {
"name": "ID da Entidade",
"description": ""
},
"api": {
"name": "API",
"description": ""
},
"data": {
"name": "Dado",
"description": ""
},
"method": {
"name": "Metodo",
"description": ""
},
"crypt": {
"name": "Crypt",
"description": ""
},
"sid": {
"name": "",
"description": ""
}
}
}
}
}
@@ -0,0 +1,132 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Выберите действие",
"data": {
"action": "Действие"
}
},
"token": {
"description": "Конфигурация устройства.",
"data": {
"host": "Локальный IP-адрес",
"token": "Токен",
"name": "Имя устройства",
"scan_interval": "Интервал обновления (секунды)"
}
},
"cloud": {
"description": "Настройка облачного аккаунта Сяоми.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Пароль",
"captcha": "Captcha",
"server_country": "Регион сервера MiCloud",
"conn_mode": "Режим подключения устройства",
"filter_models": "Фильтровать устройства по модели/семья/WiFi (Продвинутые настр.)"
}
},
"cloud_filter": {
"title": "Фильтрация облачных устройств",
"description": "Вы можете включать или исключать устройства которые хотите интегрировать. В режиме исключения, если не выбрать ни одного устройства - добавятся все.\n{tip}",
"data": {
"filter_model": "Режим фильтрации по устройствам",
"model_list": "Список модели устройств",
"filter_ssid": "Фильтр WiFi SSID",
"home_id_list": "Домашний список",
"ssid_list": "Список WiFi SSID",
"filter_bssid": "Фильтр WiFi BSSID",
"bssid_list": "Список WiFi BSSID",
"filter_did": "Фильтр устройств",
"did_list": "Список устройств"
}
},
"customizing": {
"title": "Изменение сущностей / устройств",
"description": "{tip}",
"data": {
"domain": "Выберите домен сущности",
"entity": "Выберите сущность",
"model": "Выберите модель устройств",
"model_specified": "Особая модель устройства (Дополнительно)",
"only_main_entity": "Только основные (родительские) сущности",
"reset_customizes": "Сбросить настройки по умолчанию",
"bool2selects": "Настройка опций"
}
}
},
"error": {
"cannot_connect": "Не удалось подключиться к устройству",
"cannot_login": "Не удалось подключиться к аккаунту Сяоми. Проверьте пароль. При повторных ошибках авторизируйтесь на mi.com в той же среде и попробуйте снова.",
"cannot_reach": "Нет доступа к API Сяоми",
"tzinfo_error": "Перейдите по ссылке: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Не найдено устройств в облаке Сяоми"
},
"abort": {
"config_saved": "Конфигурация сохранена! [Настроить снова](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nНекоторые опции требуют перезагрузки конфигурации [reload integration](/developer-tools/yaml) для эффекта.",
"already_configured": "Устройство уже сконфигурировано",
"not_xiaomi_miio": "Не является устройством miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Конфигурация устройства.",
"data": {
"host": "Локальный IP-адрес",
"token": "Токен",
"scan_interval": "Интервал обновления (секунды)",
"miot_cloud": "Enable miot cloud (Config account in YAML)"
}
},
"cloud": {
"description": "Конфигурация облачного аккаунта Сяоми.\n{tip}",
"data": {
"username": "Xiaomi Account ID / Email",
"password": "Пароль",
"captcha": "Captcha",
"server_country": "Регион сервера MiCloud",
"conn_mode": "Режим подключения устройств",
"renew_devices": "Заставлять устройства обновляться",
"disable_message": "Выключить датчик уведомлений от Mi Home"
}
},
"cloud_filter": {
"title": "Фильтрация облачных устройств",
"description": "Вы можете включать или исключать устройства которые хотите интегрировать. В режиме исключения, если не выбрать ни одного устройства - добавятся все.\n{tip}",
"data": {
"filter_model": "Режим фильтрации по устройствам",
"model_list": "Список модели устройств",
"filter_ssid": "Фильтр WiFi SSID",
"ssid_list": "Список WiFi SSID",
"filter_bssid": "Фильтр WiFi BSSID",
"bssid_list": "СписокWiFi BSSID",
"filter_did": "Фильтр устройств",
"did_list": "Список устройств"
}
}
},
"error": {
"cannot_connect": "Не удалость подключиться к устройству",
"cannot_login": "Не удалось полключиться к аккаунту Сяоми. Проверьте пароль. При повторных ошибках авторизируйтесь на mi.com в той же среде и попробуйте снова.",
"cannot_reach": "Нет доступа к API Сяоми",
"tzinfo_error": "Перейдите по ссылке: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Не найдено устройств в облаке Сяоми"
},
"abort": {
"show_customizes": "[![Add new integration](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Версия компонента",
"can_reach_server": "Доступность API сервера Сяоми",
"can_reach_spec": "Доступность MIoT-Spec сервера",
"logged_accounts": "Авторизованные аккаунты",
"total_devices": "Общее количество устройств MiHome"
}
}
}
@@ -0,0 +1,474 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Eylem Seçin",
"data": {
"action": "Eylem"
}
},
"token": {
"description": "Cihaz bilgilerini yapılandırın.",
"data": {
"host": "Yerel IP",
"token": "Token",
"name": "Cihaz adı",
"scan_interval": "Durum güncelleme aralığı (saniye)"
}
},
"cloud": {
"description": "Xiaomi bulut hesabını yapılandırın.\n{tip}",
"data": {
"username": "Xiaomi Hesap ID / E-posta",
"password": "Şifre",
"captcha": "Doğrulama Kodu",
"verify_ticket": "İki faktörlü doğrulama kodu",
"server_country": "MiCloud sunucu konumu",
"conn_mode": "Cihaz bağlantı modu",
"trans_options": "Özellik değeri açıklamasını çevir",
"filter_models": "Cihazları model/ev/WiFi ile filtrele (Gelişmiş)"
}
},
"cloud_filter": {
"title": "Bulut cihazlarını filtrele",
"description": "Yalnızca istediğiniz cihazları entegre etmek için cihazları dahil edebilir veya hariç tutabilirsiniz.\n{tip}",
"data": {
"filter_model": "Cihaz Modelini Filtrele",
"model_list": "Cihaz Model Listesi",
"filter_home_id": "Evi Filtrele",
"home_id_list": "Ev Listesi",
"filter_ssid": "WiFi SSID Filtrele",
"ssid_list": "WiFi SSID Listesi",
"filter_bssid": "WiFi BSSID Filtrele",
"bssid_list": "WiFi BSSID Listesi",
"filter_did": "Cihazı Filtrele",
"did_list": "Cihaz Listesi",
"home_ids": "Evi Filtrele (Kaydetmek için boş bırakın)"
},
"data_description": {
"filter_did": "Hariç tutma modunda, listeden herhangi bir seçenek işaretlemezseniz, tüm cihazları dahil etmekle eşdeğerdir.",
"home_ids": "Bu seçeneği işaretleyip gönderdikten sonra mevcut sayfaya döneceksiniz,\ncihaz listesi yenilenecek ve işaretlenen evlerin cihazları filtrelenecektir."
}
},
"customizing": {
"title": "Varlıkları / cihazları özelleştirme",
"description": "{tip}",
"data": {
"domain": "Varlık alanını seçin",
"entity": "Varlık seçin",
"model": "Cihaz modelini seçin",
"model_specified": "Belirli cihaz modeli (İsteğe bağlı)",
"only_main_entity": "Yalnızca ana (üst) varlıklar",
"yaml_mode": "YAML Modu",
"yaml_customizes": "Özelleştirmeler YAML Kodu",
"reset_customizes": "Varsayılan özelleştirmelere sıfırla",
"bool2selects": "Seçenekleri özelleştir"
}
}
},
"error": {
"cannot_connect": "Cihaza bağlanılamadı",
"cannot_login": "Xiaomi hesabına giriş yapılamadı. Lütfen hesap bilgilerinizi kontrol edin. Giriş denemeleri birden fazla kez başarısız olursa, lütfen aynı ağ ortamında Xiaomi resmi web sitesinden (mi.com) tekrar giriş yapmayı deneyin.",
"cannot_reach": "Xiaomi API arayüzüne erişilemiyor",
"tzinfo_error": "Hass ortamınızda bir sorun var gibi görünüyor. Lütfen şu adrese bakın: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Bu Xiaomi hesabında cihaz bulunamadı",
"need_verify": "Bu giriş güvenlik doğrulaması gerektiriyor. Lütfen doğrulama sayfasını açarak mobil/e-posta doğrulama kodunu gönderin. Doğrulama kodunu aldıktan sonra web sayfasında doğrulamayın. HomeAssistant'a dönün ve aşağıdaki giriş kutusuna girin."
},
"abort": {
"config_saved": "Yapılandırma kaydedildi! [Tekrar özelleştir](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nBazı özel seçeneklerin etkili olması için [entegrasyonun yeniden yüklenmesi](/developer-tools/yaml) gerekebilir.",
"already_configured": "Bu cihaz zaten yapılandırılmış.",
"not_xiaomi_miio": "Bu cihaz miio/miot protokolünü desteklemiyor."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Cihaz bilgilerini yapılandırın.",
"data": {
"host": "Cihaz IP",
"token": "Token",
"scan_interval": "Durum güncelleme aralığı (saniye)",
"miot_cloud": "Miot bulutu etkinleştir (Önce YAML'da hesabı yapılandırın)"
}
},
"cloud": {
"description": "Xiaomi bulut hesabını yapılandırın.\n{tip}",
"data": {
"username": "Xiaomi Hesap ID / E-posta",
"password": "Şifre",
"captcha": "Doğrulama Kodu",
"verify_ticket": "İki faktörlü doğrulama kodu",
"server_country": "MiCloud sunucu konumu",
"conn_mode": "Cihaz bağlantı modu",
"renew_devices": "Cihazları zorla yenile",
"trans_options": "Özellik değeri açıklamasını çevir",
"disable_message": "Mihome bildirim sensörünü devre dışı bırak",
"disable_scene_history": "Mihome sahne geçmişi sensörünü devre dışı bırak"
}
},
"cloud_filter": {
"title": "Bulut cihazlarını filtrele",
"description": "Yalnızca istediğiniz cihazları entegre etmek için cihazları dahil edebilir veya hariç tutabilirsiniz.\n{tip}",
"data": {
"filter_model": "Cihaz Modelini Filtrele",
"model_list": "Cihaz Model Listesi",
"filter_home_id": "Evi Filtrele",
"home_id_list": "Ev Listesi",
"filter_ssid": "WiFi SSID Filtrele",
"ssid_list": "WiFi SSID Listesi",
"filter_bssid": "WiFi BSSID Filtrele",
"bssid_list": "WiFi BSSID Listesi",
"filter_did": "Cihazı Filtrele",
"did_list": "Cihaz Listesi",
"home_ids": "Evi Filtrele (Kaydetmek için boş bırakın)"
},
"data_description": {
"filter_did": "Hariç tutma modunda, listeden herhangi bir seçenek işaretlemezseniz, tüm cihazları dahil etmekle eşdeğerdir.",
"home_ids": "Bu seçeneği işaretleyip gönderdikten sonra mevcut sayfaya döneceksiniz,\ncihaz listesi yenilenecek ve işaretlenen evlerin cihazları filtrelenecektir."
}
}
},
"error": {
"cannot_connect": "Cihaza bağlanılamadı",
"cannot_login": "Xiaomi hesabına giriş yapılamadı. Lütfen hesap bilgilerinizi kontrol edin. Giriş denemeleri birden fazla kez başarısız olursa, lütfen aynı ağ ortamında Xiaomi resmi web sitesinden (mi.com) tekrar giriş yapmayı deneyin.",
"cannot_reach": "Xiaomi API arayüzüne erişilemiyor",
"tzinfo_error": "Hass ortamınızda bir sorun var gibi görünüyor. Lütfen şu adrese bakın: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Bu Xiaomi hesabında cihaz bulunamadı",
"need_verify": "Bu giriş güvenlik doğrulaması gerektiriyor. Lütfen doğrulama sayfasını açarak mobil/e-posta doğrulama kodunu gönderin. Doğrulama kodunu aldıktan sonra web sayfasında doğrulamayın. HomeAssistant'a dönün ve aşağıdaki giriş kutusuna girin."
},
"abort": {
"show_customizes": "[![Yeni entegrasyon ekle](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\nÖzelleştirmeyi değiştirmek için [yeni bir entegrasyon ekleyin](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Özel bileşen sürümü",
"can_reach_server": "Xiaomi API sunucusuna erişim",
"can_reach_spec": "MIoT-Spec Sunucusuna erişim",
"logged_accounts": "Giriş yapılmış hesap sayısı",
"total_devices": "Toplam MiHome cihaz sayısı"
}
},
"entity": {
"button": {
"info": {"name": "Bilgi"}
},
"sensor": {
"clean_area": {"name": "Temizlenen Alan"},
"clean_time": {"name": "Temizlik Süresi"},
"power_cost_today": {"name": "Bugünkü Enerji"},
"power_cost_month": {"name": "Aylık Enerji"},
"power_cost_today_2": {"name": "Bugünkü Enerji"},
"power_cost_month_2": {"name": "Aylık Enerji"},
"props-clean_area": {"name": "Temizlenen Alan"},
"props-clean_time": {"name": "Temizlik Süresi"},
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Şifre",
"biological": "Biyometrik",
"key": "Anahtar",
"turntable": "Döner Kadran",
"nfc": "NFC",
"one_time_password": "Tek Kullanımlık Şifre",
"two_step_verification": "İki Adımlı Doğrulama",
"coercion": "Zorlama",
"homekit": "Homekit",
"manual": "Manuel",
"automatic": "Otomatik"
}
},
"lock_action": {
"state": {
"outside_unlock": "Dışarıdan kilidi aç",
"lock": "Kilitle",
"anti_lock_on": "Anti-kilidi etkinleştir",
"anti_lock_off": "Anti-kilidi devre dışı bırak",
"inside_unlock": "İçeriden kilidi aç",
"lock_inside": "İçeriden kilitle",
"child_lock_on": "Çocuk kilidini etkinleştir",
"child_lock_off": "Çocuk kilidini devre dışı bırak",
"lock_outside": "Dışarıdan kilitle"
}
},
"door_state": {
"state": {
"open": "Açık",
"close": "Kapalı",
"close_timeout": "Kapanma zaman aşımı",
"knock": "Kapı çalındı",
"breaking": "Zorla giriş",
"stuck": "Sıkışmış"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Otomatik",
"low": "Düşük",
"medium": "Orta",
"middle": "Orta",
"high": "Yüksek",
"quiet": "Sessiz",
"turbo": "Turbo",
"level1": "Seviye 1",
"level2": "Seviye 2",
"level3": "Seviye 3",
"level4": "Seviye 4",
"level5": "Seviye 5",
"level6": "Seviye 6",
"level7": "Seviye 7"
}
},
"swing_mode": {
"state": {
"off": "Kapalı",
"vertical": "Dikey",
"horizontal": "Yatay",
"both": "Her İkisi"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Komut gönder",
"description": "Miio komutu gönder",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"method": {
"name": "Metod",
"description": ""
},
"params": {
"name": "Parametreler",
"description": ""
}
}
},
"get_properties": {
"name": "Özellikleri al",
"description": "Miot özelliklerini al",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"mapping": {
"name": "Eşleme",
"description": ""
},
"update_entity": {
"name": "Varlığı güncelle",
"description": ""
}
}
},
"set_property": {
"name": "Özellik ayarla",
"description": "Miot özelliğini ayarla",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"field": {
"name": "Alan",
"description": ""
},
"value": {
"name": "Değer",
"description": ""
}
}
},
"set_miot_property": {
"name": "Miot özelliği ayarla",
"description": "siid/piid ile miot özelliğini ayarla",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"piid": {
"name": "",
"description": ""
},
"value": {
"name": "",
"description": ""
}
}
},
"call_action": {
"name": "Eylem çağır",
"description": "Miot eylemini çağır",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"aiid": {
"name": "",
"description": ""
},
"params": {
"name": "",
"description": ""
}
}
},
"intelligent_speaker": {
"name": "Akıllı hoparlör",
"description": "XiaoAi için akıllı hoparlör",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": "XiaoAi Varlık ID"
},
"text": {
"name": "Metin",
"description": ""
},
"execute": {
"name": "Çalıştır",
"description": ""
},
"silent": {
"name": "Sessiz",
"description": ""
}
}
},
"xiaoai_wakeup": {
"name": "XiaoAi Uyandır",
"description": "XiaoAi hoparlörünü uyandır",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": "XiaoAi Varlık ID"
},
"text": {
"name": "Metin",
"description": ""
}
}
},
"get_device_data": {
"name": "Cihaz verisini al",
"description": "Buluttan xiaomi cihaz verisini al",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"type": {
"name": "Tür",
"description": ""
},
"key": {
"name": "Anahtar",
"description": ""
},
"time_start": {
"name": "Başlangıç zamanı",
"description": ""
},
"time_end": {
"name": "Bitiş zamanı",
"description": ""
},
"limit": {
"name": "Limit",
"description": ""
},
"group": {
"name": "Grup",
"description": ""
}
}
},
"renew_devices": {
"name": "Cihazları yenile",
"description": "Önbelleğe alınmış xiaomi cihazlarını yenile",
"fields": {
"username": {
"name": "Kullanıcı adı",
"description": ""
}
}
},
"get_token": {
"name": "Token al",
"description": "Xiaomi cihaz tokenını al",
"fields": {
"name": {
"name": "Ad",
"description": ""
}
}
},
"get_bindkey": {
"name": "Bindkey al",
"description": "BLE cihazı için bindkey al",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"did": {
"name": "DID",
"description": ""
}
}
},
"request_xiaomi_api": {
"name": "Xiaomi API isteği",
"description": "Xiaomi API'sine istek gönder",
"fields": {
"entity_id": {
"name": "Varlık ID",
"description": ""
},
"api": {
"name": "API",
"description": ""
},
"data": {
"name": "Veri",
"description": ""
},
"method": {
"name": "Metod",
"description": ""
},
"crypt": {
"name": "Şifreleme",
"description": ""
},
"sid": {
"name": "",
"description": ""
}
}
}
}
}
@@ -0,0 +1,474 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "Chọn hành động",
"data": {
"action": "Hành động"
}
},
"token": {
"description": "Cấu hình thông tin thiết bị.",
"data": {
"host": "IP cục bộ",
"token": "Token",
"name": "Tên thiết bị",
"scan_interval": "Chu kỳ cập nhật trạng thái (giây)"
}
},
"cloud": {
"description": "Cấu hình tài khoản Xiaomi Cloud.\n{tip}",
"data": {
"username": "ID tài khoản Xiaomi / Email",
"password": "Mật khẩu",
"captcha": "Captcha",
"verify_ticket": "Mã xác thực hai bước",
"server_country": "Vị trí máy chủ MiCloud",
"conn_mode": "Chế độ kết nối thiết bị",
"trans_options": "Mô tả dịch thuộc tính",
"filter_models": "Lọc thiết bị theo model/home/WiFi (Nâng cao)"
}
},
"cloud_filter": {
"title": "Lọc thiết bị cloud",
"description": "Bạn có thể bao gồm hoặc loại trừ thiết bị để chỉ tích hợp thiết bị mong muốn.\n{tip}",
"data": {
"filter_model": "Lọc theo model thiết bị",
"model_list": "Danh sách model thiết bị",
"filter_home_id": "Lọc theo Home",
"home_id_list": "Danh sách Home",
"filter_ssid": "Lọc theo WiFi SSID",
"ssid_list": "Danh sách WiFi SSID",
"filter_bssid": "Lọc theo WiFi BSSID",
"bssid_list": "Danh sách WiFi BSSID",
"filter_did": "Lọc theo thiết bị",
"did_list": "Danh sách thiết bị",
"home_ids": "Lọc theo Home (Để trống để lưu)"
},
"data_description": {
"filter_did": "Ở chế độ loại trừ, nếu không chọn mục nào, sẽ bao gồm tất cả thiết bị.",
"home_ids": "Sau khi chọn và gửi, bạn sẽ quay lại trang này, danh sách thiết bị sẽ làm mới và lọc theo Home đã chọn."
}
},
"customizing": {
"title": "Tùy chỉnh thực thể / thiết bị",
"description": "{tip}",
"data": {
"domain": "Chọn domain thực thể",
"entity": "Chọn thực thể",
"model": "Chọn model thiết bị",
"model_specified": "Model thiết bị chỉ định (Tùy chọn)",
"only_main_entity": "Chỉ thực thể chính (cha)",
"yaml_mode": "Chế độ YAML",
"yaml_customizes": "Mã YAML tùy chỉnh",
"reset_customizes": "Khôi phục tùy chỉnh mặc định",
"bool2selects": "Tùy chọn tùy chỉnh"
}
}
},
"error": {
"cannot_connect": "Không thể kết nối tới thiết bị",
"cannot_login": "Đăng nhập tài khoản Xiaomi thất bại. Vui lòng kiểm tra thông tin đăng nhập. Nếu thử nhiều lần vẫn không được, hãy thử đăng nhập lại trên trang chính thức Xiaomi (mi.com) cùng mạng.",
"cannot_reach": "Không thể truy cập API Xiaomi",
"tzinfo_error": "Có vẻ môi trường Hass của bạn gặp vấn đề. Xem: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Không tìm thấy thiết bị nào trong tài khoản Xiaomi này",
"need_verify": "Đăng nhập này cần xác thực bảo mật. Hãy mở trang xác thực để gửi mã xác minh qua điện thoại/email. Sau khi nhận mã, KHÔNG xác minh trên web mà quay lại Home Assistant để nhập mã vào ô bên dưới."
},
"abort": {
"config_saved": "Đã lưu cấu hình! [Tùy chỉnh lại](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nMột số tùy chọn cần [tải lại tích hợp](/developer-tools/yaml) để có hiệu lực.",
"already_configured": "Thiết bị này đã được cấu hình.",
"not_xiaomi_miio": "Thiết bị này không hỗ trợ giao thức miio/miot."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Cấu hình thông tin thiết bị.",
"data": {
"host": "IP thiết bị",
"token": "Token",
"scan_interval": "Chu kỳ cập nhật trạng thái (giây)",
"miot_cloud": "Bật miot cloud (Cấu hình tài khoản trong YAML trước)"
}
},
"cloud": {
"description": "Cấu hình tài khoản Xiaomi Cloud.\n{tip}",
"data": {
"username": "ID tài khoản Xiaomi / Email",
"password": "Mật khẩu",
"captcha": "Captcha",
"verify_ticket": "Mã xác thực hai bước",
"server_country": "Vị trí máy chủ MiCloud",
"conn_mode": "Chế độ kết nối thiết bị",
"renew_devices": "Làm mới danh sách thiết bị",
"trans_options": "Mô tả dịch thuộc tính",
"disable_message": "Tắt cảm biến thông báo Mihome",
"disable_scene_history": "Tắt cảm biến lịch sử cảnh Mihome"
}
},
"cloud_filter": {
"title": "Lọc thiết bị cloud",
"description": "Bạn có thể bao gồm hoặc loại trừ thiết bị để chỉ tích hợp thiết bị mong muốn.\n{tip}",
"data": {
"filter_model": "Lọc theo model thiết bị",
"model_list": "Danh sách model thiết bị",
"filter_home_id": "Lọc theo Home",
"home_id_list": "Danh sách Home",
"filter_ssid": "Lọc theo WiFi SSID",
"ssid_list": "Danh sách WiFi SSID",
"filter_bssid": "Lọc theo WiFi BSSID",
"bssid_list": "Danh sách WiFi BSSID",
"filter_did": "Lọc theo thiết bị",
"did_list": "Danh sách thiết bị",
"home_ids": "Lọc theo Home (Để trống để lưu)"
},
"data_description": {
"filter_did": "Ở chế độ loại trừ, nếu không chọn mục nào, sẽ bao gồm tất cả thiết bị.",
"home_ids": "Sau khi chọn và gửi, bạn sẽ quay lại trang này, danh sách thiết bị sẽ làm mới và lọc theo Home đã chọn."
}
}
},
"error": {
"cannot_connect": "Không thể kết nối tới thiết bị",
"cannot_login": "Đăng nhập tài khoản Xiaomi thất bại. Vui lòng kiểm tra thông tin đăng nhập. Nếu thử nhiều lần vẫn không được, hãy thử đăng nhập lại trên trang chính thức Xiaomi (mi.com) cùng mạng.",
"cannot_reach": "Không thể truy cập API Xiaomi",
"tzinfo_error": "Có vẻ môi trường Hass của bạn gặp vấn đề. Xem: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "Không tìm thấy thiết bị nào trong tài khoản Xiaomi này",
"need_verify": "Đăng nhập này cần xác thực bảo mật. Hãy mở trang xác thực để gửi mã xác minh qua điện thoại/email. Sau khi nhận mã, KHÔNG xác minh trên web mà quay lại Home Assistant để nhập mã vào ô bên dưới."
},
"abort": {
"show_customizes": "[![Thêm tích hợp mới](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Thêm tích hợp mới](/_my_redirect/config_flow_start?domain=xiaomi_miot) để chỉnh sửa tùy chỉnh.\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "Phiên bản custom component",
"can_reach_server": "Kết nối tới máy chủ API Xiaomi",
"can_reach_spec": "Kết nối tới máy chủ MIoT-Spec",
"logged_accounts": "Số tài khoản đã đăng nhập",
"total_devices": "Tổng số thiết bị MiHome"
}
},
"entity": {
"button": {
"info": {"name": "Thông tin"}
},
"sensor": {
"clean_area": {"name": "Diện tích đã làm sạch"},
"clean_time": {"name": "Thời gian làm sạch"},
"power_cost_today": {"name": "Năng lượng hôm nay"},
"power_cost_month": {"name": "Năng lượng tháng này"},
"power_cost_today_2": {"name": "Năng lượng hôm nay"},
"power_cost_month_2": {"name": "Năng lượng tháng này"},
"props-clean_area": {"name": "Diện tích đã làm sạch"},
"props-clean_time": {"name": "Thời gian làm sạch"},
"lock": {
"state": {
"bluetooth": "Bluetooth",
"password": "Mật khẩu",
"biological": "Sinh trắc",
"key": "Chìa khóa",
"turntable": "Vặn tay",
"nfc": "NFC",
"one_time_password": "Mật khẩu 1 lần",
"two_step_verification": "Xác thực 2 bước",
"coercion": "Cưỡng chế",
"homekit": "Homekit",
"manual": "Thủ công",
"automatic": "Tự động"
}
},
"lock_action": {
"state": {
"outside_unlock": "Mở khóa từ ngoài",
"lock": "Khóa",
"anti_lock_on": "Bật chống khóa",
"anti_lock_off": "Tắt chống khóa",
"inside_unlock": "Mở khóa từ trong",
"lock_inside": "Khóa từ trong",
"child_lock_on": "Bật khóa trẻ em",
"child_lock_off": "Tắt khóa trẻ em",
"lock_outside": "Khóa từ ngoài"
}
},
"door_state": {
"state": {
"open": "Mở",
"close": "Đóng",
"close_timeout": "Đóng quá thời gian",
"knock": "Gõ cửa",
"breaking": "Phá cửa",
"stuck": "Kẹt"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "Tự động",
"low": "Thấp",
"medium": "Trung bình",
"middle": "Trung bình",
"high": "Cao",
"quiet": "Yên lặng",
"turbo": "Turbo",
"level1": "Mức 1",
"level2": "Mức 2",
"level3": "Mức 3",
"level4": "Mức 4",
"level5": "Mức 5",
"level6": "Mức 6",
"level7": "Mức 7"
}
},
"swing_mode": {
"state": {
"off": "Tắt",
"vertical": "Dọc",
"horizontal": "Ngang",
"both": "Cả hai"
}
}
}
}
}
},
"services": {
"send_command": {
"name": "Gửi lệnh",
"description": "Gửi lệnh miio",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"method": {
"name": "Phương thức",
"description": ""
},
"params": {
"name": "Tham số",
"description": ""
}
}
},
"get_properties": {
"name": "Lấy thuộc tính",
"description": "Lấy thuộc tính miot",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"mapping": {
"name": "Ánh xạ",
"description": ""
},
"update_entity": {
"name": "Cập nhật thực thể",
"description": ""
}
}
},
"set_property": {
"name": "Đặt thuộc tính",
"description": "Đặt thuộc tính miot",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"field": {
"name": "Trường",
"description": ""
},
"value": {
"name": "Giá trị",
"description": ""
}
}
},
"set_miot_property": {
"name": "Đặt thuộc tính miot",
"description": "Đặt thuộc tính miot theo siid/piid",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"piid": {
"name": "",
"description": ""
},
"value": {
"name": "",
"description": ""
}
}
},
"call_action": {
"name": "Gọi hành động",
"description": "Gọi action miot",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"siid": {
"name": "",
"description": ""
},
"aiid": {
"name": "",
"description": ""
},
"params": {
"name": "",
"description": ""
}
}
},
"intelligent_speaker": {
"name": "Loa thông minh",
"description": "Loa thông minh cho XiaoAi",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": "Xiaoai Entity ID"
},
"text": {
"name": "Văn bản",
"description": ""
},
"execute": {
"name": "Thực thi",
"description": ""
},
"silent": {
"name": "Im lặng",
"description": ""
}
}
},
"xiaoai_wakeup": {
"name": "Đánh thức XiaoAi",
"description": "Đánh thức loa XiaoAi",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": "Xiaoai Entity ID"
},
"text": {
"name": "Văn bản",
"description": ""
}
}
},
"get_device_data": {
"name": "Lấy dữ liệu thiết bị",
"description": "Lấy dữ liệu thiết bị xiaomi từ cloud",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"type": {
"name": "Loại",
"description": ""
},
"key": {
"name": "Khóa",
"description": ""
},
"time_start": {
"name": "Thời gian bắt đầu",
"description": ""
},
"time_end": {
"name": "Thời gian kết thúc",
"description": ""
},
"limit": {
"name": "Giới hạn",
"description": ""
},
"group": {
"name": "Nhóm",
"description": ""
}
}
},
"renew_devices": {
"name": "Làm mới thiết bị",
"description": "Làm mới cache thiết bị xiaomi",
"fields": {
"username": {
"name": "Tên đăng nhập",
"description": ""
}
}
},
"get_token": {
"name": "Lấy token",
"description": "Lấy token thiết bị xiaomi",
"fields": {
"name": {
"name": "Tên",
"description": ""
}
}
},
"get_bindkey": {
"name": "Lấy bindkey",
"description": "Lấy bindkey cho thiết bị ble",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"did": {
"name": "DID",
"description": ""
}
}
},
"request_xiaomi_api": {
"name": "Gọi API xiaomi",
"description": "Gọi API xiaomi",
"fields": {
"entity_id": {
"name": "Entity ID",
"description": ""
},
"api": {
"name": "API",
"description": ""
},
"data": {
"name": "Dữ liệu",
"description": ""
},
"method": {
"name": "Phương thức",
"description": ""
},
"crypt": {
"name": "Mã hóa",
"description": ""
},
"sid": {
"name": "",
"description": ""
}
}
}
}
}
@@ -0,0 +1,532 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "选择操作",
"data": {
"action": "操作"
}
},
"token": {
"description": "配置设备信息",
"data": {
"host": "设备IP",
"token": "Token",
"name": "名称",
"scan_interval": "更新状态频率(秒)"
}
},
"cloud": {
"description": "配置小米账户\n{tip}",
"data": {
"username": "小米ID / 邮箱",
"password": "小米密码",
"captcha": "验证码",
"verify_ticket": "邮箱/手机验证码",
"server_country": "小米服务器",
"conn_mode": "设备连接模式",
"trans_options": "翻译属性值描述",
"filter_models": "通过型号/家庭/WiFi筛选设备 (高级模式,新手勿选)"
}
},
"cloud_filter": {
"title": "筛选设备",
"description": "你可以通过筛选只接入想要的设备。如果想集成网关子设备,请直接选择子设备,接入网关不会自动集成子设备。\n{tip}",
"data": {
"filter_model": "筛选设备型号",
"model_list": "设备型号列表",
"filter_home_id": "筛选家庭",
"home_id_list": "家庭列表",
"filter_ssid": "筛选 WiFi SSID",
"ssid_list": "WiFi SSID 列表",
"filter_bssid": "筛选 WiFi BSSID",
"bssid_list": "WiFi BSSID 列表",
"filter_did": "筛选设备",
"did_list": "设备列表",
"home_ids": "筛选家庭 (留空保存)"
},
"data_description": {
"filter_did": "在排除模式中,如果不勾选任何设备,则相当于接入所有设备",
"home_ids": "勾选此项后提交,会回到在当前页面,设备列表会刷新并筛选出已勾选家庭的设备"
}
},
"customizing": {
"title": "自定义实体 / 设备",
"description": "{tip}",
"data": {
"domain": "选择实体类型",
"entity": "选择实体",
"model": "选择设备型号",
"model_specified": "指定设备型号 (可选)",
"only_main_entity": "仅主(父)实体",
"yaml_mode": "YAML模式",
"yaml_customizes": "自定义选项代码",
"reset_customizes": "重置为默认自定义",
"bool2selects": "自定义选项"
}
}
},
"error": {
"cannot_connect": "连接设备失败",
"cannot_login": "小米账户登录失败,请检查账号密码。如多次尝试仍登录失败,请在与当前网络相同的环境下登录小米官网(mi.com)后重试",
"cannot_reach": "无法访问小米API接口",
"tzinfo_error": "你的 Hass 环境看起来有点问题,请参考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "该小米账户下没有设备",
"need_verify": "本次登录需要安全验证,请打开验证网页发送手机/邮箱验证码,收到验证码后不要在网页验证,回到HomeAssistant填入下方输入框"
},
"abort": {
"config_saved": "配置已保存![再次自定义](/_my_redirect/config_flow_start?domain=xiaomi_miot)。\n{tip}\n\n有些自定义选项需要[重载集成](/developer-tools/yaml)才能生效。",
"already_configured": "该设备已经配置过",
"not_xiaomi_miio": "该设备不支持 miio/miot 协议"
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "配置设备信息",
"data": {
"host": "设备 IP",
"token": "Token",
"scan_interval": "更新状态频率(秒)",
"miot_cloud": "开启云端模式(请先配置账户密码)"
}
},
"cloud": {
"description": "配置小米账户\n{tip}",
"data": {
"username": "小米ID / 邮箱",
"password": "小米密码",
"captcha": "验证码",
"verify_ticket": "邮箱/手机验证码",
"server_country": "小米服务器",
"conn_mode": "设备连接模式",
"renew_devices": "更新设备列表",
"trans_options": "翻译属性值描述",
"disable_message": "禁用米家APP通知消息实体",
"disable_scene_history": "禁用米家场景历史实体"
}
},
"cloud_filter": {
"title": "筛选设备",
"description": "你可以通过筛选只接入想要的设备。如果想集成网关子设备,请直接选择子设备,接入网关不会自动集成子设备。\n{tip}",
"data": {
"filter_model": "筛选设备型号",
"model_list": "设备型号列表",
"filter_home_id": "筛选家庭",
"home_id_list": "家庭列表",
"filter_ssid": "筛选 WiFi SSID",
"ssid_list": "WiFi SSID 列表",
"filter_bssid": "筛选 WiFi BSSID",
"bssid_list": "WiFi BSSID 列表",
"filter_did": "筛选设备",
"did_list": "设备列表",
"home_ids": "筛选家庭 (留空保存)"
},
"data_description": {
"filter_did": "在排除模式中,如果不勾选任何设备,则相当于接入所有设备",
"home_ids": "勾选此项后提交,会回到在当前页面,设备列表会刷新并筛选出已勾选家庭的设备"
}
}
},
"error": {
"cannot_connect": "连接设备失败",
"cannot_login": "小米账户登录失败,请检查账号密码。如多次尝试仍登录失败,请使用与HA宿主机同一局域网下的其他设备登录小米官网(mi.com)后重试",
"cannot_reach": "无法访问小米API接口",
"tzinfo_error": "你的Hass环境看起来有点问题,请参考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "该小米账户下没有设备",
"need_verify": "本次登录需要安全验证,请打开验证网页发送手机/邮箱验证码,收到验证码后不要在网页验证,回到HomeAssistant填入下方输入框"
},
"abort": {
"show_customizes": "[![添加集成](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n请再次添加[`Xiaomi Miot`](/_my_redirect/config_flow_start?domain=xiaomi_miot)集成来修改自定义选项\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "自定义组件版本",
"can_reach_server": "可访问米家服务器",
"can_reach_spec": "可获取MIoT规格",
"logged_accounts": "已登录的账户数量",
"total_devices": "米家设备总数量"
}
},
"entity": {
"button": {
"info": {"name": "信息"}
},
"sensor": {
"clean_area": {"name": "清洁面积"},
"clean_time": {"name": "清洁时间"},
"power_cost_today": {"name": "日电量"},
"power_cost_month": {"name": "月电量"},
"power_cost_today_2": {"name": "日电量"},
"power_cost_month_2": {"name": "月电量"},
"prop_cal_day-power_cost-today": {"name": "日电量"},
"prop_cal_day-power_cost-month": {"name": "月电量"},
"props-clean_area": {"name": "清洁面积"},
"props-clean_time": {"name": "清洁时间"},
"lock": {
"state": {
"bluetooth": "蓝牙",
"password": "密码",
"biological": "生物",
"key": "钥匙",
"turntable": "转盘",
"nfc": "NFC",
"one_time_password": "一次性密码",
"two_step_verification": "双重认证",
"coercion": "胁迫",
"homekit": "Homekit",
"manual": "人工",
"automatic": "自动"
}
},
"lock_action": {
"state": {
"outside_unlock": "门外开锁",
"lock": "上锁",
"anti_lock_on": "开启反锁",
"anti_lock_off": "解除反锁",
"inside_unlock": "门内开锁",
"lock_inside": "门内上锁",
"child_lock_on": "开启童锁",
"child_lock_off": "关闭童锁",
"lock_outside": "门外上锁"
}
},
"door_state": {
"state": {
"open": "开门",
"close": "关门",
"close_timeout": "超时未关",
"knock": "敲门",
"breaking": "撬门",
"stuck": "卡住"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "自动",
"low": "低档",
"medium": "中档",
"middle": "中档",
"high": "高档",
"quiet": "静音",
"turbo": "超强",
"level1": "一档",
"level2": "二档",
"level3": "三档",
"level4": "四档",
"level5": "五档",
"level6": "六档",
"level7": "七档",
"level8": "八档",
"leve7": "七档",
"leve8": "八档"
}
},
"preset_mode": {
"state": {
"off": "关闭",
"auto": "自动",
"Heat": "制热",
"Warm": "温暖",
"Natural Wind": "自然风"
}
},
"swing_mode": {
"state": {
"off": "关闭",
"vertical": "垂直",
"horizontal": "水平",
"both": "立体"
}
}
}
}
},
"fan": {
"fan": {
"state_attributes": {
"preset_modes": {
"state": {
"Basic Wind": "标准风",
"Straight Wind": "直吹风",
"Natural Wind": "自然风",
"Circular Wind": "循环风",
"Baby": "婴儿",
"Sleep": "睡眠",
"Smart": "智能"
}
}
}
}
},
"light": {
"light": {
"state_attributes": {
"effect_list": {
"state": {
"Day": "日光",
"Night": "夜光",
"Color": "彩光",
"Scene": "场景",
"Music": "音乐"
}
}
}
}
},
"select": {
"fan_control-fan_level": {
"state": {
"auto": "自动",
"low": "低档",
"medium": "中档",
"middle": "中档",
"high": "高档",
"Quiet": "静音",
"Turbo": "超强",
"Level1": "一档",
"Level2": "二档",
"Level3": "三档",
"Level4": "四档",
"Level5": "五档",
"Level6": "六档",
"Level7": "七档",
"Level8": "八档",
"Leve7": "七档",
"Leve8": "八档"
}
},
"light-mode": {
"Day": "日光",
"Night": "夜光",
"Color": "彩光",
"Scene": "场景",
"Music": "音乐"
}
},
"humidifier": {
"humidifier": {
"state_attributes": {
"available_modes": {
"state": {
"Off": "关闭",
"Sleep": "睡眠",
"Strong": "强力",
"Constant Humidity": "恒湿"
}
}
}
},
"dehumidifier": {
"state_attributes": {
"available_modes": {
"state": {
"Off": "关闭",
"Sleep": "睡眠",
"Strong": "强力",
"Constant Humidity": "恒湿"
}
}
}
}
}
},
"services": {
"send_command": {
"description": "发送miio命令",
"fields": {
"entity_id": {
"description": "实体ID"
},
"method": {
"description": "Miio方法"
},
"params": {
"description": "Miio参数"
}
}
},
"get_properties": {
"description": "获取MIoT属性",
"fields": {
"entity_id": {
"description": "实体ID"
},
"mapping": {
"description": "MIoT规格"
},
"update_entity": {
"description": "更新实体状态属性"
}
}
},
"set_property": {
"description": "设置MIoT属性(通过属性名)",
"fields": {
"entity_id": {
"description": "实体ID"
},
"field": {
"description": "MIoT属性名"
},
"value": {
"description": "MIoT属性值"
}
}
},
"set_miot_property": {
"description": "设置MIoT属性(通过siid/piid)",
"fields": {
"entity_id": {
"description": "实体ID"
},
"siid": {
"description": "MIoT服务ID"
},
"piid": {
"description": "MIoT属性ID"
},
"value": {
"description": "MIoT属性值"
}
}
},
"call_action": {
"description": "执行MIoT动作",
"fields": {
"entity_id": {
"description": "实体ID"
},
"siid": {
"description": "MIoT服务ID"
},
"aiid": {
"description": "MIoT动作ID"
},
"params": {
"description": "MIoT动作参数"
}
}
},
"intelligent_speaker": {
"description": "小爱TTS及执行语音命令",
"fields": {
"entity_id": {
"description": "小爱实体ID"
},
"text": {
"description": "文本内容"
},
"execute": {
"description": "执行语音指令,否则朗读文本"
},
"silent": {
"description": "静默执行语音指令"
}
}
},
"xiaoai_wakeup": {
"description": "唤醒小爱音箱",
"fields": {
"entity_id": {
"description": "小爱实体ID"
},
"text": {
"description": "文本内容"
}
}
},
"get_device_data": {
"description": "获取设备信息",
"fields": {
"entity_id": {
"description": "实体ID"
},
"type": {
"name": "Type",
"description": ""
},
"key": {
"name": "Key",
"description": ""
},
"time_start": {
"name": "Time start",
"description": ""
},
"time_end": {
"name": "Time end",
"description": ""
},
"limit": {
"name": "Limit",
"description": ""
},
"group": {
"name": "Group",
"description": ""
}
}
},
"renew_devices": {
"description": "刷新设备列表",
"fields": {
"username": {
"name": "小米账号",
"description": "小米ID / 手机号 / 邮箱"
}
}
},
"get_token": {
"description": "获取米家设备令牌(token)",
"fields": {
"name": {
"name": "关键词",
"description": "米家APP中的设备名称关键词 / IP / 型号"
}
}
},
"get_bindkey": {
"description": "获取蓝牙设备bindkey",
"fields": {
"entity_id": {
"description": "实体ID"
},
"did": {
"description": "设备DID"
}
}
},
"request_xiaomi_api": {
"description": "请求小米云端接口",
"fields": {
"entity_id": {
"description": "实体ID"
},
"api": {
"description": "接口地址"
},
"data": {
"description": "请求数据"
},
"method": {
"description": "请求方式"
},
"crypt": {
"description": "是否加密"
},
"sid": {
"description": "小米服务ID"
}
}
}
}
}
@@ -0,0 +1,494 @@
{
"config": {
"flow_title": "Xiaomi Miot",
"step": {
"user": {
"title": "選擇操作",
"data": {
"action": "操作"
}
},
"token": {
"description": "設定裝置資訊",
"data": {
"host": "裝置 IP",
"token": "Token",
"name": "名稱",
"scan_interval": "狀態更新頻率(秒)"
}
},
"cloud": {
"description": "設定小米帳號\n{tip}",
"data": {
"username": "小米 ID / 電子郵件",
"password": "小米密碼",
"captcha": "驗證碼",
"server_country": "小米伺服器",
"conn_mode": "裝置連線模式",
"trans_options": "翻譯屬性值描述",
"filter_models": "透過型號/家庭/WiFi篩選裝置 (進階模式,新手勿選)"
}
},
"cloud_filter": {
"title": "篩選裝置",
"description": "你可以透過篩選只接入想要的裝置。在排除模式中,如果不勾選任何裝置,則相當於接入所有裝置。如果想整合網關子裝置,請直接選擇子裝置,接入網關不會自動整合子裝置。\n{tip}",
"data": {
"filter_model": "篩選裝置型號",
"model_list": "裝置型號列表",
"filter_home_id": "篩選家庭",
"home_id_list": "家庭列表",
"filter_ssid": "篩選 WiFi SSID",
"ssid_list": "WiFi SSID 列表",
"filter_bssid": "篩選 WiFi BSSID",
"bssid_list": "WiFi BSSID 列表",
"filter_did": "篩選裝置",
"did_list": "裝置列表",
"home_ids": "篩選家庭 (留白儲存)"
}
},
"customizing": {
"title": "自訂實體 / 裝置",
"description": "{tip}",
"data": {
"domain": "選擇實體類型",
"entity": "選擇實體",
"model": "選擇裝置型號",
"model_specified": "指定裝置型號 (可選)",
"only_main_entity": "僅主(父)實體",
"yaml_mode": "YAML模式",
"yaml_customizes": "自訂選項代碼",
"reset_customizes": "重設為預設自訂",
"bool2selects": "自訂選項"
}
}
},
"error": {
"cannot_connect": "連線裝置失敗",
"cannot_login": "小米帳號登入失敗,請檢查帳號密碼。如多次嘗試仍登入失敗,請在與目前網路相同的環境下登入小米官網(mi.com)後重試",
"cannot_reach": "無法存取小米 API",
"tzinfo_error": "您的 Hass 環境看起來有點問題,請參考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "該小米帳號下沒有裝置"
},
"abort": {
"config_saved": "設定已儲存![再次自訂](/_my_redirect/config_flow_start?domain=xiaomi_miot)。\n{tip}\n\n有些自訂選項需要[重新載入整合](/developer-tools/yaml)才能生效。",
"already_configured": "該裝置已經設定過",
"not_xiaomi_miio": "該裝置不支援 miio/miot 協定"
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "設定裝置資訊",
"data": {
"host": "裝置 IP",
"token": "Token",
"scan_interval": "狀態更新頻率(秒)",
"miot_cloud": "開啟雲端模式(請先設定帳號密碼)"
}
},
"cloud": {
"description": "設定小米帳號\n{tip}",
"data": {
"username": "小米 ID / 電子郵件",
"password": "小米密碼",
"captcha": "驗證碼",
"server_country": "小米伺服器",
"conn_mode": "裝置連線模式",
"renew_devices": "更新裝置列表",
"trans_options": "翻譯屬性值描述",
"disable_message": "停用米家 APP 通知訊息實體",
"disable_scene_history": "停用米家場景歷史實體"
}
},
"cloud_filter": {
"title": "篩選裝置",
"description": "你可以透過篩選只接入想要的裝置。在排除模式中,如果不勾選任何裝置,則相當於接入所有裝置。如果想整合網關子裝置,請直接選擇子裝置,接入網關不會自動整合子裝置。\n{tip}",
"data": {
"filter_model": "篩選裝置型號",
"model_list": "裝置型號列表",
"filter_home_id": "篩選家庭",
"home_id_list": "家庭列表",
"filter_ssid": "篩選 WiFi SSID",
"ssid_list": "WiFi SSID 列表",
"filter_bssid": "篩選 WiFi BSSID",
"bssid_list": "WiFi BSSID 列表",
"filter_did": "篩選裝置",
"did_list": "裝置列表",
"home_ids": "篩選家庭 (留白儲存)"
}
}
},
"error": {
"cannot_connect": "連線裝置失敗",
"cannot_login": "小米帳號登入失敗,請檢查帳號密碼。如多次嘗試仍登入失敗,請使用與 HA 主機同一區域網路下的其他裝置登入小米官網(mi.com)後重試",
"cannot_reach": "無法存取小米 API",
"tzinfo_error": "您的 Hass 環境看起來有點問題,請參考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
"none_devices": "該小米帳號下沒有裝置"
},
"abort": {
"show_customizes": "[![新增整合](https://my.home-assistant.io/badges/config_flow_start.svg)](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n請再次添加[`Xiaomi Miot`](/_my_redirect/config_flow_start?domain=xiaomi_miot)整合來修改自訂選項\n{tip}"
}
},
"system_health": {
"info": {
"component_version": "自訂元件版本",
"can_reach_server": "可存取米家伺服器",
"can_reach_spec": "可取得 MIoT 規格",
"logged_accounts": "已登入的帳號數量",
"total_devices": "米家裝置總數量"
}
},
"entity": {
"button": {
"info": {"name": "資訊"}
},
"sensor": {
"clean_area": {"name": "清潔面積"},
"clean_time": {"name": "清潔時間"},
"power_cost_today": {"name": "日用電量"},
"power_cost_month": {"name": "月用電量"},
"power_cost_today_2": {"name": "日用電量"},
"power_cost_month_2": {"name": "月用電量"},
"prop_cal_day-power_cost-today": {"name": "日用電量"},
"prop_cal_day-power_cost-month": {"name": "月用電量"},
"props-clean_area": {"name": "清潔面積"},
"props-clean_time": {"name": "清潔時間"},
"lock": {
"state": {
"bluetooth": "藍牙",
"password": "密碼",
"biological": "生物辨識",
"key": "鑰匙",
"turntable": "轉盤",
"nfc": "NFC",
"one_time_password": "一次性密碼",
"two_step_verification": "兩步驟驗證",
"coercion": "脅迫",
"homekit": "HomeKit",
"manual": "手動",
"automatic": "自動"
}
},
"lock_action": {
"state": {
"outside_unlock": "門外解鎖",
"lock": "上鎖",
"anti_lock_on": "開啟反鎖",
"anti_lock_off": "解除反鎖",
"inside_unlock": "門內解鎖",
"lock_inside": "門內上鎖",
"child_lock_on": "開啟兒童鎖",
"child_lock_off": "關閉兒童鎖",
"lock_outside": "門外上鎖"
}
},
"door_state": {
"state": {
"open": "開門",
"close": "關門",
"close_timeout": "超時未關",
"knock": "敲門",
"breaking": "撬門",
"stuck": "卡住"
}
}
},
"climate": {
"air_conditioner": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "自動",
"low": "低速",
"medium": "中速",
"middle": "中速",
"high": "高速",
"quiet": "靜音",
"turbo": "超強",
"level1": "速度1",
"level2": "速度2",
"level3": "速度3",
"level4": "速度4",
"level5": "速度5",
"level6": "速度6",
"level7": "速度7",
"level8": "速度8",
"leve7": "速度7",
"leve8": "速度8"
}
},
"preset_mode": {
"state": {
"off": "關閉",
"auto": "自動",
"Heat": "暖氣",
"Warm": "溫暖",
"Natural Wind": "自然風"
}
},
"swing_mode": {
"state": {
"off": "關閉",
"vertical": "垂直",
"horizontal": "水平",
"both": "立體"
}
}
}
}
},
"fan": {
"fan": {
"state_attributes": {
"preset_modes": {
"state": {
"Basic Wind": "標準風",
"Straight Wind": "直吹風",
"Natural Wind": "自然風",
"Circular Wind": "循環風",
"Baby": "嬰兒",
"Sleep": "睡眠",
"Smart": "智慧"
}
}
}
}
},
"light": {
"light": {
"state_attributes": {
"effect_list": {
"state": {
"Day": "日光",
"Night": "夜光",
"Color": "彩光",
"Scene": "場景",
"Music": "音樂"
}
}
}
}
},
"select": {
"fan_control-fan_level": {
"state": {
"auto": "自動",
"low": "低速",
"medium": "中速",
"middle": "中速",
"high": "高速",
"Quiet": "靜音",
"Turbo": "超強",
"Level1": "速度1",
"Level2": "速度2",
"Level3": "速度3",
"Level4": "速度4",
"Level5": "速度5",
"Level6": "速度6",
"Level7": "速度7",
"Level8": "速度8",
"Leve7": "速度7",
"Leve8": "速度8"
}
},
"light-mode": {
"Day": "日光",
"Night": "夜光",
"Color": "彩光",
"Scene": "場景",
"Music": "音樂"
}
}
},
"services": {
"send_command": {
"description": "發送 miio 指令",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"method": {
"description": "Miio 方法"
},
"params": {
"description": "Miio 參數"
}
}
},
"get_properties": {
"description": "取得 MIoT 屬性",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"mapping": {
"description": "MIoT 規格"
},
"update_entity": {
"description": "更新實體狀態屬性"
}
}
},
"set_property": {
"description": "設定 MIoT 屬性(透過屬性名)",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"field": {
"description": "MIoT 屬性名"
},
"value": {
"description": "MIoT 屬性值"
}
}
},
"set_miot_property": {
"description": "設定 MIoT 屬性(透過 siid/piid)",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"siid": {
"description": "MIoT 服務 ID"
},
"piid": {
"description": "MIoT 屬性 ID"
},
"value": {
"description": "MIoT 屬性值"
}
}
},
"call_action": {
"description": "執行 MIoT 動作",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"siid": {
"description": "MIoT 服務 ID"
},
"aiid": {
"description": "MIoT 動作 ID"
},
"params": {
"description": "MIoT 動作參數"
}
}
},
"intelligent_speaker": {
"description": "小愛 TTS 及執行語音指令",
"fields": {
"entity_id": {
"description": "小愛實體 ID"
},
"text": {
"description": "文字內容"
},
"execute": {
"description": "執行語音指令,否則朗讀文字"
},
"silent": {
"description": "靜默執行語音指令"
}
}
},
"xiaoai_wakeup": {
"description": "喚醒小愛音箱",
"fields": {
"entity_id": {
"description": "小愛實體 ID"
},
"text": {
"description": "文字內容"
}
}
},
"get_device_data": {
"description": "取得裝置資訊",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"type": {
"name": "類型",
"description": ""
},
"key": {
"name": "金鑰",
"description": ""
},
"time_start": {
"name": "開始時間",
"description": ""
},
"time_end": {
"name": "結束時間",
"description": ""
},
"limit": {
"name": "限制",
"description": ""
},
"group": {
"name": "群組",
"description": ""
}
}
},
"renew_devices": {
"description": "重新整理裝置列表",
"fields": {
"username": {
"name": "小米帳號",
"description": "小米 ID / 手機號碼 / 電子郵件"
}
}
},
"get_token": {
"description": "取得米家裝置令牌(token)",
"fields": {
"name": {
"name": "關鍵字",
"description": "米家 APP 中的裝置名稱關鍵字 / IP / 型號"
}
}
},
"get_bindkey": {
"description": "取得藍牙裝置 bindkey",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"did": {
"description": "裝置 DID"
}
}
},
"request_xiaomi_api": {
"description": "呼叫小米雲端 API",
"fields": {
"entity_id": {
"description": "實體 ID"
},
"api": {
"description": "API 網址"
},
"data": {
"description": "傳送資料"
},
"method": {
"description": "呼叫方式"
},
"crypt": {
"description": "是否加密"
},
"sid": {
"description": "小米服務 ID"
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More