Example #1
0
async def async_setup(opp: OpenPeerPower, config: ConfigType) -> bool:
    """Set up Hyperion component."""
    opp.data[DOMAIN] = {}
    return True
Example #2
0
async def async_unload_entry(opp: OpenPeerPower, config_entry: ConfigEntry):
    """Unload a config entry."""
    # unload srp client
    opp.data[SRP_ENERGY_DOMAIN] = None
    # Remove config entry
    return await opp.config_entries.async_unload_platforms(config_entry, PLATFORMS)
Example #3
0
async def async_setup_entry(opp: core.OpenPeerPower,
                            entry: config_entries.ConfigEntry):
    """Set up a bridge from a config entry."""

    # Migrate allow_unreachable from config entry data to config entry options
    if (CONF_ALLOW_UNREACHABLE not in entry.options
            and CONF_ALLOW_UNREACHABLE in entry.data and
            entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE):
        options = {
            **entry.options,
            CONF_ALLOW_UNREACHABLE:
            entry.data[CONF_ALLOW_UNREACHABLE],
        }
        data = entry.data.copy()
        data.pop(CONF_ALLOW_UNREACHABLE)
        opp.config_entries.async_update_entry(entry,
                                              data=data,
                                              options=options)

    # Migrate allow_hue_groups from config entry data to config entry options
    if (CONF_ALLOW_HUE_GROUPS not in entry.options
            and CONF_ALLOW_HUE_GROUPS in entry.data
            and entry.data[CONF_ALLOW_HUE_GROUPS] != DEFAULT_ALLOW_HUE_GROUPS):
        options = {
            **entry.options,
            CONF_ALLOW_HUE_GROUPS:
            entry.data[CONF_ALLOW_HUE_GROUPS],
        }
        data = entry.data.copy()
        data.pop(CONF_ALLOW_HUE_GROUPS)
        opp.config_entries.async_update_entry(entry,
                                              data=data,
                                              options=options)

    bridge = HueBridge(opp, entry)

    if not await bridge.async_setup():
        return False

    _register_services(opp)

    config = bridge.api.config

    # For backwards compat
    unique_id = normalize_bridge_id(config.bridgeid)
    if entry.unique_id is None:
        opp.config_entries.async_update_entry(entry, unique_id=unique_id)

    # For recovering from bug where we incorrectly assumed homekit ID = bridge ID
    elif entry.unique_id != unique_id:
        # Find entries with this unique ID
        other_entry = next(
            (entry for entry in opp.config_entries.async_entries(DOMAIN)
             if entry.unique_id == unique_id),
            None,
        )

        if other_entry is None:
            # If no other entry, update unique ID of this entry ID.
            opp.config_entries.async_update_entry(entry, unique_id=unique_id)

        elif other_entry.source == config_entries.SOURCE_IGNORE:
            # There is another entry but it is ignored, delete that one and update this one
            opp.async_create_task(
                opp.config_entries.async_remove(other_entry.entry_id))
            opp.config_entries.async_update_entry(entry, unique_id=unique_id)
        else:
            # There is another entry that already has the right unique ID. Delete this entry
            opp.async_create_task(
                opp.config_entries.async_remove(entry.entry_id))
            return False

    device_registry = await dr.async_get_registry(opp)
    device_registry.async_get_or_create(
        config_entry_id=entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, config.mac)},
        identifiers={(DOMAIN, config.bridgeid)},
        manufacturer="Signify",
        name=config.name,
        model=config.modelid,
        sw_version=config.swversion,
    )

    if config.modelid == "BSB002" and config.swversion < "1935144040":
        persistent_notification.async_create(
            opp,
            "Your Hue hub has a known security vulnerability ([CVE-2020-6007](https://cve.circl.lu/cve/CVE-2020-6007)). Go to the Hue app and check for software updates.",
            "Signify Hue",
            "hue_hub_firmware",
        )

    elif config.swupdate2_bridge_state == "readytoinstall":
        err = (
            "Please check for software updates of the bridge in the Philips Hue App.",
            "Signify Hue",
            "hue_hub_firmware",
        )
        _LOGGER.warning(err)

    return True
Example #4
0
def async_track_state_change_event(
    opp: OpenPeerPower,
    entity_ids: str | Iterable[str],
    action: Callable[[Event], Any],
) -> Callable[[], None]:
    """Track specific state change events indexed by entity_id.

    Unlike async_track_state_change, async_track_state_change_event
    passes the full event to the callback.

    In order to avoid having to iterate a long list
    of EVENT_STATE_CHANGED and fire and create a job
    for each one, we keep a dict of entity ids that
    care about the state change events so we can
    do a fast dict lookup to route events.
    """
    entity_ids = _async_string_to_lower_list(entity_ids)
    if not entity_ids:
        return _remove_empty_listener

    entity_callbacks = opp.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {})

    if TRACK_STATE_CHANGE_LISTENER not in opp.data:

        @callback
        def _async_state_change_filter(event: Event) -> bool:
            """Filter state changes by entity_id."""
            return event.data.get("entity_id") in entity_callbacks

        @callback
        def _async_state_change_dispatcher(event: Event) -> None:
            """Dispatch state changes by entity_id."""
            entity_id = event.data.get("entity_id")

            if entity_id not in entity_callbacks:
                return

            for job in entity_callbacks[entity_id][:]:
                try:
                    opp.async_run_opp_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing state change for %s", entity_id
                    )

        opp.data[TRACK_STATE_CHANGE_LISTENER] = opp.bus.async_listen(
            EVENT_STATE_CHANGED,
            _async_state_change_dispatcher,
            event_filter=_async_state_change_filter,
        )

    job = OppJob(action)

    for entity_id in entity_ids:
        entity_callbacks.setdefault(entity_id, []).append(job)

    @callback
    def remove_listener() -> None:
        """Remove state change listener."""
        _async_remove_indexed_listeners(
            opp,
            TRACK_STATE_CHANGE_CALLBACKS,
            TRACK_STATE_CHANGE_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener
Example #5
0
async def async_process_op_core_config(opp: OpenPeerPower,
                                       config: dict) -> None:
    """Process the [openpeerpower] section from the configuration.

    This method is a coroutine.
    """
    config = CORE_CONFIG_SCHEMA(config)

    # Only load auth during startup.
    if not hasattr(opp, "auth"):
        auth_conf = config.get(CONF_AUTH_PROVIDERS)

        if auth_conf is None:
            auth_conf = [{"type": "openpeerpower"}]

        mfa_conf = config.get(
            CONF_AUTH_MFA_MODULES,
            [{
                "type": "totp",
                "id": "totp",
                "name": "Authenticator app"
            }],
        )

        setattr(opp, "auth", await
                auth.auth_manager_from_config(opp, auth_conf, mfa_conf))

    await opp.config.async_load()

    hac = opp.config

    if any(k in config for k in [
            CONF_LATITUDE,
            CONF_LONGITUDE,
            CONF_NAME,
            CONF_ELEVATION,
            CONF_TIME_ZONE,
            CONF_UNIT_SYSTEM,
            CONF_EXTERNAL_URL,
            CONF_INTERNAL_URL,
    ]):
        hac.config_source = SOURCE_YAML

    for key, attr in (
        (CONF_LATITUDE, "latitude"),
        (CONF_LONGITUDE, "longitude"),
        (CONF_NAME, "location_name"),
        (CONF_ELEVATION, "elevation"),
        (CONF_INTERNAL_URL, "internal_url"),
        (CONF_EXTERNAL_URL, "external_url"),
        (CONF_MEDIA_DIRS, "media_dirs"),
        (CONF_LEGACY_TEMPLATES, "legacy_templates"),
    ):
        if key in config:
            setattr(hac, attr, config[key])

    if CONF_TIME_ZONE in config:
        hac.set_time_zone(config[CONF_TIME_ZONE])

    if CONF_MEDIA_DIRS not in config:
        if is_docker_env():
            hac.media_dirs = {"local": "/media"}
        else:
            hac.media_dirs = {"local": opp.config.path("media")}

    # Init whitelist external dir
    hac.allowlist_external_dirs = {
        opp.config.path("www"), *hac.media_dirs.values()
    }
    if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
        hac.allowlist_external_dirs.update(
            set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))

    elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
        _LOGGER.warning(
            "Key %s has been replaced with %s. Please update your config",
            LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
            CONF_ALLOWLIST_EXTERNAL_DIRS,
        )
        hac.allowlist_external_dirs.update(
            set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS]))

    # Init whitelist external URL list – make sure to add / to every URL that doesn't
    # already have it so that we can properly test "path ownership"
    if CONF_ALLOWLIST_EXTERNAL_URLS in config:
        hac.allowlist_external_urls.update(
            url if url.endswith("/") else f"{url}/"
            for url in config[CONF_ALLOWLIST_EXTERNAL_URLS])

    # Customize
    cust_exact = dict(config[CONF_CUSTOMIZE])
    cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
    cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])

    for name, pkg in config[CONF_PACKAGES].items():
        pkg_cust = pkg.get(CONF_CORE)

        if pkg_cust is None:
            continue

        try:
            pkg_cust = CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
        except vol.Invalid:
            _LOGGER.warning("Package %s contains invalid customize", name)
            continue

        cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
        cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
        cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])

    opp.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)

    if CONF_UNIT_SYSTEM in config:
        if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL:
            hac.units = IMPERIAL_SYSTEM
        else:
            hac.units = METRIC_SYSTEM
    elif CONF_TEMPERATURE_UNIT in config:
        unit = config[CONF_TEMPERATURE_UNIT]
        hac.units = METRIC_SYSTEM if unit == TEMP_CELSIUS else IMPERIAL_SYSTEM
        _LOGGER.warning(
            "Found deprecated temperature unit in core "
            "configuration expected unit system. Replace '%s: %s' "
            "with '%s: %s'",
            CONF_TEMPERATURE_UNIT,
            unit,
            CONF_UNIT_SYSTEM,
            hac.units.name,
        )
