async def upload_file(self, timeout=None): """Upload file to Jabber server and return new URL. upload a file with Jabber XEP_0363 from a remote URL or a local file path and return a URL of that file. """ if data.get(ATTR_URL_TEMPLATE): _LOGGER.debug("Got url template: %s", data[ATTR_URL_TEMPLATE]) templ = template_helper.Template(data[ATTR_URL_TEMPLATE], opp) get_url = template_helper.render_complex(templ, None) url = await self.upload_file_from_url(get_url, timeout=timeout) elif data.get(ATTR_URL): url = await self.upload_file_from_url(data[ATTR_URL], timeout=timeout) elif data.get(ATTR_PATH_TEMPLATE): _LOGGER.debug("Got path template: %s", data[ATTR_PATH_TEMPLATE]) templ = template_helper.Template(data[ATTR_PATH_TEMPLATE], opp) get_path = template_helper.render_complex(templ, None) url = await self.upload_file_from_path(get_path, timeout=timeout) elif data.get(ATTR_PATH): url = await self.upload_file_from_path(data[ATTR_PATH], timeout=timeout) else: url = None if url is None: _LOGGER.error("No path or URL found for file") raise FileUploadError("Could not upload file") return url
def _process_data(self) -> None: """Process new data.""" try: rendered = dict(self._static_rendered) for key in self._to_render: rendered[key] = self._config[key].async_render( self.coordinator.data["run_variables"], parse_result=key in self._parse_result, ) if CONF_ATTRIBUTES in self._config: rendered[CONF_ATTRIBUTES] = template.render_complex( self._config[CONF_ATTRIBUTES], self.coordinator.data["run_variables"], ) self._rendered = rendered except template.TemplateError as err: logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( "Error rendering %s template for %s: %s", key, self.entity_id, err ) self._rendered = self._static_rendered self.async_set_context(self.coordinator.data["context"])
async def _async_delay(self, action, variables, context): """Handle delay.""" # Call ourselves in the future to continue work unsub = None @callback def async_script_delay(now): """Handle delay.""" with suppress(ValueError): self._async_listener.remove(unsub) self.opp.async_create_task(self.async_run(variables, context)) delay = action[CONF_DELAY] try: if isinstance(delay, template.Template): delay = vol.All(cv.time_period, cv.positive_timedelta)( delay.async_render(variables)) elif isinstance(delay, dict): delay_data = {} delay_data.update(template.render_complex(delay, variables)) delay = cv.time_period(delay_data) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) raise _StopScript self.last_action = action.get(CONF_ALIAS, f"delay {delay}") self._log("Executing step %s" % self.last_action) unsub = async_track_point_in_utc_time(self.opp, async_script_delay, date_util.utcnow() + delay) self._async_listener.append(unsub) raise _SuspendScript
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(), )
def send_message(self, message="", **kwargs): """Send a notification to the device.""" data = {**self.data, **kwargs.get(ATTR_DATA, {})} if data.get(ATTR_VALUE) is not None: templ = template_helper.Template(self.data[ATTR_VALUE], self.opp) data[ATTR_VALUE] = template_helper.render_complex(templ, None) self.opp.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
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)
def _get_pos_time_period_template(self, key): try: return cv.positive_time_period( template.render_complex(self._action[key], self._variables)) except (exceptions.TemplateError, vol.Invalid) as ex: self._log( "Error rendering %s %s template: %s", self._script.name, key, ex, level=logging.ERROR, ) raise _StopScript from ex
async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) self._log("Executing step %s" % self.last_action) event_data = dict(action.get(CONF_EVENT_DATA, {})) if CONF_EVENT_DATA_TEMPLATE in action: try: event_data.update( template.render_complex(action[CONF_EVENT_DATA_TEMPLATE], variables)) except exceptions.TemplateError as ex: _LOGGER.error("Error rendering event data template: %s", ex) self.opp.bus.async_fire(action[CONF_EVENT], event_data, context=context)
async def _async_event_step(self): """Fire an event.""" self._step_log(self._action.get(CONF_ALIAS, self._action[CONF_EVENT])) event_data = {} for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]: if conf not in self._action: continue try: event_data.update( template.render_complex(self._action[conf], self._variables)) except exceptions.TemplateError as ex: self._log("Error rendering event data template: %s", ex, level=logging.ERROR) trace_set_result(event=self._action[CONF_EVENT], event_data=event_data) self._opp.bus.async_fire(self._action[CONF_EVENT], event_data, context=self._context)
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)
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, )
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
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, )
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()
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()
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 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"], }
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)