diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 6d066f86072..56c686df6b4 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -9,13 +9,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from .const import DOMAIN from .coordinator import ScreenlogicDataUpdateCoordinator, async_get_connect_info from .data import ENTITY_MIGRATIONS -from .services import async_load_screenlogic_services, async_unload_screenlogic_services +from .services import async_load_screenlogic_services from .util import generate_unique_id _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,16 @@ PLATFORMS = [ Platform.SWITCH, ] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Screenlogic.""" + + async_load_screenlogic_services(hass) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" @@ -62,8 +73,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - async_load_screenlogic_services(hass, entry) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -77,8 +86,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.gateway.async_disconnect() hass.data[DOMAIN].pop(entry.entry_id) - async_unload_screenlogic_services(hass) - return unload_ok diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index 3125f52989e..104736f300b 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -20,6 +20,8 @@ DOMAIN = "screenlogic" DEFAULT_SCAN_INTERVAL = 30 MIN_SCAN_INTERVAL = 10 +ATTR_CONFIG_ENTRY = "config_entry" + SERVICE_SET_COLOR_MODE = "set_color_mode" ATTR_COLOR_MODE = "color_mode" SUPPORTED_COLOR_MODES = {slugify(cm.name): cm.value for cm in COLOR_MODE} diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index 2c8e786491c..116a66d97df 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -6,14 +6,19 @@ from screenlogicpy import ScreenLogicError from screenlogicpy.device_const.system import EQUIPMENT_FLAG import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import ( + config_validation as cv, + issue_registry as ir, + selector, +) from homeassistant.helpers.service import async_extract_config_entry_ids from .const import ( ATTR_COLOR_MODE, + ATTR_CONFIG_ENTRY, ATTR_RUNTIME, DOMAIN, MAX_RUNTIME, @@ -27,44 +32,103 @@ from .coordinator import ScreenlogicDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -SET_COLOR_MODE_SCHEMA = cv.make_entity_service_schema( +BASE_SERVICE_SCHEMA = vol.Schema( { - vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), - }, + vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ) + } ) -TURN_ON_SUPER_CHLOR_SCHEMA = cv.make_entity_service_schema( +SET_COLOR_MODE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), + } + ), + cv.has_at_least_one_key(ATTR_CONFIG_ENTRY, *cv.ENTITY_SERVICE_FIELDS), +) + +TURN_ON_SUPER_CHLOR_SCHEMA = BASE_SERVICE_SCHEMA.extend( { - vol.Optional(ATTR_RUNTIME, default=24): vol.Clamp( - min=MIN_RUNTIME, max=MAX_RUNTIME + vol.Optional(ATTR_RUNTIME, default=24): vol.All( + vol.Coerce(int), vol.Clamp(min=MIN_RUNTIME, max=MAX_RUNTIME) ), } ) @callback -def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): +def async_load_screenlogic_services(hass: HomeAssistant): """Set up services for the ScreenLogic integration.""" async def extract_screenlogic_config_entry_ids(service_call: ServiceCall): if not ( - screenlogic_entry_ids := [ - entry_id - for entry_id in await async_extract_config_entry_ids(hass, service_call) - if (entry := hass.config_entries.async_get_entry(entry_id)) - and entry.domain == DOMAIN - ] + screenlogic_entry_ids := await async_extract_config_entry_ids( + hass, service_call + ) ): - raise HomeAssistantError( - f"Failed to call service '{service_call.service}'. Config entry for" - " target not found" + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry for " + "target not found" ) return screenlogic_entry_ids + async def get_coordinators( + service_call: ServiceCall, + ) -> list[ScreenlogicDataUpdateCoordinator]: + entry_ids: set[str] + if entry_id := service_call.data.get(ATTR_CONFIG_ENTRY): + entry_ids = {entry_id} + else: + ir.async_create_issue( + hass, + DOMAIN, + "service_target_deprecation", + breaks_in_ha_version="2024.8.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_target_deprecation", + ) + entry_ids = await extract_screenlogic_config_entry_ids(service_call) + + coordinators: list[ScreenlogicDataUpdateCoordinator] = [] + for entry_id in entry_ids: + config_entry: ConfigEntry | None = hass.config_entries.async_get_entry( + entry_id + ) + if not config_entry: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' not found" + ) + if not config_entry.domain == DOMAIN: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' is not a {DOMAIN} config" + ) + if not config_entry.state == ConfigEntryState.LOADED: + raise ServiceValidationError( + f"Failed to call service '{service_call.service}'. Config entry " + f"'{entry_id}' not loaded" + ) + coordinators.append(hass.data[DOMAIN][entry_id]) + + return coordinators + async def async_set_color_mode(service_call: ServiceCall) -> None: color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] - for entry_id in await extract_screenlogic_config_entry_ids(service_call): - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + coordinator: ScreenlogicDataUpdateCoordinator + for coordinator in await get_coordinators(service_call): _LOGGER.debug( "Service %s called on %s with mode %s", SERVICE_SET_COLOR_MODE, @@ -83,13 +147,19 @@ def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): is_on: bool, runtime: int | None = None, ) -> None: - for entry_id in await extract_screenlogic_config_entry_ids(service_call): - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry_id] + coordinator: ScreenlogicDataUpdateCoordinator + for coordinator in await get_coordinators(service_call): + if EQUIPMENT_FLAG.CHLORINATOR not in coordinator.gateway.equipment_flags: + raise ServiceValidationError( + f"Equipment configuration for {coordinator.gateway.name} does not" + f" support {service_call.service}" + ) + rt_log = f" with runtime {runtime}" if runtime else "" _LOGGER.debug( - "Service %s called on %s with runtime %s", - SERVICE_START_SUPER_CHLORINATION, + "Service %s called on %s%s", + service_call.service, coordinator.gateway.name, - runtime, + rt_log, ) try: await coordinator.gateway.async_set_scg_config( @@ -107,43 +177,20 @@ def async_load_screenlogic_services(hass: HomeAssistant, entry: ConfigEntry): async def async_stop_super_chlor(service_call: ServiceCall) -> None: await async_set_super_chlor(service_call, False) - if not hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): - hass.services.async_register( - DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA - ) + hass.services.async_register( + DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA + ) - coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - equipment_flags = coordinator.gateway.equipment_flags + hass.services.async_register( + DOMAIN, + SERVICE_START_SUPER_CHLORINATION, + async_start_super_chlor, + TURN_ON_SUPER_CHLOR_SCHEMA, + ) - if EQUIPMENT_FLAG.CHLORINATOR in equipment_flags: - if not hass.services.has_service(DOMAIN, SERVICE_START_SUPER_CHLORINATION): - hass.services.async_register( - DOMAIN, - SERVICE_START_SUPER_CHLORINATION, - async_start_super_chlor, - TURN_ON_SUPER_CHLOR_SCHEMA, - ) - - if not hass.services.has_service(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION): - hass.services.async_register( - DOMAIN, - SERVICE_STOP_SUPER_CHLORINATION, - async_stop_super_chlor, - ) - - -@callback -def async_unload_screenlogic_services(hass: HomeAssistant): - """Unload services for the ScreenLogic integration.""" - - if not hass.data[DOMAIN]: - _LOGGER.debug("Unloading all ScreenLogic services") - for service in hass.services.async_services_for_domain(DOMAIN): - hass.services.async_remove(DOMAIN, service) - elif not any( - EQUIPMENT_FLAG.CHLORINATOR in coordinator.gateway.equipment_flags - for coordinator in hass.data[DOMAIN].values() - ): - _LOGGER.debug("Unloading ScreenLogic chlorination services") - hass.services.async_remove(DOMAIN, SERVICE_START_SUPER_CHLORINATION) - hass.services.async_remove(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION) + hass.services.async_register( + DOMAIN, + SERVICE_STOP_SUPER_CHLORINATION, + async_stop_super_chlor, + BASE_SERVICE_SCHEMA, + ) diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml index 7b51d1a21db..f05537640ca 100644 --- a/homeassistant/components/screenlogic/services.yaml +++ b/homeassistant/components/screenlogic/services.yaml @@ -1,9 +1,11 @@ # ScreenLogic Services set_color_mode: - target: - device: - integration: screenlogic fields: + config_entry: + required: false + selector: + config_entry: + integration: screenlogic color_mode: required: true selector: @@ -32,10 +34,12 @@ set_color_mode: - thumper - white start_super_chlorination: - target: - device: - integration: screenlogic fields: + config_entry: + required: true + selector: + config_entry: + integration: screenlogic runtime: default: 24 selector: @@ -45,6 +49,9 @@ start_super_chlorination: unit_of_measurement: hours mode: slider stop_super_chlorination: - target: - device: - integration: screenlogic + fields: + config_entry: + required: true + selector: + config_entry: + integration: screenlogic diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index fcddbc1d415..755eeb4ffb2 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -41,6 +41,10 @@ "name": "Set Color Mode", "description": "Sets the color mode for all color-capable lights attached to this ScreenLogic gateway.", "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + }, "color_mode": { "name": "Color Mode", "description": "The ScreenLogic color mode to set." @@ -51,6 +55,10 @@ "name": "Start Super Chlorination", "description": "Begins super chlorination, running for the specified period or 24 hours if none is specified.", "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + }, "runtime": { "name": "Run Time", "description": "Number of hours for super chlorination to run." @@ -59,7 +67,26 @@ }, "stop_super_chlorination": { "name": "Stop Super Chlorination", - "description": "Stops super chlorination." + "description": "Stops super chlorination.", + "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service." + } + } + } + }, + "issues": { + "service_target_deprecation": { + "title": "Deprecating use of target for ScreenLogic services", + "fix_flow": { + "step": { + "confirm": { + "title": "Deprecating target for ScreenLogic services", + "description": "Use of an Area, Device, or Entity as a target for ScreenLogic services is being deprecated. Instead, use `config_entry` with the entry_id of the desired ScreenLogic integration.\n\nPlease update your automations and scripts and select **submit** to fix this issue." + } + } + } } } }