Example #6
0
async def async_setup(opp: OpenPeerPower, config: dict[str, Any]) -> bool:
    """Set up the Dynalite platform."""
    conf = config.get(DOMAIN)
    LOGGER.debug("Setting up dynalite component config = %s", conf)

    if conf is None:
        conf = {}

    opp.data[DOMAIN] = {}

    # User has configured bridges
    if CONF_BRIDGES not in conf:
        return True

    bridges = conf[CONF_BRIDGES]

    for bridge_conf in bridges:
        host = bridge_conf[CONF_HOST]
        LOGGER.debug("Starting config entry flow host=%s conf=%s", host,
                     bridge_conf)

        opp.async_create_task(
            opp.config_entries.flow.async_init(
                DOMAIN,
                context={"source": config_entries.SOURCE_IMPORT},
                data=bridge_conf,
            ))

    async def dynalite_service(service_call: ServiceCall):
        data = service_call.data
        host = data.get(ATTR_HOST, "")
        bridges = []
        for cur_bridge in opp.data[DOMAIN].values():
            if not host or cur_bridge.host == host:
                bridges.append(cur_bridge)
        LOGGER.debug("Selected bridged for service call: %s", bridges)
        if service_call.service == SERVICE_REQUEST_AREA_PRESET:
            bridge_attr = "request_area_preset"
        elif service_call.service == SERVICE_REQUEST_CHANNEL_LEVEL:
            bridge_attr = "request_channel_level"
        for bridge in bridges:
            getattr(bridge.dynalite_devices,
                    bridge_attr)(data[ATTR_AREA], data.get(ATTR_CHANNEL))

    opp.services.async_register(
        DOMAIN,
        SERVICE_REQUEST_AREA_PRESET,
        dynalite_service,
        vol.Schema({
            vol.Optional(ATTR_HOST): cv.string,
            vol.Required(ATTR_AREA): int,
            vol.Optional(ATTR_CHANNEL): int,
        }),
    )

    opp.services.async_register(
        DOMAIN,
        SERVICE_REQUEST_CHANNEL_LEVEL,
        dynalite_service,
        vol.Schema({
            vol.Optional(ATTR_HOST): cv.string,
            vol.Required(ATTR_AREA): int,
            vol.Required(ATTR_CHANNEL): int,
        }),
    )

    return True
async def async_from_config_dict(
    config: Dict[str, Any], opp: core.OpenPeerPower
) -> Optional[core.OpenPeerPower]:
    """Try to configure Open Peer Power from a configuration dictionary.

    Dynamically loads required components and its dependencies.
    This method is a coroutine.
    """
    start = monotonic()

    opp.config_entries = config_entries.ConfigEntries(opp, config)
    await opp.config_entries.async_initialize()

    # Set up core.
    _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)

    if not all(
        await asyncio.gather(
            *(
                async_setup_component(opp, domain, config)
                for domain in CORE_INTEGRATIONS
            )
        )
    ):
        _LOGGER.error("Open Peer Power core failed to initialize. ")
        return None

    _LOGGER.debug("Open Peer Power core initialized")

    core_config = config.get(core.DOMAIN, {})

    try:
        await conf_util.async_process_op_core_config(opp, core_config)
    except vol.Invalid as config_err:
        conf_util.async_log_exception(config_err, "openpeerpower", core_config, opp)
        return None
    except OpenPeerPowerError:
        _LOGGER.error(
            "Open Peer Power core failed to initialize. "
            "Further initialization aborted"
        )
        return None

    await _async_set_up_integrations(opp, config)

    stop = monotonic()
    _LOGGER.info("Open Peer Power initialized in %.2fs", stop - start)

    if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER:
        msg = (
            "Support for the running Python version "
            f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will "
            f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. "
            "Please upgrade Python to "
            f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or "
            "higher."
        )
        _LOGGER.warning(msg)
        opp.components.persistent_notification.async_create(
            msg, "Python version", "python_version"
        )

    return opp
Example #8
0
async def async_setup(opp: OpenPeerPower, config: ConfigType) -> bool:
    """Set up the HTTP API and debug interface."""
    conf: ConfData | None = config.get(DOMAIN)

    if conf is None:
        conf = cast(ConfData, HTTP_SCHEMA({}))

    server_host = conf.get(CONF_SERVER_HOST)
    server_port = conf[CONF_SERVER_PORT]
    ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
    ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
    ssl_key = conf.get(CONF_SSL_KEY)
    cors_origins = conf[CONF_CORS_ORIGINS]
    use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
    trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or []
    is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
    login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
    ssl_profile = conf[CONF_SSL_PROFILE]

    server = OpenPeerPowerHTTP(
        opp,
        server_host=server_host,
        server_port=server_port,
        ssl_certificate=ssl_certificate,
        ssl_peer_certificate=ssl_peer_certificate,
        ssl_key=ssl_key,
        cors_origins=cors_origins,
        use_x_forwarded_for=use_x_forwarded_for,
        trusted_proxies=trusted_proxies,
        login_threshold=login_threshold,
        is_ban_enabled=is_ban_enabled,
        ssl_profile=ssl_profile,
    )

    async def stop_server(event: Event) -> None:
        """Stop the server."""
        await server.stop()

    async def start_server(*_: Any) -> None:
        """Start the server."""
        with async_start_setup(opp, ["http"]):
            opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP, stop_server)
            # We already checked it's not None.
            assert conf is not None
            await start_http_server_and_save_config(opp, dict(conf), server)

    async_when_setup_or_start(opp, "frontend", start_server)

    opp.http = server

    local_ip = await opp.async_add_executor_job(opp_util.get_local_ip)

    host = local_ip
    if server_host is not None:
        # Assume the first server host name provided as API host
        host = server_host[0]

    opp.config.api = ApiConfig(local_ip, host, server_port, ssl_certificate
                               is not None)

    return True
Example #9
0
async def async_setup(opp: OpenPeerPower, config: Dict) -> bool:
    """Set up configured zones as well as Open Peer Power zone if necessary."""
    component = entity_component.EntityComponent(_LOGGER, DOMAIN, opp)
    id_manager = collection.IDManager()

    yaml_collection = IDLessCollection(
        logging.getLogger(f"{__name__}.yaml_collection"), id_manager)
    collection.attach_entity_component_collection(
        component, yaml_collection, lambda conf: Zone(conf, False))

    storage_collection = ZoneStorageCollection(
        storage.Store(opp, STORAGE_VERSION, STORAGE_KEY),
        logging.getLogger(f"{__name__}_storage_collection"),
        id_manager,
    )
    collection.attach_entity_component_collection(
        component, storage_collection, lambda conf: Zone(conf, True))

    if DOMAIN in config:
        await yaml_collection.async_load(config[DOMAIN])

    await storage_collection.async_load()

    collection.StorageCollectionWebsocket(storage_collection, DOMAIN, DOMAIN,
                                          CREATE_FIELDS,
                                          UPDATE_FIELDS).async_setup(opp)

    async def _collection_changed(change_type: str, item_id: str,
                                  config: Optional[Dict]) -> None:
        """Handle a collection change: clean up entity registry on removals."""
        if change_type != collection.CHANGE_REMOVED:
            return

        ent_reg = await entity_registry.async_get_registry(opp)
        ent_reg.async_remove(
            cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)))

    storage_collection.async_add_listener(_collection_changed)

    async def reload_service_handler(service_call: ServiceCall) -> None:
        """Remove all zones and load new ones from config."""
        conf = await component.async_prepare_reload(skip_reset=True)
        if conf is None:
            return
        await yaml_collection.async_load(conf.get(DOMAIN, []))

    service.async_register_admin_service(
        opp,
        DOMAIN,
        SERVICE_RELOAD,
        reload_service_handler,
        schema=RELOAD_SERVICE_SCHEMA,
    )

    if component.get_entity("zone.home"):
        return True

    home_zone = Zone(
        _home_conf(opp),
        True,
    )
    home_zone.entity_id = ENTITY_ID_HOME
    await component.async_add_entities([home_zone])  # type: ignore

    async def core_config_updated(_: Event) -> None:
        """Handle core config updated."""
        await home_zone.async_update_config(_home_conf(opp))

    opp.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)

    opp.data[DOMAIN] = storage_collection

    return True
