261 lines
8.2 KiB
Python
261 lines
8.2 KiB
Python
import asyncio
|
|
import json
|
|
import logging
|
|
import time
|
|
|
|
import voluptuous as vol
|
|
from homeassistant.components.system_log import CONF_LOGGER
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
EVENT_HOMEASSISTANT_STOP, MAJOR_VERSION, MINOR_VERSION
|
|
)
|
|
from homeassistant.core import HomeAssistant, ServiceCall
|
|
from homeassistant.helpers import (
|
|
aiohttp_client as ac, config_validation as cv, device_registry as dr
|
|
)
|
|
from homeassistant.helpers.storage import Store
|
|
|
|
from . import system_health
|
|
from .core import logger, utils
|
|
from .core.const import DOMAIN, TITLE
|
|
from .core.entity import XEntity
|
|
from .core.gateway import XGateway
|
|
from .core.xiaomi_cloud import MiCloud
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DOMAINS = [
|
|
'alarm_control_panel', 'binary_sensor', 'climate', 'cover', 'light',
|
|
'number', 'select', 'sensor', 'switch'
|
|
]
|
|
|
|
CONF_DEVICES = 'devices'
|
|
CONF_ATTRIBUTES_TEMPLATE = 'attributes_template'
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: vol.Schema({
|
|
vol.Optional(CONF_DEVICES): {
|
|
cv.string: vol.Schema({
|
|
vol.Optional('occupancy_timeout'): cv.positive_int,
|
|
}, extra=vol.ALLOW_EXTRA),
|
|
},
|
|
CONF_LOGGER: logger.CONFIG_SCHEMA,
|
|
vol.Optional(CONF_ATTRIBUTES_TEMPLATE): cv.template
|
|
}, extra=vol.ALLOW_EXTRA),
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, hass_config: dict):
|
|
if (MAJOR_VERSION, MINOR_VERSION) < (2021, 12):
|
|
_LOGGER.error("Minimum supported Hass version 2021.12")
|
|
return False
|
|
|
|
config = hass_config.get(DOMAIN) or {}
|
|
|
|
if CONF_LOGGER in config:
|
|
logger.init(__name__, config[CONF_LOGGER], hass.config.config_dir)
|
|
|
|
info = await hass.helpers.system_info.async_get_system_info()
|
|
_LOGGER.debug(f"SysInfo: {info}")
|
|
|
|
# update global debug_mode for all gateways
|
|
if 'debug_mode' in config[CONF_LOGGER]:
|
|
setattr(XGateway, 'debug_mode', config[CONF_LOGGER]['debug_mode'])
|
|
|
|
if CONF_ATTRIBUTES_TEMPLATE in config:
|
|
XEntity.attributes_template = config[CONF_ATTRIBUTES_TEMPLATE]
|
|
XEntity.attributes_template.hass = hass
|
|
|
|
hass.data[DOMAIN] = {}
|
|
|
|
await utils.load_devices(hass, config.get(CONF_DEVICES))
|
|
|
|
_register_send_command(hass)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
"""Support two kind of enties - MiCloud and Gateway."""
|
|
|
|
# entry for MiCloud login
|
|
if 'servers' in entry.data:
|
|
return await _setup_micloud_entry(hass, entry)
|
|
|
|
# migrate data (also after first setup) to options
|
|
if entry.data:
|
|
hass.config_entries.async_update_entry(entry, data={},
|
|
options=entry.data)
|
|
|
|
entries = hass.config_entries.async_entries(DOMAIN)
|
|
if any(e.options.get('debug') for e in entries):
|
|
await system_health.setup_debug(hass, _LOGGER)
|
|
|
|
# add options handler
|
|
if not entry.update_listeners:
|
|
entry.add_update_listener(async_update_options)
|
|
|
|
hass.data[DOMAIN][entry.entry_id] = XGateway(**entry.options)
|
|
|
|
hass.async_create_task(_setup_domains(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):
|
|
# check unload cloud integration
|
|
if entry.entry_id not in hass.data[DOMAIN]:
|
|
return
|
|
|
|
# remove all stats entities if disable stats
|
|
if not entry.options.get('stats'):
|
|
utils.remove_stats(hass, entry.entry_id)
|
|
|
|
gw: XGateway = hass.data[DOMAIN][entry.entry_id]
|
|
await gw.stop()
|
|
|
|
await asyncio.gather(*[
|
|
hass.config_entries.async_forward_entry_unload(entry, domain)
|
|
for domain in DOMAINS
|
|
])
|
|
|
|
return True
|
|
|
|
|
|
# noinspection PyUnusedLocal
|
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
return True
|
|
|
|
|
|
async def _setup_domains(hass: HomeAssistant, entry: ConfigEntry):
|
|
# init setup for each supported domains
|
|
await asyncio.gather(*[
|
|
hass.config_entries.async_forward_entry_setup(entry, domain)
|
|
for domain in DOMAINS
|
|
])
|
|
|
|
gw: XGateway = hass.data[DOMAIN][entry.entry_id]
|
|
gw.start()
|
|
|
|
entry.async_on_unload(
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw.stop)
|
|
)
|
|
|
|
|
|
async def _setup_micloud_entry(hass: HomeAssistant, config_entry):
|
|
data: dict = config_entry.data.copy()
|
|
|
|
# quick fix Hass 2022.8 - parallel integration loading
|
|
# so Gateway loads before the Cloud with default devices names
|
|
store = Store(hass, 1, f"{DOMAIN}/{data['username']}.json")
|
|
devices = await store.async_load()
|
|
if devices:
|
|
_LOGGER.debug(f"Loaded from cache {len(devices)} devices")
|
|
_update_devices(devices)
|
|
|
|
session = ac.async_create_clientsession(hass)
|
|
hass.data[DOMAIN]['cloud'] = cloud = MiCloud(session, data['servers'])
|
|
|
|
if 'service_token' in data:
|
|
# load devices with saved MiCloud auth
|
|
cloud.auth = data
|
|
devices = await cloud.get_devices()
|
|
else:
|
|
devices = None
|
|
|
|
if devices is None:
|
|
_LOGGER.debug(f"Login to MiCloud for {config_entry.title}")
|
|
if await cloud.login(data['username'], data['password']):
|
|
# update MiCloud auth in .storage
|
|
data.update(cloud.auth)
|
|
hass.config_entries.async_update_entry(config_entry, data=data)
|
|
|
|
devices = await cloud.get_devices()
|
|
if devices is None:
|
|
_LOGGER.error("Can't load devices from MiCloud")
|
|
|
|
else:
|
|
_LOGGER.error("Can't login to MiCloud")
|
|
|
|
if devices is not None:
|
|
_LOGGER.debug(f"Loaded from MiCloud {len(devices)} devices")
|
|
_update_devices(devices)
|
|
await store.async_save(devices)
|
|
else:
|
|
_LOGGER.debug("No devices in .storage")
|
|
return False
|
|
|
|
# TODO: Think about a bunch of devices
|
|
if 'devices' not in hass.data[DOMAIN]:
|
|
hass.data[DOMAIN]['devices'] = devices
|
|
else:
|
|
hass.data[DOMAIN]['devices'] += devices
|
|
|
|
for device in devices:
|
|
# key - mac for BLE, and did for others
|
|
did = device['did'] if device['pid'] != 6 else \
|
|
device['mac'].replace(':', '').lower()
|
|
XGateway.defaults.setdefault(did, {})
|
|
# don't override name if exists
|
|
XGateway.defaults[did].setdefault('name', device['name'])
|
|
|
|
return True
|
|
|
|
|
|
def _update_devices(devices: list):
|
|
for device in devices:
|
|
# key - mac for BLE, and did for others
|
|
did = device['did'] if device['pid'] != 6 else \
|
|
device['mac'].replace(':', '').lower()
|
|
XGateway.defaults.setdefault(did, {})
|
|
# don't override name if exists
|
|
XGateway.defaults[did].setdefault('name', device['name'])
|
|
|
|
|
|
def _register_send_command(hass: HomeAssistant):
|
|
async def send_command(call: ServiceCall):
|
|
host = call.data["host"]
|
|
gw = next(
|
|
gw for gw in hass.data[DOMAIN].values()
|
|
if isinstance(gw, XGateway) and gw.host == host
|
|
)
|
|
cmd = call.data["command"].split(" ")
|
|
if cmd[0] == "miio":
|
|
raw = json.loads(call.data["data"])
|
|
resp = await gw.miio.send(raw['method'], raw.get('params'))
|
|
hass.components.persistent_notification.async_create(
|
|
str(resp), TITLE
|
|
)
|
|
elif cmd[0] == "set_state": # for debug purposes
|
|
device = gw.devices.get(cmd[1])
|
|
raw = json.loads(call.data["data"])
|
|
device.available = True
|
|
device.decode_ts = time.time()
|
|
device.update(raw)
|
|
|
|
hass.services.async_register(DOMAIN, "send_command", send_command)
|
|
|
|
|
|
async def async_remove_config_entry_device(
|
|
hass: HomeAssistant, entry: ConfigEntry, device: dr.DeviceEntry
|
|
) -> bool:
|
|
"""Supported from Hass v2022.3"""
|
|
dr.async_get(hass).async_remove_device(device.id)
|
|
|
|
try:
|
|
# check if device is zigbee
|
|
if any(c[0] == dr.CONNECTION_ZIGBEE for c in device.connections):
|
|
unique_id = next(
|
|
i[1] for i in device.identifiers if i[0] == DOMAIN
|
|
)
|
|
await utils.remove_zigbee(unique_id)
|
|
|
|
return True
|
|
except Exception as e:
|
|
_LOGGER.error("Can't delete device", exc_info=e)
|
|
return False
|