Skip to content

Commit

Permalink
⚒ add options flow
Browse files Browse the repository at this point in the history
  • Loading branch information
al-one committed Feb 20, 2021
1 parent edfe503 commit fd0f0b2
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 49 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ or

### HomeAssistant GUI

> Configuration > Integration > ➕ > Search `Xiaomi Miot Auto`
> Configuration > Integrations > ➕ > Search `Xiaomi Miot Auto`

### Configuration variables:

Expand All @@ -82,6 +83,7 @@ or
- **name**(*Optional*): The name of your device
- **model**(*Optional*): The model of device, required if config by yaml


### Configuration Xiaomi Cloud:

> If your device return code -4004 or -9999 in logs, You can try this way.
Expand All @@ -98,10 +100,15 @@ xiaomi_miot:

# customize.yaml (Configuration > Customize > Select Entity > Add Other Attribute)
camera.entity_id: # Your entity id
miot_cloud: true # Enabled cloud
miot_cloud: true # Enable miot cloud for entity
# miot_did: '123456789' # Your miot device id, Optional (Get form cloud)
```

Enabled miot cloud for device:

> Configuration > Integrations > Xiaomi Miot Auto > Options > Enable miot cloud

### Customize entity

```yaml
Expand Down
26 changes: 24 additions & 2 deletions custom_components/xiaomi_miot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ async def async_setup_entry(hass: hass_core.HomeAssistant, config_entry: config_
unique_id = config_entry.unique_id
info = config_entry.data.get('miio_info') or {}
config = {}
config.update(config_entry.options or {})
for k in [CONF_HOST, CONF_TOKEN, CONF_NAME, CONF_MODEL, CONF_MODE]:
config[k] = config_entry.data.get(k)
model = str(config.get(CONF_MODEL) or info.get(CONF_MODEL) or '')
Expand All @@ -185,6 +186,7 @@ async def async_setup_entry(hass: hass_core.HomeAssistant, config_entry: config_
if m in SUPPORTED_DOMAINS
]
config[CONF_MODE] = modes

if 'miot_type' in config_entry.data:
config['miot_type'] = config_entry.data.get('miot_type')
else:
Expand All @@ -193,17 +195,31 @@ async def async_setup_entry(hass: hass_core.HomeAssistant, config_entry: config_
config['config_entry'] = config_entry
hass.data[DOMAIN]['configs'][entry_id] = config
hass.data[DOMAIN]['configs'][unique_id] = config
_LOGGER.debug('Xiaomi Miot async_setup_entry %s', {
_LOGGER.debug('Xiaomi Miot setup config entry: %s', {
'entry_id': entry_id,
'unique_id': unique_id,
'config': config,
'miio': info,
})

if not config_entry.update_listeners:
config_entry.add_update_listener(async_update_options)

for d in SUPPORTED_DOMAINS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(config_entry, d))
return True


async def async_update_options(hass: hass_core.HomeAssistant, config_entry: config_entries.ConfigEntry):
await hass.config_entries.async_reload(config_entry.entry_id)
_LOGGER.debug('Xiaomi Miot update config entry options: %s', {
'entry_id': config_entry.entry_id,
'unique_id': config_entry.unique_id,
'data': config_entry.data,
'options': config_entry.options,
})


def bind_services_to_entries(hass, services):
async def async_service_handler(service):
method = services.get(service.service)
Expand Down Expand Up @@ -252,6 +268,7 @@ def get_properties_for_mapping(self) -> list:
class MiioEntity(Entity):
def __init__(self, name, device, **kwargs):
self._device = device
self._config = dict(kwargs.get('config') or {})
try:
miio_info = kwargs.get('miio_info')
if miio_info and isinstance(miio_info, dict):
Expand Down Expand Up @@ -413,7 +430,12 @@ def miot_did(self):

@property
def miot_cloud(self):
if self.hass and self.miot_did and self.custom_config('miot_cloud'):
isc = False
if self._config.get('miot_cloud'):
isc = True
elif self.custom_config('miot_cloud'):
isc = True
if isc and self.hass and self.miot_did:
return self.hass.data[DOMAIN].get('xiaomi_cloud')
return None

Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def __init__(self, hass, config: dict, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)
Camera.__init__(self)
self._add_entities = config.get('add_entities') or {}

Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(self, config: dict, miot_service: MiotService):
_LOGGER.info('Initializing with host %s (token %s...), miot mapping: %s', host, token[:5], mapping)

self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)
self._add_entities = config.get('add_entities') or {}

self._prop_power = miot_service.get_property('on')
Expand Down
119 changes: 83 additions & 36 deletions custom_components/xiaomi_miot/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from homeassistant import config_entries
from homeassistant.const import *
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac

from miio import (
Expand All @@ -20,69 +21,115 @@

_LOGGER = logging.getLogger(__name__)

MIIO_CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): str,
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
})

async def check_miio_device(hass, user_input, errors):
host = user_input.get(CONF_HOST)
token = user_input.get(CONF_TOKEN)
try:
device = MiioDevice(host, token)
info = await hass.async_add_executor_job(device.info)
except DeviceException:
info = None
errors['base'] = 'cannot_connect'
_LOGGER.debug('Xiaomi Miot config flow: %s', {
'user_input': user_input,
'miio_info': info,
'errors': errors,
})
if info is not None:
if not user_input.get(CONF_MODEL):
user_input[CONF_MODEL] = str(info.model or '')
user_input['miio_info'] = dict(info.raw or {})
miot_type = await MiotSpec.async_get_model_type(hass, user_input.get(CONF_MODEL))
user_input['miot_type'] = miot_type
user_input['unique_did'] = format_mac(info.mac_address)
return user_input


class XiaomiMiotFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
self.host = None

async def async_step_user(self, user_input=None):
errors = {}
if user_input is not None:
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]
token = user_input.get(CONF_TOKEN)
device = MiioDevice(self.host, token)
try:
info = device.info()
except DeviceException:
info = None
errors['base'] = 'cannot_connect'
_LOGGER.debug('Xiaomi Miot async_step_user %s', {
'user_input': user_input,
'info': info,
'errors': errors,
})
if info is not None:
unique_id = format_mac(info.mac_address)
await self.async_set_unique_id(unique_id)
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 not user_input.get(CONF_MODEL):
user_input[CONF_MODEL] = str(info.model or '')
user_input['miio_info'] = dict(info.raw or {})
miot_type = await MiotSpec.async_get_model_type(self.hass, user_input.get(CONF_MODEL))
user_input['miot_type'] = miot_type
if user_input.get('miio_info'):
return self.async_create_entry(
title=user_input.get(CONF_NAME),
data=user_input,
)
else:
user_input = {}
return self.async_show_form(
step_id='user',
data_schema=MIIO_CONFIG_SCHEMA,
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,
}),
errors=errors,
)

async def async_step_zeroconf(self, discovery_info):
name = discovery_info.get('name')
self.host = discovery_info.get('host')
host = discovery_info.get('host')
mac_address = discovery_info.get('properties', {}).get('mac')
if not name or not self.host or not mac_address:
if not name or not host or not mac_address:
return self.async_abort(reason='not_xiaomi_miio')
if not name.startswith('xiaomi'):
_LOGGER.debug('Device %s discovered with host %s, not xiaomi device', name, self.host)
_LOGGER.debug('Device %s discovered with host %s, not xiaomi device', name, host)
return self.async_abort(reason='not_xiaomi_miio')
unique_id = format_mac(mac_address)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
self._abort_if_unique_id_configured({CONF_HOST: host})
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({
'title_placeholders': {'name': f'{name}({self.host})'}
'title_placeholders': {'name': f'{name}({host})'}
})
return await self.async_step_user()

@staticmethod
@callback
def async_get_options_flow(entry: config_entries.ConfigEntry):
return OptionsFlowHandler(entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry):
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
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]:
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.config_entry.data or {}
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('miot_cloud', default=user_input.get('miot_cloud', False)): bool,
}),
errors=errors,
)
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self, config: dict, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)

self._prop_status = miot_service.get_property('status')
self._prop_motor_control = miot_service.get_property('motor_control')
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(self, config: dict, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)
self._add_entities = config.get('add_entities') or {}

self._prop_power = miot_service.get_property('on', 'dryer')
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/humidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def __init__(self, config: dict, miot_service: MiotService):
_LOGGER.info('Initializing with host %s (token %s...), miot mapping: %s', host, token[:5], mapping)

self._device = MiotDevice(host, token)
super().__init__(name, self._device, miot_service, miio_info=miio_info)
super().__init__(name, self._device, miot_service, config=config, miio_info=miio_info)

self._prop_power = miot_service.get_property('on')
self._prop_mode = miot_service.get_property('mode')
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(self, config: dict, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)

self._prop_power = miot_service.get_property('on')
self._prop_brightness = miot_service.get_property('brightness')
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def __init__(self, config: dict, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)

self._prop_state = miot_service.get_property('playing_state')

Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(self, config, miot_service: MiotService):
) or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)
self._add_entities = config.get('add_entities') or {}

self._state_attrs.update({'entity_class': self.__class__.__name__})
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __init__(self, config: dict, miot_service: MiotService):
mapping = miot_service.spec.services_mapping(ENTITY_DOMAIN, 'indicator_light', 'switch_control') or {}
mapping.update(miot_service.mapping())
self._device = MiotDevice(mapping, host, token)
super().__init__(name, self._device, miot_service)
super().__init__(name, self._device, miot_service, config=config)
self._add_entities = config.get('add_entities') or {}

self._state_attrs.update({'entity_class': self.__class__.__name__})
Expand Down
16 changes: 16 additions & 0 deletions custom_components/xiaomi_miot/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,21 @@
"already_configured": "Device is already configured",
"not_xiaomi_miio": "Not a Xiaomi miio/miot device."
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "Config device info.",
"data": {
"host": "Local IP for device",
"token": "Token",
"miot_cloud": "Enable miot cloud"
}
}
},
"error": {
"cannot_connect": "Failed to connect the device"
}
}
}
16 changes: 16 additions & 0 deletions custom_components/xiaomi_miot/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,21 @@
"already_configured": "该设备已经配置过",
"not_xiaomi_miio": "该设备不支持miio/miot协议"
}
},
"options": {
"step": {
"user": {
"title": "Xiaomi Miot",
"description": "配置设备信息",
"data": {
"host": "设备IP",
"token": "Token",
"miot_cloud": "开启云端模式"
}
}
},
"error": {
"cannot_connect": "连接设备失败"
}
}
}

0 comments on commit fd0f0b2

Please sign in to comment.