Example #10
0
async def async_setup_entry(opp: OpenPeerPower, entry: ConfigEntry) -> None:
    """Set up Aqualink from a config entry."""
    username = entry.data[CONF_USERNAME]
    password = entry.data[CONF_PASSWORD]

    # These will contain the initialized devices
    binary_sensors = opp.data[DOMAIN][BINARY_SENSOR_DOMAIN] = []
    climates = opp.data[DOMAIN][CLIMATE_DOMAIN] = []
    lights = opp.data[DOMAIN][LIGHT_DOMAIN] = []
    sensors = opp.data[DOMAIN][SENSOR_DOMAIN] = []
    switches = opp.data[DOMAIN][SWITCH_DOMAIN] = []

    session = async_get_clientsession(opp)
    aqualink = AqualinkClient(username, password, session)
    try:
        await aqualink.login()
    except AqualinkLoginException as login_exception:
        _LOGGER.error("Failed to login: %s", login_exception)
        return False
    except (
            asyncio.TimeoutError,
            aiohttp.client_exceptions.ClientConnectorError,
    ) as aio_exception:
        _LOGGER.warning("Exception raised while attempting to login: %s",
                        aio_exception)
        raise ConfigEntryNotReady from aio_exception

    systems = await aqualink.get_systems()
    systems = list(systems.values())
    if not systems:
        _LOGGER.error("No systems detected or supported")
        return False

    # Only supporting the first system for now.
    devices = await systems[0].get_devices()

    for dev in devices.values():
        if isinstance(dev, AqualinkThermostat):
            climates += [dev]
        elif isinstance(dev, AqualinkLight):
            lights += [dev]
        elif isinstance(dev, AqualinkBinarySensor):
            binary_sensors += [dev]
        elif isinstance(dev, AqualinkSensor):
            sensors += [dev]
        elif isinstance(dev, AqualinkToggle):
            switches += [dev]

    forward_setup = opp.config_entries.async_forward_entry_setup
    if binary_sensors:
        _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors),
                      binary_sensors)
        opp.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN))
    if climates:
        _LOGGER.debug("Got %s climates: %s", len(climates), climates)
        opp.async_create_task(forward_setup(entry, CLIMATE_DOMAIN))
    if lights:
        _LOGGER.debug("Got %s lights: %s", len(lights), lights)
        opp.async_create_task(forward_setup(entry, LIGHT_DOMAIN))
    if sensors:
        _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors)
        opp.async_create_task(forward_setup(entry, SENSOR_DOMAIN))
    if switches:
        _LOGGER.debug("Got %s switches: %s", len(switches), switches)
        opp.async_create_task(forward_setup(entry, SWITCH_DOMAIN))

    async def _async_systems_update(now):
        """Refresh internal state for all systems."""
        prev = systems[0].last_run_success

        await systems[0].update()
        success = systems[0].last_run_success

        if not success and prev:
            _LOGGER.warning("Failed to refresh iAqualink state")
        elif success and not prev:
            _LOGGER.warning("Reconnected to iAqualink")

        async_dispatcher_send(opp, DOMAIN)

    async_track_time_interval(opp, _async_systems_update, UPDATE_INTERVAL)

    return True
Example #11
0
async def async_setup(opp: OpenPeerPower, config: dict):
    """Set up the zodiac component."""
    opp.async_create_task(
        async_load_platform(opp, "sensor", DOMAIN, {}, config))

    return True
Example #12
0
async def async_process_op_core_config(opp: OpenPeerPower, config: Dict) -> None:
    """Process the [openpeerpower] section from the configuration.

    This method is a coroutine.
    """
    config = CORE_CONFIG_SCHEMA(config)

    # Only load auth during startup.
    if not hasattr(opp, "auth"):
        auth_conf = config.get(CONF_AUTH_PROVIDERS)

        if auth_conf is None:
            auth_conf = [{"type": "openpeerpower"}]

        mfa_conf = config.get(
            CONF_AUTH_MFA_MODULES,
            [{"type": "totp", "id": "totp", "name": "Authenticator app"}],
        )

        setattr(
            opp, "auth", await auth.auth_manager_from_config(opp, auth_conf, mfa_conf)
        )

    await opp.config.async_load()

    hac = opp.config

    if any(
        [
            k in config
            for k in [
                CONF_LATITUDE,
                CONF_LONGITUDE,
                CONF_NAME,
                CONF_ELEVATION,
                CONF_TIME_ZONE,
                CONF_UNIT_SYSTEM,
            ]
        ]
    ):
        hac.config_source = SOURCE_YAML

    for key, attr in (
        (CONF_LATITUDE, "latitude"),
        (CONF_LONGITUDE, "longitude"),
        (CONF_NAME, "location_name"),
        (CONF_ELEVATION, "elevation"),
    ):
        if key in config:
            setattr(hac, attr, config[key])

    if CONF_TIME_ZONE in config:
        hac.set_time_zone(config[CONF_TIME_ZONE])

    # Init whitelist external dir
    hac.whitelist_external_dirs = {opp.config.path("www")}
    if CONF_WHITELIST_EXTERNAL_DIRS in config:
        hac.whitelist_external_dirs.update(set(config[CONF_WHITELIST_EXTERNAL_DIRS]))

    # Customize
    cust_exact = dict(config[CONF_CUSTOMIZE])
    cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
    cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])

    for name, pkg in config[CONF_PACKAGES].items():
        pkg_cust = pkg.get(CONF_CORE)

        if pkg_cust is None:
            continue

        try:
            pkg_cust = CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
        except vol.Invalid:
            _LOGGER.warning("Package %s contains invalid customize", name)
            continue

        cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
        cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
        cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])

    opp.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)

    if CONF_UNIT_SYSTEM in config:
        if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL:
            hac.units = IMPERIAL_SYSTEM
        else:
            hac.units = METRIC_SYSTEM
    elif CONF_TEMPERATURE_UNIT in config:
        unit = config[CONF_TEMPERATURE_UNIT]
        if unit == TEMP_CELSIUS:
            hac.units = METRIC_SYSTEM
        else:
            hac.units = IMPERIAL_SYSTEM
        _LOGGER.warning(
            "Found deprecated temperature unit in core "
            "configuration expected unit system. Replace '%s: %s' "
            "with '%s: %s'",
            CONF_TEMPERATURE_UNIT,
            unit,
            CONF_UNIT_SYSTEM,
            hac.units.name,
        )
Example #13
0
def async_set_agent(opp: core.OpenPeerPower, agent: AbstractConversationAgent):
    """Set the agent to handle the conversations."""
    opp.data[DATA_AGENT] = agent
