HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: /volume1/docker/homeassistant2025/custom_components/sonoff/__init__.py
import asyncio
import logging

import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
from homeassistant.const import (
    CONF_DEVICES,
    CONF_DEVICE_CLASS,
    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.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_COUNTRY_CODE,
    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
from .core.xutils import create_clientsession

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [
    "alarm_control_panel",
    "binary_sensor",
    "button",
    "climate",
    "cover",
    "fan",
    "light",
    "media_player",
    "remote",
    "sensor",
    "switch",
    "number",
    "select"
]

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, 2):
        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]
            APP.append(conf[CONF_APPSECRET])
        if CONF_DEFAULT_CLASS in conf:
            core_devices.set_default_class(conf.get(CONF_DEFAULT_CLASS))
        if CONF_SENSORS in conf:
            core_devices.get_spec = core_devices.get_spec_wrapper(
                core_devices.get_spec, conf.get(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, config_entry: ConfigEntry) -> bool:
    if config_entry.options.get("debug") and not _LOGGER.handlers:
        await system_health.setup_debug(hass, _LOGGER)

    registry: XRegistry = hass.data[DOMAIN].get(config_entry.entry_id)
    if not registry:
        session = create_clientsession(hass)
        hass.data[DOMAIN][config_entry.entry_id] = registry = XRegistry(session)

    mode = config_entry.options.get(CONF_MODE, "auto")
    data = config_entry.data

    # if has cloud password and not auth
    if not registry.cloud.auth and data.get(CONF_PASSWORD):
        try:
            await registry.cloud.login(**data)
            # store country_code for future requests optimisation
            if not data.get(CONF_COUNTRY_CODE):
                hass.config_entries.async_update_entry(
                    config_entry,
                    data={**data, CONF_COUNTRY_CODE: registry.cloud.country_code},
                )
        except Exception as e:
            _LOGGER.warning(f"Can't login in {mode} mode: {repr(e)}")
            if mode == "cloud":
                # can't continue in cloud mode
                if isinstance(e, AuthError):
                    raise ConfigEntryAuthFailed(e)
                raise ConfigEntryNotReady(e)

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

    config_entry.async_on_unload(
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, registry.stop)
    )

    # important to run before registry.setup_devices (for remote childs)
    await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

    devices: list[dict] | None = None
    store = Store(hass, 1, f"{DOMAIN}/{config_entry.data['username']}.json")

    # if auth OK - load devices from cloud
    if registry.cloud.auth:
        try:
            homes = config_entry.options.get("homes")
            devices = await registry.cloud.get_devices(homes)
            _LOGGER.debug(f"{len(devices)} devices loaded from Cloud")

            # store devices to cache
            await store.async_save(devices)

        except Exception as e:
            _LOGGER.warning("Can't load devices", exc_info=e)

    if not devices:
        if devices := await store.async_load():
            _LOGGER.debug(f"{len(devices)} devices loaded from Cache")

    if devices:
        # we need to setup_devices before local.start
        devices = internal_unique_devices(config_entry.entry_id, devices)
        entities = registry.setup_devices(devices)
    else:
        entities = None

    if mode in ("auto", "cloud") and config_entry.data.get(CONF_PASSWORD):
        registry.cloud.start(**config_entry.data)

    if mode in ("auto", "local"):
        registry.local.start(await zeroconf.async_get_instance(hass))

    _LOGGER.debug(mode.upper() + " mode start")

    # at this moment we hold EVENT_HOMEASSISTANT_START event
    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)

    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):
    ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

    registry: XRegistry = hass.data[DOMAIN][entry.entry_id]
    await registry.stop()

    return ok


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