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