From f5e973a672b2b0e129048f12cb929a50e7dd5678 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:15:41 +0100 Subject: [PATCH 1/2] Update code sample for action-setup rule --- .../rules/action-setup.md | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/core/integration-quality-scale/rules/action-setup.md b/docs/core/integration-quality-scale/rules/action-setup.md index a532d3b7033..f7664c721ab 100644 --- a/docs/core/integration-quality-scale/rules/action-setup.md +++ b/docs/core/integration-quality-scale/rules/action-setup.md @@ -24,24 +24,36 @@ This is used to first fetch the configuration entry, and then check if it is loa If the configuration entry does not exist or the configuration entry that we found is not loaded, we raise a relevant error which is shown to the user. Supply description placeholders to enable translation of service parameters, for example, to reference external resources like documentation URLs that need to be localized or updated independently of the service description. -`__init__py`: -```python {13-20} showLineNumbers +`__init__.py`: +```python {6} showLineNumbers +from .services import async_setup_services + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up my integration.""" - async def async_get_schedule(call: ServiceCall) -> ServiceResponse: - """Get the schedule for a specific range.""" - if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])): - raise ServiceValidationError("Entry not found") - if entry.state is not ConfigEntryState.LOADED: - raise ServiceValidationError("Entry not loaded") - client = cast(MyConfigEntry, entry).runtime_data - ... + async_setup_services(hass) + return True +``` + +`services.py`: +```python {14-21} showLineNumbers +async def _async_get_schedule(call: ServiceCall) -> ServiceResponse: + """Get the schedule for a specific range.""" + if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])): + raise ServiceValidationError("Entry not found") + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError("Entry not loaded") + client = cast(MyConfigEntry, entry).runtime_data + ... + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register the services.""" hass.services.async_register( DOMAIN, SERVICE_GET_SCHEDULE, - async_get_schedule, + _async_get_schedule, schema=SERVICE_GET_SCHEDULE_SCHEMA, supports_response=SupportsResponse.ONLY, description_placeholders={"example_url": "https://schedule.example.com"} @@ -58,4 +70,4 @@ There are no exceptions to this rule. ## Related rules - \ No newline at end of file + From 6db3f12eadf82a4649df95b4054f2e24336adab9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Dec 2025 09:25:42 +0100 Subject: [PATCH 2/2] Update common-modules --- .../rules/common-modules.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/core/integration-quality-scale/rules/common-modules.md b/docs/core/integration-quality-scale/rules/common-modules.md index e03a90d0042..96e4021291d 100644 --- a/docs/core/integration-quality-scale/rules/common-modules.md +++ b/docs/core/integration-quality-scale/rules/common-modules.md @@ -13,6 +13,10 @@ The second common pattern is the base entity. Since a lot of integrations provide more types of entities, a base entity can prove useful to reduce code duplication. The base entity should be placed in `entity.py`. +The third common pattern relates to action definition and registration. +Placing these in a separate module helps improve code clarity and separation of concern. +The action definition and registration should be placed in `services.py`. + The efforts done to increase consistency between integrations have a positive impact on the quality of the codebase and the developer experience. ## Example implementation @@ -48,6 +52,32 @@ class MyEntity(CoordinatorEntity[MyCoordinator]): self._attr_device_info = ... ``` +`services.py` +```python showLineNumbers +async def _async_get_schedule(call: ServiceCall) -> ServiceResponse: + """Get the schedule for a specific range.""" + if not (entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY_ID])): + raise ServiceValidationError("Entry not found") + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError("Entry not loaded") + client = cast(MyConfigEntry, entry).runtime_data + ... + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register the services.""" + + hass.services.async_register( + DOMAIN, + SERVICE_GET_SCHEDULE, + async_get_schedule, + _async_get_schedule, + schema=SERVICE_GET_SCHEDULE_SCHEMA, + supports_response=SupportsResponse.ONLY, + description_placeholders={"example_url": "https://schedule.example.com"} + ) +``` + ## Exceptions There are no exceptions to this rule.