333 lines
11 KiB
Python
333 lines
11 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
from homeassistant.components import zeroconf
|
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState
|
|
from homeassistant.const import (
|
|
CONF_DEVICE_CLASS,
|
|
CONF_DEVICES,
|
|
CONF_MODE,
|
|
CONF_NAME,
|
|
CONF_PASSWORD,
|
|
CONF_PAYLOAD_OFF,
|
|
CONF_SENSORS,
|
|
CONF_TIMEOUT,
|
|
CONF_USERNAME,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
MAJOR_VERSION,
|
|
MINOR_VERSION,
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.device_registry import async_get as device_registry
|
|
from homeassistant.helpers.storage import Store
|
|
|
|
from . import system_health
|
|
from .core import devices as core_devices
|
|
from .core.const import (
|
|
CONF_APPID,
|
|
CONF_APPSECRET,
|
|
CONF_DEFAULT_CLASS,
|
|
CONF_DEVICEKEY,
|
|
CONF_RFBRIDGE,
|
|
DOMAIN,
|
|
)
|
|
from .core.ewelink import SIGNAL_ADD_ENTITIES, SIGNAL_CONNECTED, XRegistry
|
|
from .core.ewelink.camera import XCameras
|
|
from .core.ewelink.cloud import APP, AuthError
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS = [
|
|
"binary_sensor",
|
|
"button",
|
|
"climate",
|
|
"cover",
|
|
"fan",
|
|
"light",
|
|
"remote",
|
|
"sensor",
|
|
"switch",
|
|
"number",
|
|
]
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Optional(CONF_APPID): cv.string,
|
|
vol.Optional(CONF_APPSECRET): cv.string,
|
|
vol.Optional(CONF_USERNAME): cv.string,
|
|
vol.Optional(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_DEFAULT_CLASS): cv.string,
|
|
vol.Optional(CONF_SENSORS): cv.ensure_list,
|
|
vol.Optional(CONF_RFBRIDGE): {
|
|
cv.string: vol.Schema(
|
|
{
|
|
vol.Optional(CONF_NAME): cv.string,
|
|
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
|
vol.Optional(CONF_TIMEOUT, default=120): cv.positive_int,
|
|
vol.Optional(CONF_PAYLOAD_OFF): cv.string,
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
),
|
|
},
|
|
vol.Optional(CONF_DEVICES): {
|
|
cv.string: vol.Schema(
|
|
{
|
|
vol.Optional(CONF_NAME): cv.string,
|
|
vol.Optional(CONF_DEVICE_CLASS): vol.Any(str, list),
|
|
vol.Optional(CONF_DEVICEKEY): cv.string,
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
),
|
|
},
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
),
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
UNIQUE_DEVICES = {}
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|
if (MAJOR_VERSION, MINOR_VERSION) < (2023, 1):
|
|
raise Exception("unsupported hass version")
|
|
|
|
# init storage for registries
|
|
hass.data[DOMAIN] = {}
|
|
|
|
# load optional global registry config
|
|
if DOMAIN in config:
|
|
XRegistry.config = conf = config[DOMAIN]
|
|
if CONF_APPID in conf and CONF_APPSECRET in conf:
|
|
APP[0] = (conf[CONF_APPID], conf[CONF_APPSECRET])
|
|
if CONF_DEFAULT_CLASS in conf:
|
|
core_devices.set_default_class(conf.pop(CONF_DEFAULT_CLASS))
|
|
if CONF_SENSORS in conf:
|
|
core_devices.get_spec = core_devices.get_spec_wrapper(
|
|
core_devices.get_spec, conf.pop(CONF_SENSORS)
|
|
)
|
|
|
|
# cameras starts only on first command to it
|
|
cameras = XCameras()
|
|
|
|
try:
|
|
# import ewelink account from YAML (first time)
|
|
data = {
|
|
CONF_USERNAME: XRegistry.config[CONF_USERNAME],
|
|
CONF_PASSWORD: XRegistry.config[CONF_PASSWORD],
|
|
}
|
|
if not hass.config_entries.async_entries(DOMAIN):
|
|
coro = hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=data
|
|
)
|
|
hass.async_create_task(coro)
|
|
except Exception:
|
|
pass
|
|
|
|
async def send_command(call: ServiceCall):
|
|
"""Service for send raw command to device.
|
|
:param call: `device` - required param, all other params - optional
|
|
"""
|
|
params = dict(call.data)
|
|
deviceid = str(params.pop("device"))
|
|
|
|
if len(deviceid) == 10:
|
|
registry: XRegistry = next(
|
|
r for r in hass.data[DOMAIN].values() if deviceid in r.devices
|
|
)
|
|
device = registry.devices[deviceid]
|
|
|
|
# for debugging purposes
|
|
if v := params.get("set_device"):
|
|
device.update(v)
|
|
return
|
|
|
|
params_lan = params.pop("params_lan", None)
|
|
command_lan = params.pop("command_lan", None)
|
|
|
|
await registry.send(device, params, params_lan, command_lan)
|
|
|
|
elif len(deviceid) == 6:
|
|
await cameras.send(deviceid, params["cmd"])
|
|
|
|
else:
|
|
_LOGGER.error(f"Wrong deviceid {deviceid}")
|
|
|
|
hass.services.async_register(DOMAIN, "send_command", send_command)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""
|
|
AUTO mode. If there is a login error to the cloud - it starts in LOCAL
|
|
mode with devices list from cache. Trying to reconnect to the cloud.
|
|
|
|
CLOUD mode. If there is a login error to the cloud - trying to reconnect to
|
|
the cloud.
|
|
|
|
LOCAL mode. If there is a login error to the cloud - it starts with
|
|
devices list from cache.
|
|
"""
|
|
registry = hass.data[DOMAIN].get(entry.entry_id)
|
|
if not registry:
|
|
session = async_get_clientsession(hass)
|
|
hass.data[DOMAIN][entry.entry_id] = registry = XRegistry(session)
|
|
|
|
if entry.options.get("debug") and not _LOGGER.handlers:
|
|
await system_health.setup_debug(hass, _LOGGER)
|
|
|
|
username = entry.data.get(CONF_USERNAME)
|
|
password = entry.data.get(CONF_PASSWORD)
|
|
mode = entry.options.get(CONF_MODE, "auto")
|
|
|
|
# retry only when can't login first time
|
|
if entry.state == ConfigEntryState.SETUP_RETRY:
|
|
assert mode in ("auto", "cloud")
|
|
try:
|
|
await registry.cloud.login(username, password)
|
|
except Exception as e:
|
|
_LOGGER.warning(f"Can't login with mode: {mode}", exc_info=e)
|
|
raise ConfigEntryNotReady(e)
|
|
if mode == "auto":
|
|
registry.cloud.start()
|
|
elif mode == "cloud":
|
|
hass.async_create_task(internal_normal_setup(hass, entry))
|
|
return True
|
|
|
|
if registry.cloud.auth is None and username and password:
|
|
try:
|
|
await registry.cloud.login(username, password)
|
|
except Exception as e:
|
|
_LOGGER.warning(f"Can't login with mode: {mode}", exc_info=e)
|
|
if mode in ("auto", "local"):
|
|
hass.async_create_task(internal_cache_setup(hass, entry))
|
|
if mode in ("auto", "cloud"):
|
|
if isinstance(e, AuthError):
|
|
raise ConfigEntryAuthFailed(e)
|
|
raise ConfigEntryNotReady(e)
|
|
assert mode == "local"
|
|
return True
|
|
|
|
hass.async_create_task(internal_normal_setup(hass, entry))
|
|
return True
|
|
|
|
|
|
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry):
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
registry: XRegistry = hass.data[DOMAIN][entry.entry_id]
|
|
await registry.stop()
|
|
|
|
return True
|
|
|
|
|
|
async def internal_normal_setup(hass: HomeAssistant, entry: ConfigEntry):
|
|
devices = None
|
|
|
|
try:
|
|
registry: XRegistry = hass.data[DOMAIN][entry.entry_id]
|
|
if registry.cloud.auth:
|
|
homes = entry.options.get("homes")
|
|
devices = await registry.cloud.get_devices(homes)
|
|
_LOGGER.debug(f"{len(devices)} devices loaded from Cloud")
|
|
|
|
store = Store(hass, 1, f"{DOMAIN}/{entry.data['username']}.json")
|
|
await store.async_save(devices)
|
|
|
|
except Exception as e:
|
|
_LOGGER.warning("Can't load devices", exc_info=e)
|
|
|
|
await internal_cache_setup(hass, entry, devices)
|
|
|
|
|
|
async def internal_cache_setup(
|
|
hass: HomeAssistant, entry: ConfigEntry, devices: list = None
|
|
):
|
|
registry: XRegistry = hass.data[DOMAIN][entry.entry_id]
|
|
|
|
# this may only happen if async_setup_entry will fail
|
|
if registry.online:
|
|
await async_unload_entry(hass, entry)
|
|
|
|
await asyncio.gather(
|
|
*[
|
|
hass.config_entries.async_forward_entry_setup(entry, domain)
|
|
for domain in PLATFORMS
|
|
]
|
|
)
|
|
|
|
if devices is None:
|
|
store = Store(hass, 1, f"{DOMAIN}/{entry.data['username']}.json")
|
|
devices = await store.async_load()
|
|
if devices:
|
|
# 16 devices loaded from the Cloud Server
|
|
_LOGGER.debug(f"{len(devices)} devices loaded from Cache")
|
|
|
|
if devices:
|
|
devices = internal_unique_devices(entry.entry_id, devices)
|
|
entities = registry.setup_devices(devices)
|
|
else:
|
|
entities = None
|
|
|
|
if not entry.update_listeners:
|
|
entry.add_update_listener(async_update_options)
|
|
|
|
entry.async_on_unload(
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, registry.stop)
|
|
)
|
|
|
|
mode = entry.options.get(CONF_MODE, "auto")
|
|
if mode != "local" and registry.cloud.auth:
|
|
registry.cloud.start()
|
|
if mode != "cloud":
|
|
registry.local.start(await zeroconf.async_get_instance(hass))
|
|
|
|
_LOGGER.debug(mode.upper() + " mode start")
|
|
|
|
# at this moment we hold EVENT_HOMEASSISTANT_START event, because run this
|
|
# coro with `hass.async_create_task` from `async_setup_entry`
|
|
if registry.cloud.task:
|
|
# we get cloud connected signal even with a cloud error, so we won't
|
|
# hold Hass start event forever
|
|
await registry.cloud.dispatcher_wait(SIGNAL_CONNECTED)
|
|
elif registry.local.online:
|
|
# we hope that most of local devices will be discovered in 3 seconds
|
|
await asyncio.sleep(3)
|
|
|
|
# 1. We need add_entities after cloud or local init, so they won't be
|
|
# unavailable at init state
|
|
# 2. We need add_entities before Hass start event, so Hass won't push
|
|
# unavailable state with restored=True attribute to history
|
|
if entities:
|
|
_LOGGER.debug(f"Add {len(entities)} entities")
|
|
registry.dispatcher_send(SIGNAL_ADD_ENTITIES, entities)
|
|
|
|
|
|
def internal_unique_devices(uid: str, devices: list) -> list:
|
|
"""For support multiple integrations - bind each device to one integraion.
|
|
To avoid duplicates.
|
|
"""
|
|
return [
|
|
device
|
|
for device in devices
|
|
if UNIQUE_DEVICES.setdefault(device["deviceid"], uid) == uid
|
|
]
|
|
|
|
|
|
async def async_remove_config_entry_device(
|
|
hass: HomeAssistant, entry: ConfigEntry, device
|
|
) -> bool:
|
|
device_registry(hass).async_remove_device(device.id)
|
|
return True
|