async def _async_process_config(hass, config, component): """Process script configuration.""" async def service_handler(service): """Execute a service call to script.<script name>.""" entity_id = ENTITY_ID_FORMAT.format(service.service) script = component.get_entity(entity_id) if script.is_on: _LOGGER.warning("Script %s already running.", entity_id) return await script.async_turn_on(variables=service.data, context=service.context) scripts = [] for object_id, cfg in config.get(DOMAIN, {}).items(): alias = cfg.get(CONF_ALIAS, object_id) script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE]) scripts.append(script) hass.services.async_register(DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA) # Register the service description service_desc = { CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], CONF_FIELDS: cfg[CONF_FIELDS], } async_set_service_schema(hass, DOMAIN, object_id, service_desc) await component.async_add_entities(scripts)
def discover_scripts(hass): """Discover python scripts in folder.""" path = hass.config.path(FOLDER) if not os.path.isdir(path): _LOGGER.warning("Folder %s not found in configuration folder", FOLDER) return False def python_script_service_handler(call): """Handle python script service calls.""" execute_script(hass, call.service, call.data) existing = hass.services.services.get(DOMAIN, {}).keys() for existing_service in existing: if existing_service == SERVICE_RELOAD: continue hass.services.remove(DOMAIN, existing_service) # Load user-provided service descriptions from python_scripts/services.yaml services_yaml = os.path.join(path, "services.yaml") if os.path.exists(services_yaml): services_dict = load_yaml(services_yaml) else: services_dict = {} for fil in glob.iglob(os.path.join(path, "*.py")): name = os.path.splitext(os.path.basename(fil))[0] hass.services.register(DOMAIN, name, python_script_service_handler) service_desc = { "description": services_dict.get(name, {}).get("description", ""), "fields": services_dict.get(name, {}).get("fields", {}), } async_set_service_schema(hass, DOMAIN, name, service_desc)
async def async_register_services(self) -> None: """Create or update the notify services.""" assert self.hass if hasattr(self, "targets"): stale_targets = set(self.registered_targets) # pylint: disable=no-member for name, target in self.targets.items(): # type: ignore target_name = slugify( f"{self._target_service_name_prefix}_{name}") if target_name in stale_targets: stale_targets.remove(target_name) if (target_name in self.registered_targets and target == self.registered_targets[target_name]): continue self.registered_targets[target_name] = target self.hass.services.async_register( DOMAIN, target_name, self._async_notify_message_service, schema=NOTIFY_SERVICE_SCHEMA, ) # Register the service description service_desc = { CONF_NAME: f"Send a notification via {target_name}", CONF_DESCRIPTION: f"Sends a notification message using the {target_name} integration.", CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], } async_set_service_schema(self.hass, DOMAIN, target_name, service_desc) for stale_target_name in stale_targets: del self.registered_targets[stale_target_name] self.hass.services.async_remove( DOMAIN, stale_target_name, ) if self.hass.services.has_service(DOMAIN, self._service_name): return self.hass.services.async_register( DOMAIN, self._service_name, self._async_notify_message_service, schema=NOTIFY_SERVICE_SCHEMA, ) # Register the service description service_desc = { CONF_NAME: f"Send a notification with {self._service_name}", CONF_DESCRIPTION: f"Sends a notification message using the {self._service_name} service.", CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], } async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc)
async def _async_process_config(hass, config, component): """Process script configuration.""" async def service_handler(service): """Execute a service call to script.<script name>.""" entity_id = ENTITY_ID_FORMAT.format(service.service) script_entity = component.get_entity(entity_id) if script_entity.script.is_legacy and script_entity.is_on: _LOGGER.warning("Script %s already running.", entity_id) return await script_entity.async_turn_on(variables=service.data, context=service.context) script_entities = [] for object_id, cfg in config.get(DOMAIN, {}).items(): script_entities.append( ScriptEntity( hass, object_id, cfg.get(CONF_ALIAS, object_id), cfg.get(CONF_ICON), cfg[CONF_SEQUENCE], cfg[CONF_MODE], cfg.get(CONF_QUEUE_MAX, 0), )) await component.async_add_entities(script_entities) # Register services for all entities that were created successfully. for script_entity in script_entities: object_id = script_entity.object_id if component.get_entity(script_entity.entity_id) is None: _LOGGER.error("Couldn't load script %s", object_id) continue cfg = config[DOMAIN][object_id] hass.services.async_register(DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA) # Register the service description service_desc = { CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], CONF_FIELDS: cfg[CONF_FIELDS], } async_set_service_schema(hass, DOMAIN, object_id, service_desc)
async def _register_services(hass, target_name, coordinator): async def _async_trigger_service(service: ServiceCall): _LOGGER.info("Multiscrape triggered by service: %s", service.__repr__()) await coordinator.async_request_refresh() hass.services.async_register( DOMAIN, f"trigger_{target_name}", _async_trigger_service, schema=vol.Schema({}), ) # Register the service description service_desc = { CONF_NAME: f"Trigger an update of {target_name}", CONF_DESCRIPTION: f"Triggers an update for the multiscrape {target_name} integration, independent of the update interval.", CONF_FIELDS: {}, } async_set_service_schema(hass, DOMAIN, target_name, service_desc)
async def _register_service(hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService) -> None: if entry_data.device_info is None: raise ValueError("Device Info needs to be fetched first") service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} fields = {} for arg in service.args: if arg.type not in ARG_TYPE_METADATA: _LOGGER.error( "Can't register service %s because %s is of unknown type %s", service_name, arg.name, arg.type, ) return metadata = ARG_TYPE_METADATA[arg.type] schema[vol.Required(arg.name)] = metadata.validator fields[arg.name] = { "name": arg.name, "required": True, "description": metadata.description, "example": metadata.example, "selector": metadata.selector, } async def execute_service(call: ServiceCall) -> None: await entry_data.client.execute_service(service, call.data ) # type: ignore[arg-type] hass.services.async_register(DOMAIN, service_name, execute_service, vol.Schema(schema)) service_desc = { "description": f"Calls the service {service.name} of the node {entry_data.device_info.name}", "fields": fields, } async_set_service_schema(hass, DOMAIN, service_name, service_desc)
async def _async_process_config(hass, config, component): """Process script configuration.""" async def service_handler(service): """Execute a service call to script.<script name>.""" entity_id = ENTITY_ID_FORMAT.format(service.service) script_entity = component.get_entity(entity_id) await script_entity.async_turn_on( variables=service.data, context=service.context ) script_entities = [ ScriptEntity(hass, object_id, cfg, cfg.raw_config) for object_id, cfg in config.get(DOMAIN, {}).items() ] await component.async_add_entities(script_entities) # Register services for all entities that were created successfully. for script_entity in script_entities: object_id = script_entity.object_id if component.get_entity(script_entity.entity_id) is None: _LOGGER.error("Couldn't load script %s", object_id) continue cfg = config[DOMAIN][object_id] hass.services.async_register( DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA ) # Register the service description service_desc = { CONF_NAME: script_entity.name, CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], CONF_FIELDS: cfg[CONF_FIELDS], } async_set_service_schema(hass, DOMAIN, object_id, service_desc)
async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" if p_config is None: p_config = {} platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, "async_get_engine"): provider = await platform.async_get_engine( hass, p_config, discovery_info ) else: provider = await hass.async_add_executor_job( platform.get_engine, hass, p_config, discovery_info ) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) return tts.async_register_engine(p_type, provider, p_config) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error setting up platform: %s", p_type) return async def async_say_handle(service): """Service handle for say.""" entity_ids = service.data[ATTR_ENTITY_ID] message = service.data.get(ATTR_MESSAGE) cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) options = service.data.get(ATTR_OPTIONS) try: url = await tts.async_get_url_path( p_type, message, cache=cache, language=language, options=options ) except HomeAssistantError as err: _LOGGER.error("Error on init TTS: %s", err) return base = tts.base_url or get_url(hass) url = base + url data = { ATTR_MEDIA_CONTENT_ID: url, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, ATTR_ENTITY_ID: entity_ids, } await hass.services.async_call( DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service.context, ) service_name = p_config.get(CONF_SERVICE_NAME, f"{p_type}_{SERVICE_SAY}") hass.services.async_register( DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY ) # Register the service description service_desc = { CONF_NAME: f"Say an TTS message with {p_type}", CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], } async_set_service_schema(hass, DOMAIN, service_name, service_desc)
async def trigger_init(self, func): """Initialize any decorator triggers for a newly defined function.""" func_name = func.get_name() trig_args = {} got_reqd_dec = False trig_decorators_reqd = { "time_trigger", "state_trigger", "event_trigger", } trig_decorators = { "time_trigger", "state_trigger", "event_trigger", "state_active", "time_active", } decorator_used = set() for dec in func.get_decorators(): dec_name, dec_args = dec[0], dec[1] if dec_name in decorator_used: self.logger.error( "%s defined in %s: decorator %s repeated; ignored", func_name, self.name, dec_name, ) continue decorator_used.add(dec_name) if dec_name in trig_decorators_reqd: got_reqd_dec = True if dec_name in trig_decorators: if dec_name not in trig_args: trig_args[dec_name] = [] if dec_args is not None: trig_args[dec_name] += dec_args elif dec_name == "service": if dec_args is not None: self.logger.error( "%s defined in %s: decorator @service takes no arguments; ignored", func_name, self.name, ) continue if func_name in (SERVICE_RELOAD, SERVICE_JUPYTER_KERNEL_START): self.logger.error( "function '%s' in %s with @service conflicts with builtin service; ignoring (please rename function)", func_name, self.name, ) return desc = func.get_doc_string() if desc is None or desc == "": desc = f"pyscript function {func_name}()" desc = desc.lstrip(" \n\r") if desc.startswith("yaml"): try: desc = desc[4:].lstrip(" \n\r") file_desc = io.StringIO(desc) service_desc = (yaml.load(file_desc, Loader=yaml.BaseLoader) or OrderedDict()) file_desc.close() except Exception as exc: self.logger.error( "Unable to decode yaml doc_string for %s(): %s", func_name, str(exc)) raise HomeAssistantError(exc) else: fields = OrderedDict() for arg in func.get_positional_args(): fields[arg] = OrderedDict( description=f"argument {arg}") service_desc = {"description": desc, "fields": fields} def pyscript_service_factory(func_name, func): async def pyscript_service_handler(call): """Handle python script service calls.""" # self.logger.debug("service call to %s", func_name) # # use a new AstEval context so it can run fully independently # of other instances (except for global_ctx which is common) # ast_ctx = AstEval( f"{self.name}.{func_name}", global_ctx=self, state_func=self.state_func, event_func=self.event_func, handler_func=self.handler_func, ) self.handler_func.install_ast_funcs(ast_ctx) func_args = { "trigger_type": "service", } func_args.update(call.data) async def do_service_call(func, ast_ctx, data): await func.call(ast_ctx, [], call.data) if ast_ctx.get_exception_obj(): ast_ctx.get_logger().error( ast_ctx.get_exception_long()) self.handler_func.create_task( do_service_call(func, ast_ctx, func_args)) return pyscript_service_handler self.hass.services.async_register( DOMAIN, func_name, pyscript_service_factory(func_name, func), ) async_set_service_schema(self.hass, DOMAIN, func_name, service_desc) self.services.add(func_name) else: self.logger.warning( "%s defined in %s: unknown decorator @%s: ignored", func_name, self.name, dec_name, ) if func_name in self.services and "service" not in decorator_used: # function redefined without @service, so remove it self.hass.services.async_remove(DOMAIN, func_name) self.services.discard(func_name) for dec_name in trig_decorators: if dec_name in trig_args and len(trig_args[dec_name]) == 0: trig_args[dec_name] = None arg_check = { "state_trigger": {1}, "state_active": {1}, "event_trigger": {1, 2}, } for dec_name, arg_cnt in arg_check.items(): if dec_name not in trig_args or trig_args[dec_name] is None: continue if len(trig_args[dec_name]) not in arg_cnt: self.logger.error( "%s defined in %s: decorator @%s got %d argument%s, expected %s; ignored", func_name, self.name, dec_name, len(trig_args[dec_name]), "s" if len(trig_args[dec_name]) > 1 else "", " or ".join([str(cnt) for cnt in sorted(arg_cnt)]), ) del trig_args[dec_name] if arg_cnt == 1: trig_args[dec_name] = trig_args[dec_name][0] if not got_reqd_dec and len(trig_args) > 0: self.logger.error( "%s defined in %s: needs at least one trigger decorator (ie: %s)", func_name, self.name, ", ".join(sorted(trig_decorators_reqd)), ) return if len(trig_args) == 0: # # function defined without triggers; remove old one if necessary # if func_name in self.triggers: await self.triggers[func_name].stop() del self.triggers[func_name] if func_name in self.triggers_new: del self.triggers_new[func_name] return trig_args["action"] = func trig_args["action_ast_ctx"] = AstEval( f"{self.name}.{func_name}", global_ctx=self, state_func=self.state_func, event_func=self.event_func, handler_func=self.handler_func, ) self.handler_func.install_ast_funcs(trig_args["action_ast_ctx"]) trig_args["global_sym_table"] = self.global_sym_table if func_name in self.triggers: await self.triggers[func_name].stop() self.triggers_new[func_name] = TrigInfo( f"{self.name}.{func_name}", trig_args, event_func=self.event_func, state_func=self.state_func, handler_func=self.handler_func, trig_time=self.trig_time_func, global_ctx=self, ) if self.auto_start: await self.start()
async def async_setup_platform( p_type: str, p_config: ConfigType | None = None, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a TTS platform.""" if p_config is None: p_config = {} platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, "async_get_engine"): provider = await platform.async_get_engine( hass, p_config, discovery_info) else: provider = await hass.async_add_executor_job( platform.get_engine, hass, p_config, discovery_info) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) return tts.async_register_engine(p_type, provider, p_config) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error setting up platform: %s", p_type) return async def async_say_handle(service: ServiceCall) -> None: """Service handle for say.""" entity_ids = service.data[ATTR_ENTITY_ID] message = service.data[ATTR_MESSAGE] cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) options = service.data.get(ATTR_OPTIONS) tts.process_options(p_type, language, options) params = { "message": message, } if cache is not None: params["cache"] = "true" if cache else "false" if language is not None: params["language"] = language if options is not None: params.update(options) await hass.services.async_call( DOMAIN_MP, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: generate_media_source_id( DOMAIN, str(yarl.URL.build(path=p_type, query=params)), ), ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, ATTR_MEDIA_ANNOUNCE: True, }, blocking=True, context=service.context, ) service_name = p_config.get(CONF_SERVICE_NAME, f"{p_type}_{SERVICE_SAY}") hass.services.async_register(DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY) # Register the service description service_desc = { CONF_NAME: f"Say an TTS message with {p_type}", CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], } async_set_service_schema(hass, DOMAIN, service_name, service_desc)
async def _register_service(hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService): service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} fields = {} for arg in service.args: metadata = { UserServiceArgType.BOOL: { "validator": cv.boolean, "example": "False", "selector": { "boolean": None }, }, UserServiceArgType.INT: { "validator": vol.Coerce(int), "example": "42", "selector": { "number": { CONF_MODE: "box" } }, }, UserServiceArgType.FLOAT: { "validator": vol.Coerce(float), "example": "12.3", "selector": { "number": { CONF_MODE: "box", "step": 1e-3 } }, }, UserServiceArgType.STRING: { "validator": cv.string, "example": "Example text", "selector": { "text": None }, }, UserServiceArgType.BOOL_ARRAY: { "validator": [cv.boolean], "description": "A list of boolean values.", "example": "[True, False]", "selector": { "object": {} }, }, UserServiceArgType.INT_ARRAY: { "validator": [vol.Coerce(int)], "description": "A list of integer values.", "example": "[42, 34]", "selector": { "object": {} }, }, UserServiceArgType.FLOAT_ARRAY: { "validator": [vol.Coerce(float)], "description": "A list of floating point numbers.", "example": "[ 12.3, 34.5 ]", "selector": { "object": {} }, }, UserServiceArgType.STRING_ARRAY: { "validator": [cv.string], "description": "A list of strings.", "example": "['Example text', 'Another example']", "selector": { "object": {} }, }, }[arg.type_] schema[vol.Required(arg.name)] = metadata["validator"] fields[arg.name] = { "name": arg.name, "required": True, "description": metadata.get("description"), "example": metadata["example"], "selector": metadata["selector"], } async def execute_service(call): await entry_data.client.execute_service(service, call.data) hass.services.async_register(DOMAIN, service_name, execute_service, vol.Schema(schema)) service_desc = { "description": f"Calls the service {service.name} of the node {entry_data.device_info.name}", "fields": fields, } async_set_service_schema(hass, DOMAIN, service_name, service_desc)
async def _async_process_config(hass, config, component) -> bool: """Process script configuration. Return true, if Blueprints were used. """ entities = [] blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] for object_id, config_block in conf.items(): raw_blueprint_inputs = None raw_config = None if isinstance(config_block, BlueprintInputs): blueprints_used = True blueprint_inputs = config_block raw_blueprint_inputs = blueprint_inputs.config_with_inputs try: raw_config = blueprint_inputs.async_substitute() config_block = cast( Dict[str, Any], await async_validate_config_item(hass, raw_config), ) except vol.Invalid as err: LOGGER.error( "Blueprint %s generated invalid script with input %s: %s", blueprint_inputs.blueprint.name, blueprint_inputs.inputs, humanize_error(config_block, err), ) continue else: raw_config = cast(ScriptConfig, config_block).raw_config entities.append( ScriptEntity(hass, object_id, config_block, raw_config, raw_blueprint_inputs)) await component.async_add_entities(entities) async def service_handler(service): """Execute a service call to script.<script name>.""" entity_id = ENTITY_ID_FORMAT.format(service.service) script_entity = component.get_entity(entity_id) await script_entity.async_turn_on(variables=service.data, context=service.context) # Register services for all entities that were created successfully. for entity in entities: hass.services.async_register(DOMAIN, entity.object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA) # Register the service description service_desc = { CONF_NAME: entity.name, CONF_DESCRIPTION: entity.description, CONF_FIELDS: entity.fields, } async_set_service_schema(hass, DOMAIN, entity.object_id, service_desc) return blueprints_used