async def async_get_tts_audio(self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. This method is a coroutine. """ provider = self.providers[engine] extension, data = await provider.async_get_tts_audio( message, language, options) if data is None or extension is None: raise OpenPeerPowerError(f"No TTS from {engine} for '{message}'") # Create file infos filename = f"{key}.{extension}".lower() # Validate filename if not _RE_VOICE_FILE.match(filename): raise OpenPeerPowerError( f"TTS filename '{filename}' from {engine} is invalid!") # Save to memory data = self.write_tags(filename, data, provider, message, language, options) self._async_store_to_memcache(key, filename, data) if cache: self.opp.async_create_task( self.async_save_tts_audio(key, filename, data)) return filename
async def async_handle_core_service(call): """Service handler for handling core services.""" if (call.service in SHUTDOWN_SERVICES and await recorder.async_migration_in_progress(opp)): _LOGGER.error( "The system cannot %s while a database upgrade is in progress", call.service, ) raise OpenPeerPowerError( f"The system cannot {call.service} " "while a database upgrade is in progress.") if call.service == SERVICE_OPENPEERPOWER_STOP: await oppio.stop_openpeerpower() return errors = await conf_util.async_check_op_config_file(opp) if errors: _LOGGER.error( "The system cannot %s because the configuration is not valid: %s", call.service, errors, ) opp.components.persistent_notification.async_create( "Config error. See [the logs](/config/logs) for details.", "Config validating", f"{OPP_DOMAIN}.check_config", ) raise OpenPeerPowerError( f"The system cannot {call.service} " f"because the configuration is not valid: {errors}") if call.service == SERVICE_OPENPEERPOWER_RESTART: await oppio.restart_openpeerpower()
async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" # Check for file access if not self.opp.config.is_allowed_path(video_path): raise OpenPeerPowerError( f"Can't write {video_path}, no access to path!") # Add recorder recorder = self.outputs().get("recorder") if recorder: raise OpenPeerPowerError( f"Stream already recording to {recorder.video_path}!") recorder = self.add_provider("recorder", timeout=duration) recorder.video_path = video_path self.start() _LOGGER.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback hls = self.outputs().get("hls") if lookback > 0 and hls: num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments:])
def lookup_plex_media(opp, content_type, content_id): """Look up Plex media for other integrations using media_player.play_media service payloads.""" content = json.loads(content_id) if isinstance(content, int): content = {"plex_key": content} content_type = DOMAIN plex_server_name = content.pop("plex_server", None) plex_server = get_plex_server(opp, plex_server_name) playqueue_id = content.pop("playqueue_id", None) if playqueue_id: try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: raise OpenPeerPowerError( f"PlayQueue '{playqueue_id}' could not be found") from err else: shuffle = content.pop("shuffle", 0) media = plex_server.lookup_media(content_type, **content) if media is None: raise OpenPeerPowerError( f"Plex media not found using payload: '{content_id}'") playqueue = plex_server.create_playqueue(media, shuffle=shuffle) return (playqueue, plex_server)
def get_plex_server(opp, plex_server_name=None): """Retrieve a configured Plex server by name.""" if DOMAIN not in opp.data: raise OpenPeerPowerError("Plex integration not configured") plex_servers = opp.data[DOMAIN][SERVERS].values() if not plex_servers: raise OpenPeerPowerError("No Plex servers available") if plex_server_name: plex_server = next( (x for x in plex_servers if x.friendly_name == plex_server_name), None) if plex_server is not None: return plex_server friendly_names = [x.friendly_name for x in plex_servers] raise OpenPeerPowerError( f"Requested Plex server '{plex_server_name}' not found in {friendly_names}" ) if len(plex_servers) == 1: return next(iter(plex_servers)) friendly_names = [x.friendly_name for x in plex_servers] raise OpenPeerPowerError( f"Multiple Plex servers configured, choose with 'plex_server' key: {friendly_names}" )
async def async_get_url_path(self, engine, message, cache=None, language=None, options=None): """Get URL for play message. This method is a coroutine. """ provider = self.providers[engine] msg_hash = hashlib.sha1(bytes(message, "utf-8")).hexdigest() use_cache = cache if cache is not None else self.use_cache # Languages language = language or provider.default_language if language is None or language not in provider.supported_languages: raise OpenPeerPowerError(f"Not supported language {language}") # Options if provider.default_options and options: merged_options = provider.default_options.copy() merged_options.update(options) options = merged_options options = options or provider.default_options if options is not None: invalid_opts = [ opt_name for opt_name in options.keys() if opt_name not in (provider.supported_options or []) ] if invalid_opts: raise OpenPeerPowerError( f"Invalid options found: {invalid_opts}") options_key = _hash_options(options) else: options_key = "-" key = KEY_PATTERN.format(msg_hash, language.replace("_", "-"), options_key, engine).lower() # Is speech already in memory if key in self.mem_cache: filename = self.mem_cache[key][MEM_CACHE_FILENAME] # Is file store in file cache elif use_cache and key in self.file_cache: filename = self.file_cache[key] self.opp.async_create_task(self.async_file_to_mem(key)) # Load speech from provider into memory else: filename = await self.async_get_tts_audio(engine, key, message, use_cache, language, options) return f"/api/tts_proxy/{filename}"
def get(self, requester_path: str, secret: str) -> str: """Return the value of a secret.""" current_path = Path(requester_path) secret_dir = current_path while True: secret_dir = secret_dir.parent try: secret_dir.relative_to(self.config_dir) except ValueError: # We went above the config dir break secrets = self._load_secret_yaml(secret_dir) if secret in secrets: _LOGGER.debug( "Secret %s retrieved from secrets.yaml in folder %s", secret, secret_dir, ) return secrets[secret] raise OpenPeerPowerError(f"Secret {secret} not defined")
async def async_from_config( opp: OpenPeerPower, config: ConfigType | Template, config_validation: bool = True, ) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. """ if isinstance(config, Template): # We got a condition template, wrap it in a configuration to pass along. config = { CONF_CONDITION: "template", CONF_VALUE_TEMPLATE: config, } condition = config.get(CONF_CONDITION) for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT): factory = getattr(sys.modules[__name__], fmt.format(condition), None) if factory: break if factory is None: raise OpenPeerPowerError(f'Invalid condition "{condition}" specified {config}') # Check for partials to properly determine if coroutine function check_factory = factory while isinstance(check_factory, ft.partial): check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): return cast(ConditionCheckerType, await factory(opp, config, config_validation)) return cast(ConditionCheckerType, factory(config, config_validation))
def _load_secret_yaml(self, secret_dir: Path) -> dict[str, str]: """Load the secrets yaml from path.""" secret_path = secret_dir / SECRET_YAML if secret_path in self._cache: return self._cache[secret_path] _LOGGER.debug("Loading %s", secret_path) try: secrets = load_yaml(str(secret_path)) if not isinstance(secrets, dict): raise OpenPeerPowerError("Secrets is not a dictionary") if "logger" in secrets: logger = str(secrets["logger"]).lower() if logger == "debug": _LOGGER.setLevel(logging.DEBUG) else: _LOGGER.error( "Error in secrets.yaml: 'logger: debug' expected, but 'logger: %s' found", logger, ) del secrets["logger"] except FileNotFoundError: secrets = {} self._cache[secret_path] = secrets return secrets
async def test_webhook_camera_stream_stream_available_but_errors( opp, create_registrations, webhook_client): """Test fetching camera stream URLs for an HLS/stream-supporting camera but that streaming errors.""" opp.states.async_set("camera.stream_camera", "idle", {"supported_features": CAMERA_SUPPORT_STREAM}) webhook_id = create_registrations[1]["webhook_id"] with patch( "openpeerpower.components.camera.async_request_stream", side_effect=OpenPeerPowerError(), ): resp = await webhook_client.post( f"/api/webhook/{webhook_id}", json={ "type": "stream_camera", "data": { "camera_entity_id": "camera.stream_camera" }, }, ) assert resp.status == 200 webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert webhook_json[ "mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera"
async def load_auth_provider_module(opp: OpenPeerPower, provider: str) -> types.ModuleType: """Load an auth provider.""" try: module = importlib.import_module( f"openpeerpower.auth.providers.{provider}") except ImportError as err: _LOGGER.error("Unable to load auth provider %s: %s", provider, err) raise OpenPeerPowerError( f"Unable to load auth provider {provider}: {err}") if opp.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module processed = opp.data.get(DATA_REQS) if processed is None: processed = opp.data[DATA_REQS] = set() elif provider in processed: return module # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore await requirements.async_process_requirements(opp, f"auth provider {provider}", reqs) processed.add(provider) return module
async def async_handle_record_service(camera, call): """Handle stream recording service calls.""" async with async_timeout.timeout(10): source = await camera.stream_source() if not source: raise OpenPeerPowerError( f"{camera.entity_id} does not support record service") opp = camera.opp filename = call.data[CONF_FILENAME] filename.opp = opp video_path = filename.async_render(variables={ATTR_ENTITY_ID: camera}) data = { CONF_STREAM_SOURCE: source, CONF_FILENAME: video_path, CONF_DURATION: call.data[CONF_DURATION], CONF_LOOKBACK: call.data[CONF_LOOKBACK], } await opp.services.async_call(DOMAIN_STREAM, SERVICE_RECORD, data, blocking=True, context=call.context)
async def ws_camera_stream(opp, connection, msg): """Handle get camera stream websocket command. Async friendly. """ try: entity_id = msg["entity_id"] camera = _get_camera_from_entity_id(opp, entity_id) camera_prefs = opp.data[DATA_CAMERA_PREFS].get(entity_id) async with async_timeout.timeout(10): source = await camera.stream_source() if not source: raise OpenPeerPowerError( f"{camera.entity_id} does not support play stream service") fmt = msg["format"] url = request_stream(opp, source, fmt=fmt, keepalive=camera_prefs.preload_stream) connection.send_result(msg["id"], {"url": url}) except OpenPeerPowerError as ex: _LOGGER.error("Error requesting stream: %s", ex) connection.send_error(msg["id"], "start_stream_failed", str(ex)) except asyncio.TimeoutError: _LOGGER.error("Timeout getting stream source") connection.send_error(msg["id"], "start_stream_failed", "Timeout getting stream source")
async def async_handle_play_stream_service(camera, service_call): """Handle play stream services calls.""" async with async_timeout.timeout(10): source = await camera.stream_source() if not source: raise OpenPeerPowerError( f"{camera.entity_id} does not support play stream service") opp = camera.opp camera_prefs = opp.data[DATA_CAMERA_PREFS].get(camera.entity_id) fmt = service_call.data[ATTR_FORMAT] entity_ids = service_call.data[ATTR_MEDIA_PLAYER] url = request_stream(opp, source, fmt=fmt, keepalive=camera_prefs.preload_stream) data = { ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: f"{opp.config.api.base_url}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } await opp.services.async_call(DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context)
def start_multiple(service): """Service to start multiple zones in sequence.""" zones_list = [] person = opp.data[DOMAIN_RACHIO][config_entry.entry_id] entity_id = service.data[ATTR_ENTITY_ID] duration = iter(service.data[ATTR_DURATION]) default_time = service.data[ATTR_DURATION][0] entity_to_zone_id = { entity.entity_id: entity.zone_id for entity in zone_entities } for (count, data) in enumerate(entity_id): if data in entity_to_zone_id: # Time can be passed as a list per zone, # or one time for all zones time = int(next(duration, default_time)) * 60 zones_list.append({ ATTR_ID: entity_to_zone_id.get(data), ATTR_DURATION: time, ATTR_SORT_ORDER: count, }) if len(zones_list) != 0: person.start_multiple_zones(zones_list) _LOGGER.debug("Starting zone(s) %s", entity_id) else: raise OpenPeerPowerError( "No matching zones found in given entity_ids")
def _get_camera_from_entity_id(opp, entity_id): """Get camera component from entity_id.""" component = opp.data.get(DOMAIN) if component is None: raise OpenPeerPowerError("Camera integration not set up") camera = component.get_entity(entity_id) if camera is None: raise OpenPeerPowerError("Camera not found") if not camera.is_on: raise OpenPeerPowerError("Camera is off") return camera
async def async_from_config( opp: OpenPeerPower, config: ConfigType, config_validation: bool = True) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. """ for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT): factory = getattr(sys.modules[__name__], fmt.format(config.get(CONF_CONDITION)), None) if factory: break if factory is None: raise OpenPeerPowerError('Invalid condition "{}" specified {}'.format( config.get(CONF_CONDITION), config)) # Check for partials to properly determine if coroutine function check_factory = factory while isinstance(check_factory, ft.partial): check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): return cast(ConditionCheckerType, await factory(opp, config, config_validation)) return cast(ConditionCheckerType, factory(config, config_validation))
async def async_subscribe( self, topic: str, msg_callback: MessageCallbackType, qos: int, encoding: Optional[str] = None, ) -> Callable[[], None]: """Set up a subscription to a topic with the provided qos. This method is a coroutine. """ if not isinstance(topic, str): raise OpenPeerPowerError("Topic needs to be a string!") subscription = Subscription(topic, msg_callback, qos, encoding) self.subscriptions.append(subscription) await self._async_perform_subscription(topic, qos) @callback def async_remove() -> None: """Remove subscription.""" if subscription not in self.subscriptions: raise OpenPeerPowerError("Can't remove subscription twice") self.subscriptions.remove(subscription) if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return # Only unsubscribe if currently connected. if self.connected: self.opp.async_create_task(self._async_unsubscribe(topic)) return async_remove
async def async_remove(self, *, force_remove: bool = False) -> None: """Remove entity from Open Peer Power. If the entity has a non disabled entry in the entity registry, the entity's state will be set to unavailable, in the same way as when the entity registry is loaded. If the entity doesn't have a non disabled entry in the entity registry, or if force_remove=True, its state will be removed. """ if self.platform and not self._added: raise OpenPeerPowerError( f"Entity {self.entity_id} async_remove called twice") self._added = False if self._on_remove is not None: while self._on_remove: self._on_remove.pop()() await self.async_internal_will_remove_from_opp() await self.async_will_remove_from_opp() # Check if entry still exists in entity registry (e.g. unloading config entry) if (not force_remove and self.registry_entry and not self.registry_entry.disabled): # Set the entity's state will to unavailable + ATTR_RESTORED: True self.registry_entry.write_unavailable_state(self.opp) else: self.opp.states.async_remove(self.entity_id, context=self._context)
async def test_reload_config_handles_load_fails(opp, calls): """Test the reload config service.""" assert await async_setup_component( opp, automation.DOMAIN, { automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event"}, "action": { "service": "test.automation", "data_template": {"event": "{{ trigger.event.event_type }}"}, }, } }, ) assert opp.states.get("automation.hello") is not None opp.bus.async_fire("test_event") await opp.async_block_till_done() assert len(calls) == 1 assert calls[0].data.get("event") == "test_event" with patch( "openpeerpower.config.load_yaml_config_file", side_effect=OpenPeerPowerError("bla"), ): await opp.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) assert opp.states.get("automation.hello") is not None opp.bus.async_fire("test_event") await opp.async_block_till_done() assert len(calls) == 2
async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) device = self.device if device.hasDualSetpointStatus: if target_temp_low is None or target_temp_high is None: raise OpenPeerPowerError( "Could not find target_temp_low and/or target_temp_high in arguments" ) _LOGGER.debug("Set temperature: %s - %s", target_temp_low, target_temp_high) try: await self._update_thermostat( self.location, device, coolSetpoint=target_temp_low, heatSetpoint=target_temp_high, ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) else: temp = kwargs.get(ATTR_TEMPERATURE) _LOGGER.debug("Set temperature: %s", temp) try: await self._update_thermostat(self.location, device, heatSetpoint=temp) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh()
async def _load_mfa_module(opp: OpenPeerPower, module_name: str) -> types.ModuleType: """Load an mfa auth module.""" module_path = f"openpeerpower.auth.mfa_modules.{module_name}" try: module = importlib.import_module(module_path) except ImportError as err: _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) raise OpenPeerPowerError( f"Unable to load mfa module {module_name}: {err}") from err if opp.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module processed = opp.data.get(DATA_REQS) if processed and module_name in processed: return module processed = opp.data[DATA_REQS] = set() # https://github.com/python/mypy/issues/1424 await requirements.async_process_requirements( opp, module_path, module.REQUIREMENTS # type: ignore ) processed.add(module_name) return module
def async_remove() -> None: """Remove trigger.""" if instance not in self.trigger_instances: raise OpenPeerPowerError("Can't remove trigger twice") if instance.remove: instance.remove() self.trigger_instances.remove(instance)
async def call_api(self, method, function, data=None, binary=False, params=None): """Make an api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} url = self._server_url.format(function) payload = None if binary: headers[CONTENT_TYPE] = "application/octet-stream" payload = data else: headers[CONTENT_TYPE] = CONTENT_TYPE_JSON if data is not None: payload = json.dumps(data).encode() else: payload = None try: with async_timeout.timeout(self.timeout): response = await getattr(self.websession, method)(url, data=payload, headers=headers, params=params) answer = await response.json() _LOGGER.debug("Read from microsoft face api: %s", answer) if response.status < 300: return answer _LOGGER.warning("Error %d microsoft face api %s", response.status, response.url) raise OpenPeerPowerError(answer["error"]["message"]) except aiohttp.ClientError: _LOGGER.warning("Can't connect to microsoft face api") except asyncio.TimeoutError: _LOGGER.warning("Timeout from microsoft face api %s", response.url) raise OpenPeerPowerError("Network error on microsoft face api.")
def turn_off(self, **kwargs): """Turn off the fan.""" _LOGGER.debug("Turning off fan") if not self._smarty.turn_off(): raise OpenPeerPowerError("Failed to turn off the fan") self._smarty_fan_speed = 0 self.schedule_update_op_state()
async def async_call_fritz_service(service_call): """Call correct Fritz service.""" if not (fritzbox_entry_ids := await _async_get_configured_fritz_tools( opp, service_call)): raise OpenPeerPowerError( f"Failed to call service '{service_call.service}'. Config entry for target not found" )
def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE: """Load a YAML file.""" try: with open(fname, encoding="utf-8") as conf_file: return parse_yaml(conf_file, secrets) except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) raise OpenPeerPowerError(exc) from exc
async def async_delete(self): """Delete config.""" if self.opp.config.safe_mode: raise OpenPeerPowerError("Deleting not supported in safe mode") await self._store.async_remove() self._data = None self._config_updated()
def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" # pylint: disable=import-outside-toplevel import paho.mqtt.client as mqtt if result_code != 0: raise OpenPeerPowerError( f"Error talking to MQTT: {mqtt.error_string(result_code)}")
async def async_get_zwave_parameter(self, parameter): """Repsond to an entity service command to request a Z-Wave device parameter from the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise OpenPeerPowerError( f"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave device {self.entity_id}" ) await self._node.get_zwave_parameter(parameter)