def __init__( self, opp: OpenPeerPower, sequence: Sequence[Dict[str, Any]], name: Optional[str] = None, change_listener: Optional[Callable[..., Any]] = None, ) -> None: """Initialize the script.""" self.opp = opp self.sequence = sequence template.attach(opp, self.sequence) self.name = name self._change_listener = change_listener self._cur = -1 self._exception_step: Optional[int] = None self.last_action = None self.last_triggered: Optional[datetime] = None self.can_cancel = any( CONF_DELAY in action or CONF_WAIT_TEMPLATE in action for action in self.sequence) self._async_listener: List[CALLBACK_TYPE] = [] self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} self._actions = { ACTION_DELAY: self._async_delay, ACTION_WAIT_TEMPLATE: self._async_wait_template, ACTION_CHECK_CONDITION: self._async_check_condition, ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, ACTION_DEVICE_AUTOMATION: self._async_device_automation, ACTION_ACTIVATE_SCENE: self._async_activate_scene, } self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None
def __init__( self, opp, camera_entity, name, category_index, config, ): """Initialize the TensorFlow entity.""" model_config = config.get(CONF_MODEL) self.opp = opp self._camera_entity = camera_entity if name: self._name = name else: self._name = f"TensorFlow {split_entity_id(camera_entity)[1]}" self._category_index = category_index self._min_confidence = config.get(CONF_CONFIDENCE) self._file_out = config.get(CONF_FILE_OUT) # handle categories and specific detection areas self._label_id_offset = model_config.get(CONF_LABEL_OFFSET) categories = model_config.get(CONF_CATEGORIES) self._include_categories = [] self._category_areas = {} for category in categories: if isinstance(category, dict): category_name = category.get(CONF_CATEGORY) category_area = category.get(CONF_AREA) self._include_categories.append(category_name) self._category_areas[category_name] = [0, 0, 1, 1] if category_area: self._category_areas[category_name] = [ category_area.get(CONF_TOP), category_area.get(CONF_LEFT), category_area.get(CONF_BOTTOM), category_area.get(CONF_RIGHT), ] else: self._include_categories.append(category) self._category_areas[category] = [0, 0, 1, 1] # Handle global detection area self._area = [0, 0, 1, 1] area_config = model_config.get(CONF_AREA) if area_config: self._area = [ area_config.get(CONF_TOP), area_config.get(CONF_LEFT), area_config.get(CONF_BOTTOM), area_config.get(CONF_RIGHT), ] template.attach(opp, self._file_out) self._matches = {} self._total_matches = 0 self._last_image = None self._process_time = 0
async def async_call_from_config(opp, config, blocking=False, variables=None, validate_config=True, context=None): """Call a service based on a config hash.""" if validate_config: try: config = cv.SERVICE_SCHEMA(config) except vol.Invalid as ex: _LOGGER.error("Invalid config for calling service: %s", ex) return if CONF_SERVICE in config: domain_service = config[CONF_SERVICE] else: try: config[CONF_SERVICE_TEMPLATE].opp = opp domain_service = config[CONF_SERVICE_TEMPLATE].async_render( variables) domain_service = cv.service(domain_service) except TemplateError as ex: if blocking: raise _LOGGER.error("Error rendering service name template: %s", ex) return except vol.Invalid: if blocking: raise _LOGGER.error("Template rendered invalid service: %s", domain_service) return domain, service_name = domain_service.split(".", 1) service_data = dict(config.get(CONF_SERVICE_DATA, {})) if CONF_SERVICE_DATA_TEMPLATE in config: try: template.attach(opp, config[CONF_SERVICE_DATA_TEMPLATE]) service_data.update( template.render_complex(config[CONF_SERVICE_DATA_TEMPLATE], variables)) except TemplateError as ex: _LOGGER.error("Error rendering data template: %s", ex) return if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] await opp.services.async_call(domain, service_name, service_data, blocking=blocking, context=context)
async def webhook_render_template(opp, config_entry, data): """Handle a render template webhook.""" resp = {} for key, item in data.items(): try: tpl = item[ATTR_TEMPLATE] attach(opp, tpl) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) except TemplateError as ex: resp[key] = {"error": str(ex)} return webhook_response(resp, registration=config_entry.data)
async def async_setup(opp, config): """Activate Alexa component.""" intents = copy.deepcopy(config[DOMAIN]) template.attach(opp, intents) for intent_type, conf in intents.items(): if CONF_ACTION in conf: conf[CONF_ACTION] = script.Script(opp, conf[CONF_ACTION], f"Intent Script {intent_type}", DOMAIN) intent.async_register(opp, ScriptIntentHandler(intent_type, conf)) return True
def test_not_mutate_input(self): """Test for immutable input.""" config = cv.SERVICE_SCHEMA({ "service": "test_domain.test_service", "entity_id": "hello.world, sensor.beer", "data": { "hello": 1 }, "data_template": { "nested": { "value": "{{ 1 + 1 }}" } }, }) orig = deepcopy(config) # Only change after call is each template getting opp.attached template.attach(self.opp, orig) service.call_from_config(self.opp, config, validate_config=False) assert orig == config
async def async_call_action_from_config(opp: OpenPeerPower, config: dict, variables: dict, context: Context | None) -> None: """Execute a device action.""" webhook_id = webhook_id_from_device_id(opp, config[CONF_DEVICE_ID]) if webhook_id is None: raise InvalidDeviceAutomationConfig( "Unable to resolve webhook ID from the device ID") service_name = get_notify_service(opp, webhook_id) if service_name is None: raise InvalidDeviceAutomationConfig( "Unable to find notify service for webhook ID") service_data = {notify.ATTR_TARGET: webhook_id} # Render it here because we have access to variables here. for key in (notify.ATTR_MESSAGE, notify.ATTR_TITLE, notify.ATTR_DATA): if key not in config: continue value_template = config[key] template.attach(opp, value_template) try: service_data[key] = template.render_complex( value_template, variables) except template.TemplateError as err: raise InvalidDeviceAutomationConfig( f"Error rendering {key}: {err}") from err await opp.services.async_call(notify.DOMAIN, service_name, service_data, blocking=True, context=context)
async def async_attach_trigger( opp: OpenPeerPower, config, action, automation_info, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: Dict[str, timedelta] = {} @callback def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" @callback def call_action(): """Call action with right context.""" opp.async_run_job( action( { "trigger": { "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], } }, context=to_s.context, )) # Ignore changes to state attributes if from/to is in use if (not match_all and from_s is not None and to_s is not None and from_s.state == to_s.state): return if not time_delta: call_action() return variables = { "trigger": { "platform": "state", "entity_id": entity, "from_state": from_s, "to_state": to_s, } } try: if isinstance(time_delta, template.Template): period[entity] = vol.All( cv.time_period, cv.positive_timedelta)(time_delta.async_render(variables)) elif isinstance(time_delta, dict): time_delta_data = {} time_delta_data.update( template.render_complex(time_delta, variables)) period[entity] = vol.All( cv.time_period, cv.positive_timedelta)(time_delta_data) else: period[entity] = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' for template: %s", automation_info["name"], ex) return unsub_track_same[entity] = async_track_same_state( opp, period[entity], call_action, lambda _, _2, to_state: to_state.state == to_s.state, entity_ids=entity, ) unsub = async_track_state_change(opp, entity_id, state_automation_listener, from_state, to_state) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
async def async_attach_trigger(opp, config, action, automation_info, *, platform_type="event"): """Listen for events based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None variables = None if automation_info: variables = automation_info.get("variables") template.attach(opp, config[CONF_EVENT_TYPE]) event_types = template.render_complex(config[CONF_EVENT_TYPE], variables, limited=True) removes = [] event_data_schema = None if CONF_EVENT_DATA in config: # Render the schema input template.attach(opp, config[CONF_EVENT_DATA]) event_data = {} event_data.update( template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)) # Build the schema event_data_schema = vol.Schema( {vol.Required(key): value for key, value in event_data.items()}, extra=vol.ALLOW_EXTRA, ) event_context_schema = None if CONF_EVENT_CONTEXT in config: # Render the schema input template.attach(opp, config[CONF_EVENT_CONTEXT]) event_context = {} event_context.update( template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)) # Build the schema event_context_schema = vol.Schema( { vol.Required(key): _schema_value(value) for key, value in event_context.items() }, extra=vol.ALLOW_EXTRA, ) job = OppJob(action) @callback def handle_event(event): """Listen for events and calls the action when data matches.""" try: # Check that the event data and context match the configured # schema if one was provided if event_data_schema: event_data_schema(event.data) if event_context_schema: event_context_schema(event.context.as_dict()) except vol.Invalid: # If event doesn't match, skip event return opp.async_run_opp_job( job, { "trigger": { "platform": platform_type, "event": event, "description": f"event '{event.event_type}'", "id": trigger_id, } }, event.context, ) removes = [ opp.bus.async_listen(event_type, handle_event) for event_type in event_types ] @callback def remove_listen_events(): """Remove event listeners.""" for remove in removes: remove() return remove_listen_events
async def async_added_to_opp(self) -> None: """Handle being added to Open Peer Power.""" template.attach(self.opp, self._config) await super().async_added_to_opp() if self.coordinator.data is not None: self._process_data()
def async_prepare_call_from_config( opp: OpenPeerPower, config: ConfigType, variables: TemplateVarsType = None, validate_config: bool = False, ) -> ServiceParams: """Prepare to call a service based on a config hash.""" if validate_config: try: config = cv.SERVICE_SCHEMA(config) except vol.Invalid as ex: raise OpenPeerPowerError( f"Invalid config for calling service: {ex}") from ex if CONF_SERVICE in config: domain_service = config[CONF_SERVICE] else: domain_service = config[CONF_SERVICE_TEMPLATE] if isinstance(domain_service, template.Template): try: domain_service.opp = opp domain_service = domain_service.async_render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: raise OpenPeerPowerError( f"Error rendering service name template: {ex}") from ex except vol.Invalid as ex: raise OpenPeerPowerError( f"Template rendered invalid service: {domain_service}") from ex domain, service = domain_service.split(".", 1) target = {} if CONF_TARGET in config: conf = config[CONF_TARGET] try: if isinstance(conf, template.Template): conf.opp = opp target.update(conf.async_render(variables)) else: template.attach(opp, conf) target.update(template.render_complex(conf, variables)) if CONF_ENTITY_ID in target: target[CONF_ENTITY_ID] = cv.comp_entity_ids( target[CONF_ENTITY_ID]) except TemplateError as ex: raise OpenPeerPowerError( f"Error rendering service target template: {ex}") from ex except vol.Invalid as ex: raise OpenPeerPowerError( f"Template rendered invalid entity IDs: {target[CONF_ENTITY_ID]}" ) from ex service_data = {} for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: if conf not in config: continue try: template.attach(opp, config[conf]) service_data.update( template.render_complex(config[conf], variables)) except TemplateError as ex: raise OpenPeerPowerError( f"Error rendering data template: {ex}") from ex if CONF_SERVICE_ENTITY_ID in config: if target: target[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] else: target = {ATTR_ENTITY_ID: config[CONF_SERVICE_ENTITY_ID]} return { "domain": domain, "service": service, "service_data": service_data, "target": target, }
async def async_attach_trigger( opp, config, action, automation_info, *, platform_type="numeric_state" ): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.opp = opp time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) unsub_track_same = None @callback def template_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" nonlocal unsub_track_same @callback def call_action(): """Call action with right context.""" opp.async_run_job( action( { "trigger": { "platform": "template", "entity_id": entity_id, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period, } }, context=(to_s.context if to_s else None), ) ) if not time_delta: call_action() return variables = { "trigger": { "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, } } try: if isinstance(time_delta, template.Template): period = vol.All(cv.time_period, cv.positive_timedelta)( time_delta.async_render(variables) ) elif isinstance(time_delta, dict): time_delta_data = {} time_delta_data.update(template.render_complex(time_delta, variables)) period = vol.All(cv.time_period, cv.positive_timedelta)(time_delta_data) else: period = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex ) return unsub_track_same = async_track_same_state( opp, period, call_action, lambda _, _2, _3: condition.async_template(opp, value_template), value_template.extract_entities(), ) unsub = async_track_template(opp, value_template, template_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() if unsub_track_same: # pylint: disable=not-callable unsub_track_same() return async_remove
def __init__( self, opp: OpenPeerPower, sequence: Sequence[dict[str, Any]], name: str, domain: str, *, # Used in "Running <running_description>" log message running_description: str | None = None, change_listener: Callable[..., Any] | None = None, script_mode: str = DEFAULT_SCRIPT_MODE, max_runs: int = DEFAULT_MAX, max_exceeded: str = DEFAULT_MAX_EXCEEDED, logger: logging.Logger | None = None, log_exceptions: bool = True, top_level: bool = True, variables: ScriptVariables | None = None, ) -> None: """Initialize the script.""" all_scripts = opp.data.get(DATA_SCRIPTS) if not all_scripts: all_scripts = opp.data[DATA_SCRIPTS] = [] opp.bus.async_listen_once( EVENT_OPENPEERPOWER_STOP, partial(_async_stop_scripts_at_shutdown, opp)) self._top_level = top_level if top_level: all_scripts.append({ "instance": self, "started_before_shutdown": not opp.is_stopping }) if DATA_SCRIPT_BREAKPOINTS not in opp.data: opp.data[DATA_SCRIPT_BREAKPOINTS] = {} self._opp = opp self.sequence = sequence template.attach(opp, self.sequence) self.name = name self.domain = domain self.running_description = running_description or f"{domain} script" self._change_listener = change_listener self._change_listener_job = (None if change_listener is None else OppJob(change_listener)) self.script_mode = script_mode self._set_logger(logger) self._log_exceptions = log_exceptions self.last_action = None self.last_triggered: datetime | None = None self._runs: list[_ScriptRun] = [] self.max_runs = max_runs self._max_exceeded = max_exceeded if script_mode == SCRIPT_MODE_QUEUED: self._queue_lck = asyncio.Lock() self._config_cache: dict[set[tuple], Callable[..., bool]] = {} self._repeat_script: dict[int, Script] = {} self._choose_data: dict[int, _ChooseData] = {} self._referenced_entities: set[str] | None = None self._referenced_devices: set[str] | None = None self._referenced_areas: set[str] | None = None self.variables = variables self._variables_dynamic = template.is_complex(variables) if self._variables_dynamic: template.attach(opp, variables)
def __init__(self, opp, camera_entity, name, doods, detector, config): """Initialize the DOODS entity.""" self.opp = opp self._camera_entity = camera_entity if name: self._name = name else: name = split_entity_id(camera_entity)[1] self._name = f"Doods {name}" self._doods = doods self._file_out = config[CONF_FILE_OUT] self._detector_name = detector["name"] # detector config and aspect ratio self._width = None self._height = None self._aspect = None if detector["width"] and detector["height"]: self._width = detector["width"] self._height = detector["height"] self._aspect = self._width / self._height # the base confidence dconfig = {} confidence = config[CONF_CONFIDENCE] # handle labels and specific detection areas labels = config[CONF_LABELS] self._label_areas = {} self._label_covers = {} for label in labels: if isinstance(label, dict): label_name = label[CONF_NAME] if label_name not in detector["labels"] and label_name != "*": _LOGGER.warning("Detector does not support label %s", label_name) continue # If label confidence is not specified, use global confidence label_confidence = label.get(CONF_CONFIDENCE) if not label_confidence: label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence # Label area label_area = label.get(CONF_AREA) self._label_areas[label_name] = [0, 0, 1, 1] self._label_covers[label_name] = True if label_area: self._label_areas[label_name] = [ label_area[CONF_TOP], label_area[CONF_LEFT], label_area[CONF_BOTTOM], label_area[CONF_RIGHT], ] self._label_covers[label_name] = label_area[CONF_COVERS] else: if label not in detector["labels"] and label != "*": _LOGGER.warning("Detector does not support label %s", label) continue self._label_areas[label] = [0, 0, 1, 1] self._label_covers[label] = True if label not in dconfig or dconfig[label] > confidence: dconfig[label] = confidence if not dconfig: dconfig["*"] = confidence # Handle global detection area self._area = [0, 0, 1, 1] self._covers = True area_config = config.get(CONF_AREA) if area_config: self._area = [ area_config[CONF_TOP], area_config[CONF_LEFT], area_config[CONF_BOTTOM], area_config[CONF_RIGHT], ] self._covers = area_config[CONF_COVERS] template.attach(opp, self._file_out) self._dconfig = dconfig self._matches = {} self._total_matches = 0 self._last_image = None self._process_time = 0
async def async_attach_trigger( opp, config, action, automation_info, *, platform_type="numeric_state" ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_ids = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} armed_entities = set() period: dict = {} attribute = config.get(CONF_ATTRIBUTE) job = OppJob(action) trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} if value_template is not None: value_template.opp = opp def variables(entity_id): """Return a dict with trigger variables.""" trigger_info = { "trigger": { "platform": "numeric_state", "entity_id": entity_id, "below": below, "above": above, "attribute": attribute, } } return {**_variables, **trigger_info} @callback def check_numeric_state(entity_id, from_s, to_s): """Return whether the criteria are met, raise ConditionError if unknown.""" return condition.async_numeric_state( opp, to_s, below, above, value_template, variables(entity_id), attribute ) # Each entity that starts outside the range is already armed (ready to fire). for entity_id in entity_ids: try: if not check_numeric_state(entity_id, None, entity_id): armed_entities.add(entity_id) except exceptions.ConditionError as ex: _LOGGER.warning( "Error initializing '%s' trigger: %s", automation_info["name"], ex, ) @callback def state_automation_listener(event): """Listen for state changes and calls action.""" entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @callback def call_action(): """Call action with right context.""" opp.async_run_opp_job( job, { "trigger": { "platform": platform_type, "entity_id": entity_id, "below": below, "above": above, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", "id": trigger_id, } }, to_s.context, ) @callback def check_numeric_state_no_raise(entity_id, from_s, to_s): """Return True if the criteria are now met, False otherwise.""" try: return check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError: # This is an internal same-state listener so we just drop the # error. The same error will be reached and logged by the # primary async_track_state_change_event() listener. return False try: matching = check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError as ex: _LOGGER.warning("Error in '%s' trigger: %s", automation_info["name"], ex) return if not matching: armed_entities.add(entity_id) elif entity_id in armed_entities: armed_entities.discard(entity_id) if time_delta: try: period[entity_id] = cv.positive_time_period( template.render_complex(time_delta, variables(entity_id)) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex, ) return unsub_track_same[entity_id] = async_track_same_state( opp, period[entity_id], call_action, entity_ids=entity_id, async_check_same_func=check_numeric_state_no_raise, ) else: call_action() unsub = async_track_state_change_event(opp, entity_ids, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
async def async_attach_trigger(opp, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None topic = config[CONF_TOPIC] wanted_payload = config.get(CONF_PAYLOAD) value_template = config.get(CONF_VALUE_TEMPLATE) encoding = config[CONF_ENCODING] or None qos = config[CONF_QOS] job = OppJob(action) variables = None if automation_info: variables = automation_info.get("variables") template.attach(opp, wanted_payload) if wanted_payload: wanted_payload = wanted_payload.async_render(variables, limited=True, parse_result=False) template.attach(opp, topic) if isinstance(topic, template.Template): topic = topic.async_render(variables, limited=True, parse_result=False) topic = mqtt.util.valid_subscribe_topic(topic) template.attach(opp, value_template) @callback def mqtt_automation_listener(mqttmsg): """Listen for MQTT messages.""" payload = mqttmsg.payload if value_template is not None: payload = value_template.async_render_with_possible_json_value( payload, error_value=None, ) if wanted_payload is None or wanted_payload == payload: data = { "platform": "mqtt", "topic": mqttmsg.topic, "payload": mqttmsg.payload, "qos": mqttmsg.qos, "description": f"mqtt topic {mqttmsg.topic}", "id": trigger_id, } with suppress(ValueError): data["payload_json"] = json.loads(mqttmsg.payload) opp.async_run_opp_job(job, {"trigger": data}) _LOGGER.debug("Attaching MQTT trigger for topic: '%s', payload: '%s'", topic, wanted_payload) remove = await mqtt.async_subscribe(opp, topic, mqtt_automation_listener, encoding=encoding, qos=qos) return remove
async def async_attach_trigger(opp, config, action, automation_info, *, platform_type="numeric_state") -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} entities_triggered = set() period: dict = {} if value_template is not None: value_template.opp = opp @callback def check_numeric_state(entity, from_s, to_s): """Return True if criteria are now met.""" if to_s is None: return False variables = { "trigger": { "platform": "numeric_state", "entity_id": entity, "below": below, "above": above, } } return condition.async_numeric_state(opp, to_s, below, above, value_template, variables) @callback def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" @callback def call_action(): """Call action with right context.""" opp.async_run_job( action( { "trigger": { "platform": platform_type, "entity_id": entity, "below": below, "above": above, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], } }, context=to_s.context, )) matching = check_numeric_state(entity, from_s, to_s) if not matching: entities_triggered.discard(entity) elif entity not in entities_triggered: entities_triggered.add(entity) if time_delta: variables = { "trigger": { "platform": "numeric_state", "entity_id": entity, "below": below, "above": above, } } try: if isinstance(time_delta, template.Template): period[entity] = vol.All( cv.time_period, cv.positive_timedelta)( time_delta.async_render(variables)) elif isinstance(time_delta, dict): time_delta_data = {} time_delta_data.update( template.render_complex(time_delta, variables)) period[entity] = vol.All( cv.time_period, cv.positive_timedelta)(time_delta_data) else: period[entity] = time_delta except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex, ) entities_triggered.discard(entity) return unsub_track_same[entity] = async_track_same_state( opp, period[entity], call_action, entity_ids=entity, async_check_same_func=check_numeric_state, ) else: call_action() unsub = async_track_state_change(opp, entity_id, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
def __init__(self, opp, flash_briefings): """Initialize Alexa view.""" super().__init__() self.flash_briefings = copy.deepcopy(flash_briefings) template.attach(opp, self.flash_briefings)
async def async_attach_trigger( opp: OpenPeerPower, config, action, automation_info, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) attribute = config.get(CONF_ATTRIBUTE) job = OppJob(action) trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @callback def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] from_s: State | None = event.data.get("old_state") to_s: State | None = event.data.get("new_state") if from_s is None: old_value = None elif attribute is None: old_value = from_s.state else: old_value = from_s.attributes.get(attribute) if to_s is None: new_value = None elif attribute is None: new_value = to_s.state else: new_value = to_s.attributes.get(attribute) # When we listen for state changes with `match_all`, we # will trigger even if just an attribute changes. When # we listen to just an attribute, we should ignore all # other attribute changes. if attribute is not None and old_value == new_value: return if ( not match_from_state(old_value) or not match_to_state(new_value) or (not match_all and old_value == new_value) ): return @callback def call_action(): """Call action with right context.""" opp.async_run_opp_job( job, { "trigger": { "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], "attribute": attribute, "description": f"state of {entity}", "id": trigger_id, } }, event.context, ) if not time_delta: call_action() return trigger_info = { "trigger": { "platform": "state", "entity_id": entity, "from_state": from_s, "to_state": to_s, } } variables = {**_variables, **trigger_info} try: period[entity] = cv.positive_time_period( template.render_complex(time_delta, variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex ) return def _check_same_state(_, _2, new_st: State): if new_st is None: return False if attribute is None: cur_value = new_st.state else: cur_value = new_st.attributes.get(attribute) if CONF_FROM in config and CONF_TO not in config: return cur_value != old_value return cur_value == new_value unsub_track_same[entity] = async_track_same_state( opp, period[entity], call_action, _check_same_state, entity_ids=entity, ) unsub = async_track_state_change_event(opp, entity_id, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
async def test_confirmable_notification(opp: OpenPeerPower) -> None: """Test confirmable notification blueprint.""" with patch_blueprint( "confirmable_notification.yaml", BUILTIN_BLUEPRINT_FOLDER / "confirmable_notification.yaml", ): assert await async_setup_component( opp, script.DOMAIN, { "script": { "confirm": { "use_blueprint": { "path": "confirmable_notification.yaml", "input": { "notify_device": "frodo", "title": "Lord of the things", "message": "Throw ring in mountain?", "confirm_action": [{ "service": "openpeerpower.turn_on", "target": { "entity_id": "mount.doom" }, }], }, } } } }, ) turn_on_calls = async_mock_service(opp, "openpeerpower", "turn_on") context = Context() with patch( "openpeerpower.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: # Trigger script await opp.services.async_call(script.DOMAIN, "confirm", context=context) # Give script the time to attach the trigger. await asyncio.sleep(0.1) opp.bus.async_fire("mobile_app_notification_action", {"action": "ANYTHING_ELSE"}) opp.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM_" + Context().id}) opp.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM_" + context.id}) await opp.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 _opp, config, variables, _context = mock_call_action.mock_calls[0][1] template.attach(opp, config) rendered_config = template.render_complex(config, variables) assert rendered_config == { "title": "Lord of the things", "message": "Throw ring in mountain?", "alias": "Send notification", "domain": "mobile_app", "type": "notify", "device_id": "frodo", "data": { "actions": [ { "action": "CONFIRM_" + _context.id, "title": "Confirm" }, { "action": "DISMISS_" + _context.id, "title": "Dismiss" }, ] }, } assert len(turn_on_calls) == 1 assert turn_on_calls[0].data == { "entity_id": ["mount.doom"], }
async def async_attach_trigger(opp, config, action, automation_info, *, platform_type="template"): """Listen for state changes based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None value_template = config.get(CONF_VALUE_TEMPLATE) value_template.opp = opp time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) delay_cancel = None job = OppJob(action) armed = False # Arm at setup if the template is already false. try: if not result_as_boolean( value_template.async_render(automation_info["variables"])): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( "Error initializing 'template' trigger for '%s': %s", automation_info["name"], ex, ) @callback def template_listener(event, updates): """Listen for state changes and calls action.""" nonlocal delay_cancel, armed result = updates.pop().result if isinstance(result, exceptions.TemplateError): _LOGGER.warning( "Error evaluating 'template' trigger for '%s': %s", automation_info["name"], result, ) return if delay_cancel: # pylint: disable=not-callable delay_cancel() delay_cancel = None if not result_as_boolean(result): armed = True return # Only fire when previously armed. if not armed: return # Fire! armed = False entity_id = event and event.data.get("entity_id") from_s = event and event.data.get("old_state") to_s = event and event.data.get("new_state") if entity_id is not None: description = f"{entity_id} via template" else: description = "time change or manual update via template" template_variables = { "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, } trigger_variables = { "for": time_delta, "description": description, "id": trigger_id, } @callback def call_action(*_): """Call action with right context.""" nonlocal trigger_variables opp.async_run_opp_job( job, {"trigger": { **template_variables, **trigger_variables }}, (to_s.context if to_s else None), ) if not time_delta: call_action() return try: period = cv.positive_time_period( template.render_complex(time_delta, {"trigger": template_variables})) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' for template: %s", automation_info["name"], ex) return trigger_variables["for"] = period delay_cancel = async_call_later(opp, period.total_seconds(), call_action) info = async_track_template_result( opp, [TrackTemplate(value_template, automation_info["variables"])], template_listener, ) unsub = info.async_remove @callback def async_remove(): """Remove state listeners async.""" unsub() if delay_cancel: # pylint: disable=not-callable delay_cancel() return async_remove