Example #14
0
async def async_setup_entry(opp: OpenPeerPower, config_entry: ConfigEntry) -> bool:
    """Set up Hyperion from a config entry."""
    host = config_entry.data[CONF_HOST]
    port = config_entry.data[CONF_PORT]
    token = config_entry.data.get(CONF_TOKEN)

    hyperion_client = await async_create_connect_hyperion_client(
        host, port, token=token, raw_connection=True
    )

    # Client won't connect? => Not ready.
    if not hyperion_client:
        raise ConfigEntryNotReady
    version = await hyperion_client.async_sysinfo_version()
    if version is not None:
        with suppress(ValueError):
            if AwesomeVersion(version) < AwesomeVersion(HYPERION_VERSION_WARN_CUTOFF):
                _LOGGER.warning(
                    "Using a Hyperion server version < %s is not recommended -- "
                    "some features may be unavailable or may not function correctly. "
                    "Please consider upgrading: %s",
                    HYPERION_VERSION_WARN_CUTOFF,
                    HYPERION_RELEASES_URL,
                )

    # Client needs authentication, but no token provided? => Reauth.
    auth_resp = await hyperion_client.async_is_auth_required()
    if (
        auth_resp is not None
        and client.ResponseOK(auth_resp)
        and auth_resp.get(hyperion_const.KEY_INFO, {}).get(
            hyperion_const.KEY_REQUIRED, False
        )
        and token is None
    ):
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryAuthFailed

    # Client login doesn't work? => Reauth.
    if not await hyperion_client.async_client_login():
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryAuthFailed

    # Cannot switch instance or cannot load state? => Not ready.
    if (
        not await hyperion_client.async_client_switch_instance()
        or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo())
    ):
        await hyperion_client.async_client_disconnect()
        raise ConfigEntryNotReady

    # We need 1 root client (to manage instances being removed/added) and then 1 client
    # per Hyperion server instance which is shared for all entities associated with
    # that instance.
    opp.data[DOMAIN][config_entry.entry_id] = {
        CONF_ROOT_CLIENT: hyperion_client,
        CONF_INSTANCE_CLIENTS: {},
        CONF_ON_UNLOAD: [],
    }

    async def async_instances_to_clients(response: dict[str, Any]) -> None:
        """Convert instances to Hyperion clients."""
        if not response or hyperion_const.KEY_DATA not in response:
            return
        await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA])

    async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None:
        """Convert instances to Hyperion clients."""
        device_registry = dr.async_get(opp)
        running_instances: set[int] = set()
        stopped_instances: set[int] = set()
        existing_instances = opp.data[DOMAIN][config_entry.entry_id][
            CONF_INSTANCE_CLIENTS
        ]
        server_id = cast(str, config_entry.unique_id)

        # In practice, an instance can be in 3 states as seen by this function:
        #
        #    * Exists, and is running: Should be present in HASS/registry.
        #    * Exists, but is not running: Cannot add it yet, but entity may have be
        #      registered from a previous time it was running.
        #    * No longer exists at all: Should not be present in HASS/registry.

        # Add instances that are missing.
        for instance in instances:
            instance_num = instance.get(hyperion_const.KEY_INSTANCE)
            if instance_num is None:
                continue
            if not instance.get(hyperion_const.KEY_RUNNING, False):
                stopped_instances.add(instance_num)
                continue
            running_instances.add(instance_num)
            if instance_num in existing_instances:
                continue
            hyperion_client = await async_create_connect_hyperion_client(
                host, port, instance=instance_num, token=token
            )
            if not hyperion_client:
                continue
            existing_instances[instance_num] = hyperion_client
            instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME)
            async_dispatcher_send(
                opp,
                SIGNAL_INSTANCE_ADD.format(config_entry.entry_id),
                instance_num,
                instance_name,
            )

        # Remove entities that are are not running instances on Hyperion.
        for instance_num in set(existing_instances) - running_instances:
            del existing_instances[instance_num]
            async_dispatcher_send(
                opp, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num
            )

        # Ensure every device associated with this config entry is still in the list of
        # motionEye cameras, otherwise remove the device (and thus entities).
        known_devices = {
            get_hyperion_device_id(server_id, instance_num)
            for instance_num in running_instances | stopped_instances
        }
        for device_entry in dr.async_entries_for_config_entry(
            device_registry, config_entry.entry_id
        ):
            for (kind, key) in device_entry.identifiers:
                if kind == DOMAIN and key in known_devices:
                    break
            else:
                device_registry.async_remove_device(device_entry.id)

    hyperion_client.set_callbacks(
        {
            f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": async_instances_to_clients,
        }
    )

    async def setup_then_listen() -> None:
        await asyncio.gather(
            *[
                opp.config_entries.async_forward_entry_setup(config_entry, platform)
                for platform in PLATFORMS
            ]
        )
        assert hyperion_client
        if hyperion_client.instances is not None:
            await async_instances_to_clients_raw(hyperion_client.instances)
        opp.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
            config_entry.add_update_listener(_async_entry_updated)
        )

    opp.async_create_task(setup_then_listen())
    return True
Example #15
0
async def async_setup_entry(opp: OpenPeerPower, entry: ConfigEntry) -> bool:
    """Set up an Meteo-France account from a config entry."""
    opp.data.setdefault(DOMAIN, {})

    latitude = entry.data.get(CONF_LATITUDE)

    client = MeteoFranceClient()
    # Migrate from previous config
    if not latitude:
        places = await opp.async_add_executor_job(client.search_places,
                                                  entry.data[CONF_CITY])
        opp.config_entries.async_update_entry(
            entry,
            title=f"{places[0]}",
            data={
                CONF_LATITUDE: places[0].latitude,
                CONF_LONGITUDE: places[0].longitude,
            },
        )

    latitude = entry.data[CONF_LATITUDE]
    longitude = entry.data[CONF_LONGITUDE]

    async def _async_update_data_forecast_forecast():
        """Fetch data from API endpoint."""
        return await opp.async_add_executor_job(client.get_forecast, latitude,
                                                longitude)

    async def _async_update_data_rain():
        """Fetch data from API endpoint."""
        return await opp.async_add_executor_job(client.get_rain, latitude,
                                                longitude)

    async def _async_update_data_alert():
        """Fetch data from API endpoint."""
        return await opp.async_add_executor_job(
            client.get_warning_current_phenomenoms, department, 0, True)

    coordinator_forecast = DataUpdateCoordinator(
        opp,
        _LOGGER,
        name=f"Météo-France forecast for city {entry.title}",
        update_method=_async_update_data_forecast_forecast,
        update_interval=SCAN_INTERVAL,
    )
    coordinator_rain = None
    coordinator_alert = None

    # Fetch initial data so we have data when entities subscribe
    await coordinator_forecast.async_refresh()

    if not coordinator_forecast.last_update_success:
        raise ConfigEntryNotReady

    # Check if rain forecast is available.
    if coordinator_forecast.data.position.get("rain_product_available") == 1:
        coordinator_rain = DataUpdateCoordinator(
            opp,
            _LOGGER,
            name=f"Météo-France rain for city {entry.title}",
            update_method=_async_update_data_rain,
            update_interval=SCAN_INTERVAL_RAIN,
        )
        await coordinator_rain.async_refresh()

        if not coordinator_rain.last_update_success:
            raise ConfigEntryNotReady
    else:
        _LOGGER.warning(
            "1 hour rain forecast not available. %s is not in covered zone",
            entry.title,
        )

    department = coordinator_forecast.data.position.get("dept")
    _LOGGER.debug(
        "Department corresponding to %s is %s",
        entry.title,
        department,
    )
    if is_valid_warning_department(department):
        if not opp.data[DOMAIN].get(department):
            coordinator_alert = DataUpdateCoordinator(
                opp,
                _LOGGER,
                name=f"Météo-France alert for department {department}",
                update_method=_async_update_data_alert,
                update_interval=SCAN_INTERVAL,
            )

            await coordinator_alert.async_refresh()

            if not coordinator_alert.last_update_success:
                raise ConfigEntryNotReady

            opp.data[DOMAIN][department] = True
        else:
            _LOGGER.warning(
                "Weather alert for department %s won't be added with city %s, as it has already been added within another city",
                department,
                entry.title,
            )
    else:
        _LOGGER.warning(
            "Weather alert not available: The city %s is not in metropolitan France or Andorre",
            entry.title,
        )

    undo_listener = entry.add_update_listener(_async_update_listener)

    opp.data[DOMAIN][entry.entry_id] = {
        COORDINATOR_FORECAST: coordinator_forecast,
        COORDINATOR_RAIN: coordinator_rain,
        COORDINATOR_ALERT: coordinator_alert,
        UNDO_UPDATE_LISTENER: undo_listener,
    }

    opp.config_entries.async_setup_platforms(entry, PLATFORMS)

    return True
