async def async_setup(opp: OpenPeerPower, config: ConfigType) -> bool: """Set up Hyperion component.""" opp.data[DOMAIN] = {} return True
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)
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
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
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, )
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
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
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
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
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
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, )
def async_set_agent(opp: core.OpenPeerPower, agent: AbstractConversationAgent): """Set the agent to handle the conversations.""" opp.data[DATA_AGENT] = agent
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
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
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]), )
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
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
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
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)
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
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
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] }
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
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
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)
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))
def breakpoint_clear_all(opp: OpenPeerPower) -> None: """Clear all breakpoints.""" opp.data[DATA_SCRIPT_BREAKPOINTS] = {}
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