初始化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
@@ -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