Example #16
0
async def setup_smartapp_endpoint(opp: OpenPeerPower):
    """
    Configure the SmartApp webhook in opp.

    SmartApps are an extension point within the SmartThings ecosystem and
    is used to receive push updates (i.e. device updates) from the cloud.
    """
    data = opp.data.get(DOMAIN)
    if data:
        # already setup
        return

    # Get/create config to store a unique id for this opp.instance.
    store = opp.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
    config = await store.async_load()
    if not config:
        # Create config
        config = {
            CONF_INSTANCE_ID: str(uuid4()),
            CONF_WEBHOOK_ID: secrets.token_hex(),
            CONF_CLOUDHOOK_URL: None,
        }
        await store.async_save(config)

    # Register webhook
    webhook.async_register(opp, DOMAIN, "SmartApp", config[CONF_WEBHOOK_ID],
                           smartapp_webhook)

    # Create webhook if eligible
    cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
    if (cloudhook_url is None
            and opp.components.cloud.async_active_subscription()
            and not opp.config_entries.async_entries(DOMAIN)):
        cloudhook_url = await opp.components.cloud.async_create_cloudhook(
            config[CONF_WEBHOOK_ID])
        config[CONF_CLOUDHOOK_URL] = cloudhook_url
        await store.async_save(config)
        _LOGGER.debug("Created cloudhook '%s'", cloudhook_url)

    # SmartAppManager uses a dispatcher to invoke callbacks when push events
    # occur. Use opp. implementation instead of the built-in one.
    dispatcher = Dispatcher(
        signal_prefix=SIGNAL_SMARTAPP_PREFIX,
        connect=functools.partial(async_dispatcher_connect, opp),
        send=functools.partial(async_dispatcher_send, opp),
    )
    # Path is used in digital signature validation
    path = (urlparse(cloudhook_url).path if cloudhook_url else
            webhook.async_generate_path(config[CONF_WEBHOOK_ID]))
    manager = SmartAppManager(path, dispatcher=dispatcher)
    manager.connect_install(functools.partial(smartapp_install, opp))
    manager.connect_update(functools.partial(smartapp_update, opp))
    manager.connect_uninstall(functools.partial(smartapp_uninstall, opp))

    opp.data[DOMAIN] = {
        DATA_MANAGER: manager,
        CONF_INSTANCE_ID: config[CONF_INSTANCE_ID],
        DATA_BROKERS: {},
        CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
        # Will not be present if not enabled
        CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
    }
    _LOGGER.debug(
        "Setup endpoint for %s",
        cloudhook_url if cloudhook_url else webhook.async_generate_url(
            opp, config[CONF_WEBHOOK_ID]),
    )
Example #17
0
async def async_setup_entry(opp: core.OpenPeerPower,
                            entry: config_entries.ConfigEntry):
    """Set up the motion_blinds components from a config entry."""
    opp.data.setdefault(DOMAIN, {})
    host = entry.data[CONF_HOST]
    key = entry.data[CONF_API_KEY]

    # Create multicast Listener
    if KEY_MULTICAST_LISTENER not in opp.data[DOMAIN]:
        multicast = MotionMulticast()
        opp.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast
        # start listening for local pushes (only once)
        await opp.async_add_executor_job(multicast.Start_listen)

        # register stop callback to shutdown listening for local pushes
        def stop_motion_multicast(event):
            """Stop multicast thread."""
            _LOGGER.debug("Shutting down Motion Listener")
            multicast.Stop_listen()

        opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP,
                                  stop_motion_multicast)

    # Connect to motion gateway
    multicast = opp.data[DOMAIN][KEY_MULTICAST_LISTENER]
    connect_gateway_class = ConnectMotionGateway(opp, multicast)
    if not await connect_gateway_class.async_connect_gateway(host, key):
        raise ConfigEntryNotReady
    motion_gateway = connect_gateway_class.gateway_device

    coordinator = DataUpdateCoordinatorMotionBlinds(
        opp,
        _LOGGER,
        motion_gateway,
        # Name of the data. For logging purposes.
        name=entry.title,
        # Polling interval. Will only be polled if there are subscribers.
        update_interval=timedelta(seconds=UPDATE_INTERVAL),
    )

    # Fetch initial data so we have data when entities subscribe
    await coordinator.async_config_entry_first_refresh()

    opp.data[DOMAIN][entry.entry_id] = {
        KEY_GATEWAY: motion_gateway,
        KEY_COORDINATOR: coordinator,
    }

    device_registry = await dr.async_get_registry(opp)
    device_registry.async_get_or_create(
        config_entry_id=entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)},
        identifiers={(DOMAIN, entry.unique_id)},
        manufacturer=MANUFACTURER,
        name=entry.title,
        model="Wi-Fi bridge",
        sw_version=motion_gateway.protocol,
    )

    opp.config_entries.async_setup_platforms(entry, PLATFORMS)

    return True
Example #18
0
async def async_setup_gateway_entry(opp: core.OpenPeerPower,
                                    entry: config_entries.ConfigEntry):
    """Set up the Xiaomi Gateway component from a config entry."""
    host = entry.data[CONF_HOST]
    token = entry.data[CONF_TOKEN]
    name = entry.title
    gateway_id = entry.unique_id

    # For backwards compat
    if entry.unique_id.endswith("-gateway"):
        opp.config_entries.async_update_entry(entry,
                                              unique_id=entry.data["mac"])

    # Connect to gateway
    gateway = ConnectXiaomiGateway(opp)
    if not await gateway.async_connect_gateway(host, token):
        return False
    gateway_info = gateway.gateway_info

    gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}"

    device_registry = await dr.async_get_registry(opp)
    device_registry.async_get_or_create(
        config_entry_id=entry.entry_id,
        connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)},
        identifiers={(DOMAIN, gateway_id)},
        manufacturer="Xiaomi",
        name=name,
        model=gateway_model,
        sw_version=gateway_info.firmware_version,
    )

    def update_data():
        """Fetch data from the subdevice."""
        data = {}
        for sub_device in gateway.gateway_device.devices.values():
            try:
                sub_device.update()
            except GatewayException as ex:
                _LOGGER.error("Got exception while fetching the state: %s", ex)
                data[sub_device.sid] = {ATTR_AVAILABLE: False}
            else:
                data[sub_device.sid] = {ATTR_AVAILABLE: True}
        return data

    async def async_update_data():
        """Fetch data from the subdevice using async_add_executor_job."""
        return await opp.async_add_executor_job(update_data)

    # Create update coordinator
    coordinator = DataUpdateCoordinator(
        opp,
        _LOGGER,
        # Name of the data. For logging purposes.
        name=name,
        update_method=async_update_data,
        # Polling interval. Will only be polled if there are subscribers.
        update_interval=timedelta(seconds=10),
    )

    opp.data[DOMAIN][entry.entry_id] = {
        CONF_GATEWAY: gateway.gateway_device,
        KEY_COORDINATOR: coordinator,
    }

    for platform in GATEWAY_PLATFORMS:
        opp.async_create_task(
            opp.config_entries.async_forward_entry_setup(entry, platform))

    return True
Example #19
0
async def async_setup(opp: OpenPeerPower, config: dict) -> bool:
    """Set up the Sure Petcare integration."""
    conf = config[DOMAIN]
    opp.data.setdefault(DOMAIN, {})

    try:
        surepy = Surepy(
            conf[CONF_USERNAME],
            conf[CONF_PASSWORD],
            auth_token=None,
            api_timeout=SURE_API_TIMEOUT,
            session=async_get_clientsession(opp),
        )
    except SurePetcareAuthenticationError:
        _LOGGER.error(
            "Unable to connect to surepetcare.io: Wrong credentials!")
        return False
    except SurePetcareError as error:
        _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error)
        return False

    spc = SurePetcareAPI(opp, surepy)
    opp.data[DOMAIN][SPC] = spc

    await spc.async_update()

    async_track_time_interval(opp, spc.async_update, SCAN_INTERVAL)

    # load platforms
    opp.async_create_task(
        opp.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {},
                                                  config))
    opp.async_create_task(
        opp.helpers.discovery.async_load_platform("sensor", DOMAIN, {},
                                                  config))

    async def handle_set_lock_state(call):
        """Call when setting the lock state."""
        await spc.set_lock_state(call.data[ATTR_FLAP_ID],
                                 call.data[ATTR_LOCK_STATE])
        await spc.async_update()

    lock_state_service_schema = vol.Schema({
        vol.Required(ATTR_FLAP_ID):
        vol.All(cv.positive_int, vol.In(spc.states.keys())),
        vol.Required(ATTR_LOCK_STATE):
        vol.All(
            cv.string,
            vol.Lower,
            vol.In([
                # https://github.com/PyCQA/pylint/issues/2062
                # pylint: disable=no-member
                LockState.UNLOCKED.name.lower(),
                LockState.LOCKED_IN.name.lower(),
                LockState.LOCKED_OUT.name.lower(),
                LockState.LOCKED_ALL.name.lower(),
            ]),
        ),
    })

    opp.services.async_register(
        DOMAIN,
        SERVICE_SET_LOCK_STATE,
        handle_set_lock_state,
        schema=lock_state_service_schema,
    )

    return True
Example #20
0
async def async_load(opp: OpenPeerPower) -> None:
    """Load entity registry."""
    assert DATA_REGISTRY not in opp.data
    opp.data[DATA_REGISTRY] = EntityRegistry(opp)
    await opp.data[DATA_REGISTRY].async_load()
def async_enable_logging(
    opp: core.OpenPeerPower,
    verbose: bool = False,
    log_rotate_days: Optional[int] = None,
    log_file: Optional[str] = None,
    log_no_color: bool = False,
) -> None:
    """Set up the logging.

    This method must be run in the event loop.
    """
    fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
    datefmt = "%Y-%m-%d %H:%M:%S"

    if not log_no_color:
        try:
            from colorlog import ColoredFormatter

            # basicConfig must be called after importing colorlog in order to
            # ensure that the handlers it sets up wraps the correct streams.
            logging.basicConfig(level=logging.INFO)

            colorfmt = f"%(log_color)s{fmt}%(reset)s"
            logging.getLogger().handlers[0].setFormatter(
                ColoredFormatter(
                    colorfmt,
                    datefmt=datefmt,
                    reset=True,
                    log_colors={
                        "DEBUG": "cyan",
                        "INFO": "green",
                        "WARNING": "yellow",
                        "ERROR": "red",
                        "CRITICAL": "red",
                    },
                )
            )
        except ImportError:
            pass

    # If the above initialization failed for any reason, setup the default
    # formatting.  If the above succeeds, this will result in a no-op.
    logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)

    # Suppress overly verbose logs from libraries that aren't helpful
    logging.getLogger("requests").setLevel(logging.WARNING)
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("aiohttp.access").setLevel(logging.WARNING)

    # Log errors to a file if we have write access to file or config dir
    if log_file is None:
        err_log_path = opp.config.path(ERROR_LOG_FILENAME)
    else:
        err_log_path = os.path.abspath(log_file)

    err_path_exists = os.path.isfile(err_log_path)
    err_dir = os.path.dirname(err_log_path)

    # Check if we can write to the error log if it exists or that
    # we can create files in the containing directory if not.
    if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
        not err_path_exists and os.access(err_dir, os.W_OK)
    ):

        if log_rotate_days:
            err_handler: logging.FileHandler = (
                logging.handlers.TimedRotatingFileHandler(
                    err_log_path, when="midnight", backupCount=log_rotate_days
                )
            )
        else:
            err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)

        err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
        err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))

        async_handler = AsyncHandler(opp.loop, err_handler)

        async def async_stop_async_handler(_: Any) -> None:
            """Cleanup async handler."""
            logging.getLogger("").removeHandler(async_handler)  # type: ignore
            await async_handler.async_close(blocking=True)

        opp.bus.async_listen_once(EVENT_OPENPEERPOWER_CLOSE, async_stop_async_handler)

        logger = logging.getLogger("")
        logger.addHandler(async_handler)  # type: ignore
        logger.setLevel(logging.INFO)

        # Save the log file location for access by other components.
        opp.data[DATA_LOGGING] = err_log_path
    else:
        _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
Example #22
0
async def async_setup(opp: OpenPeerPower, config: ConfigType):
    """Set up the Lovelace commands."""
    mode = config[DOMAIN][CONF_MODE]
    yaml_resources = config[DOMAIN].get(CONF_RESOURCES)

    frontend.async_register_built_in_panel(opp, DOMAIN, config={"mode": mode})

    async def reload_resources_service_handler(service_call: ServiceCall) -> None:
        """Reload yaml resources."""
        try:
            conf = await async_opp_config_yaml(opp)
        except OpenPeerPowerError as err:
            _LOGGER.error(err)
            return

        integration = await async_get_integration(opp, DOMAIN)

        config = await async_process_component_config(opp, conf, integration)

        resource_collection = await create_yaml_resource_col(
            opp, config[DOMAIN].get(CONF_RESOURCES)
        )
        opp.data[DOMAIN]["resources"] = resource_collection

    if mode == MODE_YAML:
        default_config = dashboard.LovelaceYAML(opp, None, None)
        resource_collection = await create_yaml_resource_col(opp, yaml_resources)

        async_register_admin_service(
            opp,
            DOMAIN,
            SERVICE_RELOAD_RESOURCES,
            reload_resources_service_handler,
            schema=RESOURCE_RELOAD_SERVICE_SCHEMA,
        )

    else:
        default_config = dashboard.LovelaceStorage(opp, None)

        if yaml_resources is not None:
            _LOGGER.warning(
                "Lovelace is running in storage mode. Define resources via user interface"
            )

        resource_collection = resources.ResourceStorageCollection(opp, default_config)

        collection.StorageCollectionWebsocket(
            resource_collection,
            "lovelace/resources",
            "resource",
            RESOURCE_CREATE_FIELDS,
            RESOURCE_UPDATE_FIELDS,
        ).async_setup(opp, create_list=False)

    opp.components.websocket_api.async_register_command(
        websocket.websocket_lovelace_config
    )
    opp.components.websocket_api.async_register_command(
        websocket.websocket_lovelace_save_config
    )
    opp.components.websocket_api.async_register_command(
        websocket.websocket_lovelace_delete_config
    )
    opp.components.websocket_api.async_register_command(
        websocket.websocket_lovelace_resources
    )

    opp.components.websocket_api.async_register_command(
        websocket.websocket_lovelace_dashboards
    )

    opp.data[DOMAIN] = {
        # We store a dictionary mapping url_path: config. None is the default.
        "dashboards": {None: default_config},
        "resources": resource_collection,
        "yaml_dashboards": config[DOMAIN].get(CONF_DASHBOARDS, {}),
    }

    if opp.config.safe_mode:
        return True

    async def storage_dashboard_changed(change_type, item_id, item):
        """Handle a storage dashboard change."""
        url_path = item[CONF_URL_PATH]

        if change_type == collection.CHANGE_REMOVED:
            frontend.async_remove_panel(opp, url_path)
            await opp.data[DOMAIN]["dashboards"].pop(url_path).async_delete()
            return

        if change_type == collection.CHANGE_ADDED:

            existing = opp.data[DOMAIN]["dashboards"].get(url_path)

            if existing:
                _LOGGER.warning(
                    "Cannot register panel at %s, it is already defined in %s",
                    url_path,
                    existing,
                )
                return

            opp.data[DOMAIN]["dashboards"][url_path] = dashboard.LovelaceStorage(
                opp, item
            )

            update = False
        else:
            opp.data[DOMAIN]["dashboards"][url_path].config = item
            update = True

        try:
            _register_panel(opp, url_path, MODE_STORAGE, item, update)
        except ValueError:
            _LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path)

    # Process YAML dashboards
    for url_path, dashboard_conf in opp.data[DOMAIN]["yaml_dashboards"].items():
        # For now always mode=yaml
        config = dashboard.LovelaceYAML(opp, url_path, dashboard_conf)
        opp.data[DOMAIN]["dashboards"][url_path] = config

        try:
            _register_panel(opp, url_path, MODE_YAML, dashboard_conf, False)
        except ValueError:
            _LOGGER.warning("Panel url path %s is not unique", url_path)

    # Process storage dashboards
    dashboards_collection = dashboard.DashboardsCollection(opp)

    dashboards_collection.async_add_listener(storage_dashboard_changed)
    await dashboards_collection.async_load()

    collection.StorageCollectionWebsocket(
        dashboards_collection,
        "lovelace/dashboards",
        "dashboard",
        STORAGE_DASHBOARD_CREATE_FIELDS,
        STORAGE_DASHBOARD_UPDATE_FIELDS,
    ).async_setup(opp, create_list=False)

    return True
Example #23
0
def async_track_entity_registry_updated_event(
    opp: OpenPeerPower,
    entity_ids: str | Iterable[str],
    action: Callable[[Event], Any],
) -> Callable[[], None]:
    """Track specific entity registry updated events indexed by entity_id.

    Similar to async_track_state_change_event.
    """
    entity_ids = _async_string_to_lower_list(entity_ids)
    if not entity_ids:
        return _remove_empty_listener

    entity_callbacks = opp.data.setdefault(TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {})

    if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in opp.data:

        @callback
        def _async_entity_registry_updated_filter(event: Event) -> bool:
            """Filter entity registry updates by entity_id."""
            entity_id = event.data.get("old_entity_id", event.data["entity_id"])
            return entity_id in entity_callbacks

        @callback
        def _async_entity_registry_updated_dispatcher(event: Event) -> None:
            """Dispatch entity registry updates by entity_id."""
            entity_id = event.data.get("old_entity_id", event.data["entity_id"])

            if entity_id not in entity_callbacks:
                return

            for job in entity_callbacks[entity_id][:]:
                try:
                    opp.async_run_opp_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing entity registry update for %s",
                        entity_id,
                    )

        opp.data[TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = opp.bus.async_listen(
            EVENT_ENTITY_REGISTRY_UPDATED,
            _async_entity_registry_updated_dispatcher,
            event_filter=_async_entity_registry_updated_filter,
        )

    job = OppJob(action)

    for entity_id in entity_ids:
        entity_callbacks.setdefault(entity_id, []).append(job)

    @callback
    def remove_listener() -> None:
        """Remove state change listener."""
        _async_remove_indexed_listeners(
            opp,
            TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS,
            TRACK_ENTITY_REGISTRY_UPDATED_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener
Example #24
0
def _async_setup_shared_data(opp: OpenPeerPower):
    """Create shared data for platform config and rest coordinators."""
    opp.data[DOMAIN] = {
        key: []
        for key in [REST_DATA, *COORDINATOR_AWARE_PLATFORMS]
    }
Example #25
0
async def async_start(opp: OpenPeerPower, discovery_topic, config_entry,
                      tasmota_mqtt, setup_device) -> bool:
    """Start Tasmota device discovery."""
    async def _discover_entity(tasmota_entity_config, discovery_hash,
                               platform):
        """Handle adding or updating a discovered entity."""
        if not tasmota_entity_config:
            # Entity disabled, clean up entity registry
            entity_registry = await opp.helpers.entity_registry.async_get_registry(
            )
            unique_id = unique_id_from_hash(discovery_hash)
            entity_id = entity_registry.async_get_entity_id(
                platform, DOMAIN, unique_id)
            if entity_id:
                _LOGGER.debug("Removing entity: %s %s", platform,
                              discovery_hash)
                entity_registry.async_remove(entity_id)
            return

        if discovery_hash in opp.data[ALREADY_DISCOVERED]:
            _LOGGER.debug(
                "Entity already added, sending update: %s %s",
                platform,
                discovery_hash,
            )
            async_dispatcher_send(
                opp,
                TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
                tasmota_entity_config,
            )
        else:
            tasmota_entity = tasmota_get_entity(tasmota_entity_config,
                                                tasmota_mqtt)
            _LOGGER.debug(
                "Adding new entity: %s %s %s",
                platform,
                discovery_hash,
                tasmota_entity.unique_id,
            )

            opp.data[ALREADY_DISCOVERED][discovery_hash] = None

            async_dispatcher_send(
                opp,
                TASMOTA_DISCOVERY_ENTITY_NEW.format(platform),
                tasmota_entity,
                discovery_hash,
            )

    async def async_device_discovered(payload, mac):
        """Process the received message."""

        if ALREADY_DISCOVERED not in opp.data:
            # Discovery is shutting down
            return

        _LOGGER.debug("Received discovery data for tasmota device: %s", mac)
        tasmota_device_config = tasmota_get_device_config(payload)
        setup_device(tasmota_device_config, mac)

        if not payload:
            return

        tasmota_triggers = tasmota_get_triggers(payload)
        for trigger_config in tasmota_triggers:
            discovery_hash = (mac, "automation", "trigger",
                              trigger_config.trigger_id)
            if discovery_hash in opp.data[ALREADY_DISCOVERED]:
                _LOGGER.debug(
                    "Trigger already added, sending update: %s",
                    discovery_hash,
                )
                async_dispatcher_send(
                    opp,
                    TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
                    trigger_config,
                )
            elif trigger_config.is_active:
                _LOGGER.debug("Adding new trigger: %s", discovery_hash)
                opp.data[ALREADY_DISCOVERED][discovery_hash] = None

                tasmota_trigger = tasmota_get_trigger(trigger_config,
                                                      tasmota_mqtt)

                async_dispatcher_send(
                    opp,
                    TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"),
                    tasmota_trigger,
                    discovery_hash,
                )

        for platform in PLATFORMS:
            tasmota_entities = tasmota_get_entities_for_platform(
                payload, platform)
            for (tasmota_entity_config, discovery_hash) in tasmota_entities:
                await _discover_entity(tasmota_entity_config, discovery_hash,
                                       platform)

    async def async_sensors_discovered(sensors, mac):
        """Handle discovery of (additional) sensors."""
        platform = sensor.DOMAIN

        device_registry = await opp.helpers.device_registry.async_get_registry(
        )
        entity_registry = await opp.helpers.entity_registry.async_get_registry(
        )
        device = device_registry.async_get_device(
            set(), {(dev_reg.CONNECTION_NETWORK_MAC, mac)})

        if device is None:
            _LOGGER.warning("Got sensors for unknown device mac: %s", mac)
            return

        orphaned_entities = {
            entry.unique_id
            for entry in async_entries_for_device(
                entity_registry, device.id, include_disabled_entities=True)
            if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN
        }
        for (tasmota_sensor_config, discovery_hash) in sensors:
            if tasmota_sensor_config:
                orphaned_entities.discard(tasmota_sensor_config.unique_id)
            await _discover_entity(tasmota_sensor_config, discovery_hash,
                                   platform)
        for unique_id in orphaned_entities:
            entity_id = entity_registry.async_get_entity_id(
                platform, DOMAIN, unique_id)
            if entity_id:
                _LOGGER.debug("Removing entity: %s %s", platform, entity_id)
                entity_registry.async_remove(entity_id)

    opp.data[ALREADY_DISCOVERED] = {}

    tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt)
    await tasmota_discovery.start_discovery(async_device_discovered,
                                            async_sensors_discovered)
    opp.data[TASMOTA_DISCOVERY_INSTANCE] = tasmota_discovery
Example #26
0
async def async_setup(opp: OpenPeerPower, config: dict) -> bool:
    """Set up the persistent notification component."""
    persistent_notifications: MutableMapping[str,
                                             MutableMapping] = OrderedDict()
    opp.data[DOMAIN] = {"notifications": persistent_notifications}

    @callback
    def create_service(call):
        """Handle a create notification service call."""
        title = call.data.get(ATTR_TITLE)
        message = call.data.get(ATTR_MESSAGE)
        notification_id = call.data.get(ATTR_NOTIFICATION_ID)

        if notification_id is not None:
            entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
        else:
            entity_id = async_generate_entity_id(ENTITY_ID_FORMAT,
                                                 DEFAULT_OBJECT_ID,
                                                 opp=opp)
            notification_id = entity_id.split(".")[1]

        attr = {}
        if title is not None:
            if isinstance(title, Template):
                try:
                    title.opp = opp
                    title = title.async_render(parse_result=False)
                except TemplateError as ex:
                    _LOGGER.error("Error rendering title %s: %s", title, ex)
                    title = title.template

            attr[ATTR_TITLE] = title
            attr[ATTR_FRIENDLY_NAME] = title

        if isinstance(message, Template):
            try:
                message.opp = opp
                message = message.async_render(parse_result=False)
            except TemplateError as ex:
                _LOGGER.error("Error rendering message %s: %s", message, ex)
                message = message.template

        attr[ATTR_MESSAGE] = message

        opp.states.async_set(entity_id, STATE, attr)

        # Store notification and fire event
        # This will eventually replace state machine storage
        persistent_notifications[entity_id] = {
            ATTR_MESSAGE: message,
            ATTR_NOTIFICATION_ID: notification_id,
            ATTR_STATUS: STATUS_UNREAD,
            ATTR_TITLE: title,
            ATTR_CREATED_AT: dt_util.utcnow(),
        }

        opp.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)

    @callback
    def dismiss_service(call):
        """Handle the dismiss notification service call."""
        notification_id = call.data.get(ATTR_NOTIFICATION_ID)
        entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))

        if entity_id not in persistent_notifications:
            return

        opp.states.async_remove(entity_id, call.context)

        del persistent_notifications[entity_id]
        opp.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)

    @callback
    def mark_read_service(call):
        """Handle the mark_read notification service call."""
        notification_id = call.data.get(ATTR_NOTIFICATION_ID)
        entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))

        if entity_id not in persistent_notifications:
            _LOGGER.error(
                "Marking persistent_notification read failed: "
                "Notification ID %s not found",
                notification_id,
            )
            return

        persistent_notifications[entity_id][ATTR_STATUS] = STATUS_READ
        opp.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)

    opp.services.async_register(DOMAIN, SERVICE_CREATE, create_service,
                                SCHEMA_SERVICE_CREATE)

    opp.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service,
                                SCHEMA_SERVICE_DISMISS)

    opp.services.async_register(DOMAIN, SERVICE_MARK_READ, mark_read_service,
                                SCHEMA_SERVICE_MARK_READ)

    opp.components.websocket_api.async_register_command(
        websocket_get_notifications)

    return True
Example #27
0
def publish(opp: OpenPeerPower, topic, payload, qos=None, retain=None) -> None:
    """Publish message to an MQTT topic."""
    opp.add_job(async_publish, opp, topic, payload, qos, retain)
Example #28
0
def async_dismiss(opp: OpenPeerPower, notification_id: str) -> None:
    """Remove a notification."""
    data = {ATTR_NOTIFICATION_ID: notification_id}

    opp.async_create_task(
        opp.services.async_call(DOMAIN, SERVICE_DISMISS, data))
Example #29
0
def breakpoint_clear_all(opp: OpenPeerPower) -> None:
    """Clear all breakpoints."""
    opp.data[DATA_SCRIPT_BREAKPOINTS] = {}
Example #30
0
async def async_setup_entry(opp: OpenPeerPower,
                            entry: ConfigEntry):  # noqa: C901
    """Set up ozw from a config entry."""
    opp.data.setdefault(DOMAIN, {})
    ozw_data = opp.data[DOMAIN][entry.entry_id] = {}
    ozw_data[DATA_UNSUBSCRIBE] = []

    data_nodes = {}
    opp.data[DOMAIN][NODES_VALUES] = data_values = {}
    removed_nodes = []
    manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"}

    if entry.unique_id is None:
        opp.config_entries.async_update_entry(entry, unique_id=DOMAIN)

    if entry.data.get(CONF_USE_ADDON):
        # Do not use MQTT integration. Use own MQTT client.
        # Retrieve discovery info from the OpenZWave add-on.
        discovery_info = await opp.components.oppio.async_get_addon_discovery_info(
            "core_zwave")

        if not discovery_info:
            _LOGGER.error("Failed to get add-on discovery info")
            raise ConfigEntryNotReady

        discovery_info_config = discovery_info["config"]

        host = discovery_info_config["host"]
        port = discovery_info_config["port"]
        username = discovery_info_config["username"]
        password = discovery_info_config["password"]
        mqtt_client = MQTTClient(host,
                                 port,
                                 username=username,
                                 password=password)
        manager_options["send_message"] = mqtt_client.send_message

    else:
        mqtt_entries = opp.config_entries.async_entries("mqtt")
        if not mqtt_entries or mqtt_entries[
                0].state is not ConfigEntryState.LOADED:
            _LOGGER.error("MQTT integration is not set up")
            return False

        mqtt_entry = mqtt_entries[0]  # MQTT integration only has one entry.

        @callback
        def send_message(topic, payload):
            if mqtt_entry.state is not ConfigEntryState.LOADED:
                _LOGGER.error("MQTT integration is not set up")
                return

            mqtt.async_publish(opp, topic, json.dumps(payload))

        manager_options["send_message"] = send_message

    options = OZWOptions(**manager_options)
    manager = OZWManager(options)

    opp.data[DOMAIN][MANAGER] = manager

    @callback
    def async_node_added(node):
        # Caution: This is also called on (re)start.
        _LOGGER.debug("[NODE ADDED] node_id: %s", node.id)
        data_nodes[node.id] = node
        if node.id not in data_values:
            data_values[node.id] = []

    @callback
    def async_node_changed(node):
        _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id)
        data_nodes[node.id] = node
        # notify devices about the node change
        if node.id not in removed_nodes:
            opp.async_create_task(async_handle_node_update(opp, node))

    @callback
    def async_node_removed(node):
        _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id)
        data_nodes.pop(node.id)
        # node added/removed events also happen on (re)starts of opp/mqtt/ozw
        # cleanup device/entity registry if we know this node is permanently deleted
        # entities itself are removed by the values logic
        if node.id in removed_nodes:
            opp.async_create_task(async_handle_remove_node(opp, node))
            removed_nodes.remove(node.id)

    @callback
    def async_instance_event(message):
        event = message["event"]
        event_data = message["data"]
        _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data)
        # The actual removal action of a Z-Wave node is reported as instance event
        # Only when this event is detected we cleanup the device and entities from opp
        # Note: Find a more elegant way of doing this, e.g. a notification of this event from OZW
        if event in ["removenode", "removefailednode"
                     ] and "Node" in event_data:
            removed_nodes.append(event_data["Node"])

    @callback
    def async_value_added(value):
        node = value.node
        # Clean up node.node_id and node.id use. They are the same.
        node_id = value.node.node_id

        # Filter out CommandClasses we're definitely not interested in.
        if value.command_class in [
                CommandClass.MANUFACTURER_SPECIFIC,
        ]:
            return

        _LOGGER.debug(
            "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s",
            value.node.id,
            value.label,
            value.value,
            value.value_id_key,
            value.command_class,
        )

        node_data_values = data_values[node_id]

        # Check if this value should be tracked by an existing entity
        value_unique_id = create_value_id(value)
        for values in node_data_values:
            values.async_check_value(value)
            if values.values_id == value_unique_id:
                return  # this value already has an entity

        # Run discovery on it and see if any entities need created
        for schema in DISCOVERY_SCHEMAS:
            if not check_node_schema(node, schema):
                continue
            if not check_value_schema(
                    value, schema[const.DISC_VALUES][const.DISC_PRIMARY]):
                continue

            values = ZWaveDeviceEntityValues(opp, options, schema, value)
            values.async_setup()

            # This is legacy and can be cleaned up since we are in the main thread:
            # We create a new list and update the reference here so that
            # the list can be safely iterated over in the main thread
            data_values[node_id] = node_data_values + [values]

    @callback
    def async_value_changed(value):
        # if an entity belonging to this value needs updating,
        # it's handled within the entity logic
        _LOGGER.debug(
            "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s",
            value.node.id,
            value.label,
            value.value,
            value.value_id_key,
            value.command_class,
        )
        # Handle a scene activation message
        if value.command_class in [
                CommandClass.SCENE_ACTIVATION,
                CommandClass.CENTRAL_SCENE,
        ]:
            async_handle_scene_activated(opp, value)
            return

    @callback
    def async_value_removed(value):
        _LOGGER.debug(
            "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s",
            value.node.id,
            value.label,
            value.value,
            value.value_id_key,
            value.command_class,
        )
        # signal all entities using this value for removal
        value_unique_id = create_value_id(value)
        async_dispatcher_send(opp, const.SIGNAL_DELETE_ENTITY, value_unique_id)
        # remove value from our local list
        node_data_values = data_values[value.node.id]
        node_data_values[:] = [
            item for item in node_data_values
            if item.values_id != value_unique_id
        ]

    # Listen to events for node and value changes
    for event, event_callback in (
        (EVENT_NODE_ADDED, async_node_added),
        (EVENT_NODE_CHANGED, async_node_changed),
        (EVENT_NODE_REMOVED, async_node_removed),
        (EVENT_VALUE_ADDED, async_value_added),
        (EVENT_VALUE_CHANGED, async_value_changed),
        (EVENT_VALUE_REMOVED, async_value_removed),
        (EVENT_INSTANCE_EVENT, async_instance_event),
    ):
        ozw_data[DATA_UNSUBSCRIBE].append(options.listen(
            event, event_callback))

    # Register Services
    services = ZWaveServices(opp, manager)
    services.async_register()

    # Register WebSocket API
    async_register_api(opp)

    @callback
    def async_receive_message(msg):
        manager.receive_message(msg.topic, msg.payload)

    async def start_platforms():
        await asyncio.gather(*[
            opp.config_entries.async_forward_entry_setup(entry, platform)
            for platform in PLATFORMS
        ])
        if entry.data.get(CONF_USE_ADDON):
            mqtt_client_task = asyncio.create_task(
                mqtt_client.start_client(manager))

            async def async_stop_mqtt_client(event=None):
                """Stop the mqtt client.

                Do not unsubscribe the manager topic.
                """
                mqtt_client_task.cancel()
                with suppress(asyncio.CancelledError):
                    await mqtt_client_task

            ozw_data[DATA_UNSUBSCRIBE].append(
                opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP,
                                          async_stop_mqtt_client))
            ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client

        else:
            ozw_data[DATA_UNSUBSCRIBE].append(await mqtt.async_subscribe(
                opp, f"{manager.options.topic_prefix}#",
                async_receive_message))

    opp.async_create_task(start_platforms())

    return True