mirror of
https://github.com/home-assistant/core.git
synced 2026-06-03 18:03:43 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53211759cb | |||
| 4593059db2 | |||
| 593ae9eb80 | |||
| 37b4bcaa39 | |||
| 6bda3ea3a5 | |||
| f4db5fb346 |
@@ -8,39 +8,8 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
|
||||
## Follow these steps:
|
||||
1. Use 'gh pr view' to get the PR details and description.
|
||||
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||
3. Analyze the code changes for:
|
||||
- Code quality and style consistency
|
||||
- Potential bugs or issues
|
||||
- Performance implications
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
- Documentation updates if needed
|
||||
4. Ensure any existing review comments have been addressed.
|
||||
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
|
||||
|
||||
## Verification:
|
||||
|
||||
- After the review, run parallel subagents for each finding to double check it.
|
||||
- Spawn up to a maximum of 10 parallel subagents at a time.
|
||||
- Gather the results from the subagents and summarize them in the final review comments.
|
||||
|
||||
3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions.
|
||||
4. Check if all existing review comments have been addressed.
|
||||
|
||||
## IMPORTANT:
|
||||
- Just review. DO NOT make any changes
|
||||
- Be constructive and specific in your comments
|
||||
- Suggest improvements where appropriate
|
||||
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||
- No need to run tests or linters, just review the code changes.
|
||||
- No need to highlight things that are already good.
|
||||
|
||||
## Output format:
|
||||
- List specific comments for each file/line that needs attention.
|
||||
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||
- Example output:
|
||||
```
|
||||
Overall assessment: request changes.
|
||||
- [CRITICAL] sensor.py:143 - Memory leak
|
||||
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
|
||||
- [SUGGESTION] test_init.py:45 - Improve x variable name
|
||||
```
|
||||
- Make sure to include the file and line number when possible in the bullet points.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: review
|
||||
description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests.
|
||||
---
|
||||
|
||||
# Review Code Changes
|
||||
|
||||
## Analyze the code changes for:
|
||||
- Code quality and style consistency
|
||||
- Potential bugs or issues
|
||||
- Performance implications
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
- Documentation updates if needed
|
||||
|
||||
## Verification:
|
||||
- After the review, run parallel subagents for each finding to double-check it.
|
||||
- Spawn up to a maximum of 10 parallel subagents at a time.
|
||||
- Gather the results from the subagents and summarize them in the final review comments.
|
||||
|
||||
## IMPORTANT:
|
||||
- Just review. DO NOT make any changes.
|
||||
- Be constructive and specific in your comments.
|
||||
- Suggest improvements where appropriate.
|
||||
- No need to run tests or linters, just review the code changes.
|
||||
- No need to highlight things that are already good.
|
||||
|
||||
## Output format:
|
||||
- List specific comments for each file/line that needs attention.
|
||||
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||
- Example output:
|
||||
```
|
||||
Overall assessment: request changes.
|
||||
- [CRITICAL] sensor.py:143 - Memory leak
|
||||
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
|
||||
- [SUGGESTION] test_init.py:45 - Improve x variable name
|
||||
```
|
||||
- Make sure to include the file and line number when possible in the bullet points.
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
import avea
|
||||
|
||||
from homeassistant.components.bluetooth import async_ble_device_from_address
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothReachabilityIntent,
|
||||
async_address_reachability_diagnostics,
|
||||
async_ble_device_from_address,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type AveaConfigEntry = ConfigEntry[avea.Bulb]
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
@@ -15,12 +21,20 @@ PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AveaConfigEntry) -> bool:
|
||||
"""Set up Avea from a config entry."""
|
||||
ble_device = async_ble_device_from_address(
|
||||
hass, entry.data[CONF_ADDRESS], connectable=True
|
||||
)
|
||||
address = entry.data[CONF_ADDRESS]
|
||||
ble_device = async_ble_device_from_address(hass, address, connectable=True)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Avea device with address {entry.data[CONF_ADDRESS]}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={
|
||||
"address": address,
|
||||
"reason": async_address_reachability_diagnostics(
|
||||
hass,
|
||||
address.upper(),
|
||||
BluetoothReachabilityIntent.CONNECTION,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
entry.runtime_data = avea.Bulb(ble_device)
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_not_found": {
|
||||
"message": "Could not find Avea device with address {address}: {reason}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "[%key:component::homeassistant::issues::deprecated_yaml::description%]",
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260527.3"]
|
||||
"requirements": ["home-assistant-frontend==20260527.4"]
|
||||
}
|
||||
|
||||
@@ -1027,14 +1027,53 @@ async def handle_test_condition(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle test condition command."""
|
||||
# Do static + dynamic validation of the condition
|
||||
config = await async_validate_condition_config(hass, msg["condition"])
|
||||
# Test the condition
|
||||
condition = await async_condition_from_config(hass, config)
|
||||
# Validating and instantiating the condition can fail on bad user input.
|
||||
# Handle those errors here so they are reported to the client without being
|
||||
# logged as unexpected errors by the default websocket error handler.
|
||||
try:
|
||||
connection.send_result(
|
||||
msg["id"], {"result": condition.async_check(variables=msg.get("variables"))}
|
||||
# Do static + dynamic validation of the condition
|
||||
config = await async_validate_condition_config(hass, msg["condition"])
|
||||
condition = await async_condition_from_config(hass, config)
|
||||
except vol.Invalid as err:
|
||||
connection.send_error(msg["id"], const.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
except HomeAssistantError as err:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
const.ERR_HOME_ASSISTANT_ERROR,
|
||||
str(err),
|
||||
translation_domain=err.translation_domain,
|
||||
translation_key=err.translation_key,
|
||||
translation_placeholders=err.translation_placeholders,
|
||||
)
|
||||
return
|
||||
|
||||
# Template errors (e.g. undefined variables) are recorded in the trace
|
||||
# instead of being logged. Capture the trace and forward them to the client
|
||||
# alongside the result.
|
||||
condition_trace = trace.trace_get()
|
||||
try:
|
||||
with trace.record_template_errors():
|
||||
check_result = condition.async_check(variables=msg.get("variables"))
|
||||
except HomeAssistantError as err:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
const.ERR_HOME_ASSISTANT_ERROR,
|
||||
str(err),
|
||||
translation_domain=err.translation_domain,
|
||||
translation_key=err.translation_key,
|
||||
translation_placeholders=err.translation_placeholders,
|
||||
)
|
||||
else:
|
||||
result: dict[str, Any] = {"result": check_result}
|
||||
if template_errors := [
|
||||
template_error
|
||||
for elements in condition_trace.values()
|
||||
for element in elements
|
||||
for template_error in element.template_errors
|
||||
]:
|
||||
result["template_errors"] = template_errors
|
||||
connection.send_result(msg["id"], result)
|
||||
finally:
|
||||
condition.async_unload()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.8.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260527.3
|
||||
home-assistant-frontend==20260527.4
|
||||
home-assistant-intents==2026.6.1
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -99,11 +99,31 @@ Every check has a code following the
|
||||
| `W7407` | [`home-assistant-config-flow-polling-field`](#w7407-home-assistant-config-flow-polling-field) | Config flow should not include polling interval fields |
|
||||
| `W7408` | [`home-assistant-config-flow-name-field`](#w7408-home-assistant-config-flow-name-field) | Config flow should not include name fields |
|
||||
| `R7402` | [`home-assistant-unused-test-fixture-argument`](#r7402-home-assistant-unused-test-fixture-argument) | Unused test function argument should use `@pytest.mark.usefixtures` |
|
||||
| `C7415` | [`home-assistant-domain-argument`](#c7415-home-assistant-domain-argument) | Domain argument in tests should be a domain constant or variable |
|
||||
| `W7418` | [`home-assistant-tests-direct-async-setup-entry`](#w7418-home-assistant-tests-direct-async-setup-entry) | Tests should not call an integration's `async_setup_entry` directly |
|
||||
| `W7420` | [`home-assistant-tests-direct-platform-async-setup-entry`](#w7420-home-assistant-tests-direct-platform-async-setup-entry) | Tests should not call a platform's `async_setup_entry` directly |
|
||||
| `W7421` | [`home-assistant-tests-direct-async-migrate-entry`](#w7421-home-assistant-tests-direct-async-migrate-entry) | Tests should not call an integration's `async_migrate_entry` directly |
|
||||
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
|
||||
| `C7414` | [`home-assistant-enforce-utcnow`](#c7414-home-assistant-enforce-utcnow) | Use `homeassistant.util.dt.utcnow` instead of `datetime.now(UTC)` |
|
||||
| `C7412` | [`home-assistant-entity-description-redundant-default`](#c7412-home-assistant-entity-description-redundant-default) | Setting an EntityDescription field to its default value is redundant |
|
||||
| `C7413` | [`home-assistant-duplicate-const`](#c7413-home-assistant-duplicate-const) | Constant duplicates one in `homeassistant.const` with the same value |
|
||||
| `E7405` | [`home-assistant-action-swallowed-exception`](#e7405-home-assistant-action-swallowed-exception) | Action handler must not swallow exceptions |
|
||||
| `W7414` | [`home-assistant-service-registered-in-setup-entry`](#w7414-home-assistant-service-registered-in-setup-entry) | Services should be registered in `async_setup`, not `async_setup_entry` |
|
||||
| `W7417` | [`home-assistant-exception-not-translated`](#w7417-home-assistant-exception-not-translated) | `HomeAssistantError` should use `translation_key`/`translation_domain` |
|
||||
| `W7419` | [`home-assistant-exception-message-with-translation`](#w7419-home-assistant-exception-message-with-translation) | Don't pass a positional message when `translation_key` is set |
|
||||
| `E7406` | [`home-assistant-exception-translation-key-missing`](#e7406-home-assistant-exception-translation-key-missing) | Translation key not found in `strings.json` exceptions section |
|
||||
| `E7408` | [`home-assistant-exception-translation-key-domain-mismatch`](#e7408-home-assistant-exception-translation-key-domain-mismatch) | Only one of `translation_key` / `translation_domain` is set |
|
||||
| `E7418` | [`home-assistant-exception-placeholder-mismatch`](#e7418-home-assistant-exception-placeholder-mismatch) | Translation placeholders in code don't match `strings.json` |
|
||||
| `E7409` | [`home-assistant-mdi-icon-not-found`](#e7409-home-assistant-mdi-icon-not-found) | MDI icon string does not exist in the Material Design Icons set |
|
||||
| `E7410` | [`home-assistant-mdi-icon-json-not-found`](#e7410-home-assistant-mdi-icon-json-not-found) | MDI icon in `icons.json` does not exist in the Material Design Icons set |
|
||||
| `R7403` | [`home-assistant-tests-redundant-usefixtures`](#r7403-home-assistant-tests-redundant-usefixtures) | `@pytest.mark.usefixtures` redundant when `pytestmark` already applies it |
|
||||
| `W7409` | [`home-assistant-test-non-deterministic`](#w7409-home-assistant-test-non-deterministic) | Test contains `if`/`match` creating non-deterministic execution |
|
||||
| `W7410` | [`home-assistant-missing-reauthentication-flow`](#w7410-home-assistant-missing-reauthentication-flow) | Config flow should implement `async_step_reauth` |
|
||||
| `W7411` | [`home-assistant-missing-parallel-updates`](#w7411-home-assistant-missing-parallel-updates) | Platform module should define `PARALLEL_UPDATES` |
|
||||
| `W7412` | [`home-assistant-missing-diagnostics`](#w7412-home-assistant-missing-diagnostics) | Integration diagnostics module should implement a diagnostics function |
|
||||
| `W7413` | [`home-assistant-missing-config-entry-unloading`](#w7413-home-assistant-missing-config-entry-unloading) | Integration should implement `async_unload_entry` |
|
||||
| `W7415` | [`home-assistant-sequential-executor-jobs`](#w7415-home-assistant-sequential-executor-jobs) | Sequential `async_add_executor_job` calls should be grouped |
|
||||
| `W7416` | [`home-assistant-missing-has-entity-name`](#w7416-home-assistant-missing-has-entity-name) | Entity class should set `_attr_has_entity_name = True` |
|
||||
|
||||
|
||||
## `home_assistant_logger` checker
|
||||
@@ -346,6 +366,27 @@ only needed for its side effects.
|
||||
This rule only applies to `test_*` functions, not to fixture functions.
|
||||
|
||||
|
||||
## `home_assistant_domain_constant` checker
|
||||
|
||||
Encourages using `DOMAIN` constants (or variables) when passing a domain to common test helpers.
|
||||
String literals are allowed for cases where the constant is not imported.
|
||||
Only runs on test modules.
|
||||
|
||||
### `C7415`: `home-assistant-domain-argument`
|
||||
|
||||
The domain (or handler) argument to test helpers such as
|
||||
`async_setup_component`, `async_mock_service`, `MockConfigEntry`,
|
||||
`hass.services.async_call`, `hass.services.call`, and
|
||||
`hass.config_entries.flow.async_init` should use a domain constant or variable when available.
|
||||
The following are accepted:
|
||||
|
||||
* a `DOMAIN`/`domain` attribute or one ending in `_DOMAIN`/`_domain`
|
||||
(e.g. `sensor.DOMAIN`),
|
||||
* a `DOMAIN`/`domain` name or one ending in `_DOMAIN`/`_domain`,
|
||||
* a string literal (for cases where the constant is not imported),
|
||||
* a subscript expression (e.g. `data["key"]`).
|
||||
|
||||
|
||||
## `home_assistant_tests_direct_async_setup_entry` checker
|
||||
|
||||
Detects tests that call an integration's `async_setup_entry` directly.
|
||||
@@ -413,3 +454,204 @@ The helper is implemented as
|
||||
`functools.partial(datetime.datetime.now, UTC)` and avoids the global
|
||||
lookup of `UTC` on every call, while keeping the codebase consistent in
|
||||
how the current UTC time is obtained.
|
||||
|
||||
|
||||
## `home_assistant_entity_description_defaults` checker
|
||||
|
||||
Detects fields in `EntityDescription` (and subclasses) that are explicitly
|
||||
set to their default value.
|
||||
|
||||
### `C7412`: `home-assistant-entity-description-redundant-default`
|
||||
|
||||
An EntityDescription field is set equal to a default already declared
|
||||
anywhere in the class hierarchy; the assignment can be removed. Only the
|
||||
literal defaults `None`, `True`, and `False` are checked; other default
|
||||
values are not flagged.
|
||||
|
||||
|
||||
## `home_assistant_duplicate_const` checker
|
||||
|
||||
Detects constants in integration modules that duplicate one already exported
|
||||
from `homeassistant.const` with the same value.
|
||||
|
||||
### `C7413`: `home-assistant-duplicate-const`
|
||||
|
||||
Import the constant from `homeassistant.const` instead of redefining it.
|
||||
|
||||
|
||||
## `home_assistant_actions_swallowed_exceptions` checker
|
||||
|
||||
Detects action handlers that catch exceptions without re-raising. Swallowed
|
||||
exceptions are not surfaced to the user.
|
||||
|
||||
### `E7405`: `home-assistant-action-swallowed-exception`
|
||||
|
||||
Action handlers must re-raise so the user is notified of the failure.
|
||||
The checker detects empty `except` blocks, blocks that only log,
|
||||
`contextlib.suppress(...)`, and equivalent patterns on decorators. It does
|
||||
not validate the *type* of exception being raised; a separate rule covers
|
||||
that.
|
||||
|
||||
|
||||
## `home_assistant_actions_service_registration` checker
|
||||
|
||||
Detects services registered inside `async_setup_entry` rather than
|
||||
`async_setup`.
|
||||
|
||||
### `W7414`: `home-assistant-service-registered-in-setup-entry`
|
||||
|
||||
Services should be registered in `async_setup` so they are available for
|
||||
automation validation even when no config entry is loaded. Registrations
|
||||
inside helper functions that are called from `async_setup_entry` are
|
||||
caught too.
|
||||
|
||||
See the [action-setup quality scale rule](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/action-setup).
|
||||
|
||||
|
||||
## `home_assistant_exception_translations` checker
|
||||
|
||||
Ensures `HomeAssistantError` and its subclasses use the translation system
|
||||
(`translation_domain`, `translation_key`) instead of hardcoded English
|
||||
strings. Also verifies that referenced translation keys exist in the
|
||||
integration's `strings.json` and that placeholders match.
|
||||
|
||||
### `W7417`: `home-assistant-exception-not-translated`
|
||||
|
||||
A `HomeAssistantError` subclass is raised with a hardcoded message; use
|
||||
`translation_domain` and `translation_key` instead. Quality-scale-gated.
|
||||
|
||||
### `W7419`: `home-assistant-exception-message-with-translation`
|
||||
|
||||
Don't pass a positional message argument when `translation_key` is also set;
|
||||
the translation system supplies the message.
|
||||
|
||||
### `E7406`: `home-assistant-exception-translation-key-missing`
|
||||
|
||||
The translation key referenced from code is missing from `strings.json`
|
||||
under the `exceptions` section.
|
||||
|
||||
### `E7408`: `home-assistant-exception-translation-key-domain-mismatch`
|
||||
|
||||
Both `translation_key` and `translation_domain` must be set together; only
|
||||
one of the two was provided.
|
||||
|
||||
### `E7418`: `home-assistant-exception-placeholder-mismatch`
|
||||
|
||||
The placeholders passed in code (e.g. `translation_placeholders={...}`)
|
||||
don't match the `{placeholder}` slots in the `strings.json` message.
|
||||
|
||||
|
||||
## `home_assistant_mdi_icons` checker
|
||||
|
||||
Validates that `mdi:` icon references in code and `icons.json` refer to
|
||||
icons that actually exist in the Material Design Icons set.
|
||||
|
||||
### `E7409`: `home-assistant-mdi-icon-not-found`
|
||||
|
||||
MDI icon reference in Python code does not exist in the Material Design
|
||||
Icons set.
|
||||
|
||||
### `E7410`: `home-assistant-mdi-icon-json-not-found`
|
||||
|
||||
MDI icon reference in `icons.json` does not exist in the Material Design
|
||||
Icons set.
|
||||
|
||||
|
||||
## `home_assistant_tests_redundant_usefixtures` checker
|
||||
|
||||
Detects `@pytest.mark.usefixtures(...)` decorators that duplicate a fixture
|
||||
already applied module-wide, either through a module-level `pytestmark`
|
||||
or via `autouse=True` on a fixture defined in a parent `conftest.py`.
|
||||
|
||||
### `R7403`: `home-assistant-tests-redundant-usefixtures`
|
||||
|
||||
Drop the redundant `@pytest.mark.usefixtures` decorator; the fixture is
|
||||
already applied to every test in the module.
|
||||
|
||||
|
||||
## `home_assistant_test_determinism` checker
|
||||
|
||||
`if` and `match` statements inside test functions create non-deterministic
|
||||
execution paths: some branches may never run, silently hiding failures.
|
||||
|
||||
### `W7409`: `home-assistant-test-non-deterministic`
|
||||
|
||||
Test function contains an `if` or `match` statement. Use
|
||||
`@pytest.mark.parametrize` to cover cases explicitly, or split into separate
|
||||
test functions. `if` statements have several exemptions: guard clauses
|
||||
(`return`/`raise`/`pytest.skip`/`pytest.xfail`/`pytest.fail`),
|
||||
conditions that reference a function parameter, and branches that contain
|
||||
no `assert`. `match` statements have no exemptions.
|
||||
|
||||
|
||||
## `home_assistant_reauthentication_flow` checker
|
||||
|
||||
Quality-scale-gated checker for the
|
||||
[`reauthentication-flow`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/reauthentication-flow)
|
||||
Silver rule. Fires only when the integration claims the rule as `done`.
|
||||
|
||||
### `W7410`: `home-assistant-missing-reauthentication-flow`
|
||||
|
||||
Integration's `config_flow.py` should implement `async_step_reauth`.
|
||||
|
||||
|
||||
## `home_assistant_parallel_updates` checker
|
||||
|
||||
Quality-scale-gated checker for the
|
||||
[`parallel-updates`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/parallel-updates)
|
||||
Silver rule. Fires only when the integration claims the rule as `done`.
|
||||
|
||||
### `W7411`: `home-assistant-missing-parallel-updates`
|
||||
|
||||
Platform module should define a module-level `PARALLEL_UPDATES` constant.
|
||||
|
||||
|
||||
## `home_assistant_diagnostics` checker
|
||||
|
||||
Quality-scale-gated checker for the
|
||||
[`diagnostics`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/diagnostics)
|
||||
Gold rule. Fires only when the integration claims the rule as `done`.
|
||||
|
||||
### `W7412`: `home-assistant-missing-diagnostics`
|
||||
|
||||
Integration's `diagnostics.py` should implement
|
||||
`async_get_config_entry_diagnostics` or `async_get_device_diagnostics`.
|
||||
|
||||
|
||||
## `home_assistant_config_entry_unloading` checker
|
||||
|
||||
Quality-scale-gated checker for the
|
||||
[`config-entry-unloading`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-entry-unloading)
|
||||
Silver rule. Fires only when the integration claims the rule as `done`.
|
||||
|
||||
### `W7413`: `home-assistant-missing-config-entry-unloading`
|
||||
|
||||
Integration's `__init__.py` should implement `async_unload_entry`.
|
||||
|
||||
|
||||
## `home_assistant_sequential_executor_jobs` checker
|
||||
|
||||
Detects consecutive `async_add_executor_job` calls in integration modules
|
||||
that could be grouped into a single executor job.
|
||||
|
||||
### `W7415`: `home-assistant-sequential-executor-jobs`
|
||||
|
||||
Two or more `async_add_executor_job` calls appearing as consecutive
|
||||
statements (uninterrupted by control flow such as `if`/`try`/`with`/`for`)
|
||||
should be combined into a single executor job that performs all the work,
|
||||
avoiding unnecessary context switches back to the event loop between
|
||||
blocking calls. The rule applies to integration modules only.
|
||||
|
||||
|
||||
## `home_assistant_has_entity_name` checker
|
||||
|
||||
Quality-scale-gated checker for the
|
||||
[`has-entity-name`](https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/has-entity-name)
|
||||
Bronze rule. Fires only when the integration claims the rule as `done`.
|
||||
|
||||
### `W7416`: `home-assistant-missing-has-entity-name`
|
||||
|
||||
Entity class should statically guarantee `_attr_has_entity_name = True`:
|
||||
either set at class level, set unconditionally at the top of a method, or
|
||||
supplied by an `entity_description` whose class sets `has_entity_name = True`.
|
||||
Conditional patterns are rejected.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Plugin to encourage correct use of DOMAIN constants in tests."""
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.helpers.module_info import is_test_module
|
||||
|
||||
_FUNCTION_CHECKS: list[tuple[str, int | None, str]] = [
|
||||
("async_setup_component", 1, "domain"),
|
||||
("async_mock_service", 1, "domain"),
|
||||
("MockConfigEntry", None, "domain"),
|
||||
]
|
||||
_METHOD_CHECKS: list[tuple[str, str, int | None, str]] = [
|
||||
("hass.services", "async_call", 0, "domain"),
|
||||
("hass.services", "call", 0, "domain"),
|
||||
("hass.config_entries.flow", "async_init", 0, "handler"),
|
||||
]
|
||||
|
||||
_DOMAIN_CONSTANTS: set[str] = {"DOMAIN", "domain"}
|
||||
_DOMAIN_SUFFIXES: tuple[str, ...] = ("_DOMAIN", "_domain")
|
||||
|
||||
|
||||
def _check_call_node_domain_arguments(node: nodes.Call) -> nodes.NodeNG | None:
|
||||
"""Ensure the call node arguments are valid domain constant or variable.
|
||||
|
||||
Return None if the argument node is valid, or the argument node if it is invalid.
|
||||
"""
|
||||
match node.func:
|
||||
case nodes.Attribute():
|
||||
for (
|
||||
method_source,
|
||||
method_name,
|
||||
arg_position,
|
||||
kwarg_name,
|
||||
) in _METHOD_CHECKS:
|
||||
if (
|
||||
node.func.attrname == method_name
|
||||
and node.func.expr.as_string() == method_source
|
||||
):
|
||||
return _check_call_node_domain_argument(
|
||||
node, arg_position=arg_position, kwarg_name=kwarg_name
|
||||
)
|
||||
case nodes.Name():
|
||||
for func_name, arg_position, kwarg_name in _FUNCTION_CHECKS:
|
||||
if node.func.name == func_name:
|
||||
return _check_call_node_domain_argument(
|
||||
node, arg_position=arg_position, kwarg_name=kwarg_name
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _check_call_node_domain_argument(
|
||||
call_node: nodes.Call,
|
||||
*,
|
||||
arg_position: int | None,
|
||||
kwarg_name: str,
|
||||
) -> nodes.NodeNG | None:
|
||||
"""Ensure the argument node is a domain constant or variable.
|
||||
|
||||
Return None if the argument node is valid, or the argument node if it is invalid.
|
||||
"""
|
||||
if arg_position is not None and len(call_node.args) > arg_position:
|
||||
argument_node = call_node.args[arg_position]
|
||||
else:
|
||||
argument_node = next(
|
||||
iter(
|
||||
keyword.value
|
||||
for keyword in call_node.keywords
|
||||
if keyword.arg == kwarg_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if argument_node and not _check_domain_argument(argument_node):
|
||||
return argument_node
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _check_domain_argument(arg_node: nodes.NodeNG) -> bool:
|
||||
"""Ensure the argument node is a domain constant or variable.
|
||||
|
||||
We allow:
|
||||
- x.DOMAIN/x.domain attribute (including *_DOMAIN/*_domain)
|
||||
- DOMAIN/domain name (including *_DOMAIN/*_domain)
|
||||
- string literals
|
||||
- subscript expressions (e.g. data["key"])
|
||||
|
||||
Return True if the argument is valid, False otherwise.
|
||||
"""
|
||||
match arg_node:
|
||||
case nodes.Attribute():
|
||||
if (
|
||||
attrname := arg_node.attrname
|
||||
) in _DOMAIN_CONSTANTS or attrname.endswith(_DOMAIN_SUFFIXES):
|
||||
return True
|
||||
case nodes.Const():
|
||||
if isinstance(arg_node.value, str):
|
||||
return True
|
||||
case nodes.Name():
|
||||
if (node_name := arg_node.name) in _DOMAIN_CONSTANTS or node_name.endswith(
|
||||
_DOMAIN_SUFFIXES
|
||||
):
|
||||
return True
|
||||
case nodes.Subscript():
|
||||
# Ignore subscripts like dict["key"]
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DomainConstantChecker(BaseChecker):
|
||||
"""Checker for correct use of DOMAIN constants in tests."""
|
||||
|
||||
name = "home_assistant_domain_constant"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"C7415": (
|
||||
"Argument %s should be a domain constant or variable in %s",
|
||||
"home-assistant-domain-argument",
|
||||
"Used when argument should be a domain constant/variable.",
|
||||
),
|
||||
}
|
||||
|
||||
options = ()
|
||||
|
||||
_in_test_module: bool
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Visit Module node."""
|
||||
self._in_test_module = is_test_module(node.name)
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Visit Call node."""
|
||||
if not self._in_test_module:
|
||||
return
|
||||
|
||||
if invalid_arg_node := _check_call_node_domain_arguments(node):
|
||||
self.add_message(
|
||||
"home-assistant-domain-argument",
|
||||
node=invalid_arg_node,
|
||||
args=(invalid_arg_node.as_string(), node.func.as_string()),
|
||||
)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(DomainConstantChecker(linter))
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260527.3"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.4"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
Generated
+1
-1
@@ -1269,7 +1269,7 @@ hole==0.9.0
|
||||
holidays==0.97
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260527.3
|
||||
home-assistant-frontend==20260527.4
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.6.1
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.avea.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -50,19 +53,31 @@ async def _setup_yaml_import(hass: HomeAssistant, bulbs: list[MagicMock]) -> Non
|
||||
|
||||
async def test_setup_entry_retries_when_ble_device_is_missing(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setup retries when the Bluetooth device is unavailable."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.avea.async_ble_device_from_address",
|
||||
return_value=None,
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.avea.async_ble_device_from_address",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.avea.async_address_reachability_diagnostics",
|
||||
return_value="mock reachability reason",
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert (
|
||||
"Could not find Avea device with address "
|
||||
f"{mock_config_entry.data[CONF_ADDRESS]}: mock reachability reason"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_yaml_import_creates_entries_for_discovered_bulbs(
|
||||
|
||||
@@ -2821,6 +2821,131 @@ async def test_test_condition(
|
||||
assert msg["result"]["result"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value_template", "expected_template_errors"),
|
||||
[
|
||||
("{{ no_such_variable }}", ["'no_such_variable' is undefined"]),
|
||||
# A single render emitting multiple errors forwards all of them
|
||||
("{{ foo }}{{ bar }}", ["'foo' is undefined", "'bar' is undefined"]),
|
||||
],
|
||||
)
|
||||
async def test_test_condition_template_error(
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
value_template: str,
|
||||
expected_template_errors: list[str],
|
||||
) -> None:
|
||||
"""Test template errors are forwarded in the result without being logged."""
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
await websocket_client.send_json_auto_id(
|
||||
{
|
||||
"type": "test_condition",
|
||||
"condition": {"condition": "template", "value_template": value_template},
|
||||
}
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["type"] == const.TYPE_RESULT
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"result": False,
|
||||
"template_errors": expected_template_errors,
|
||||
}
|
||||
|
||||
assert "Template variable" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "expected_error"),
|
||||
[
|
||||
# Missing mandatory config, raised by async_validate_condition_config
|
||||
(
|
||||
{"condition": "sun"},
|
||||
{
|
||||
"code": "invalid_format",
|
||||
"message": (
|
||||
"must contain at least one of before, after. for dictionary value "
|
||||
"@ data['options']"
|
||||
),
|
||||
},
|
||||
),
|
||||
# Failing enabled template, raised by async_condition_from_config
|
||||
(
|
||||
{
|
||||
"condition": "template",
|
||||
"value_template": "{{ true }}",
|
||||
"enabled": "{{ 1 / 0 }}",
|
||||
},
|
||||
{
|
||||
"code": "home_assistant_error",
|
||||
"message": (
|
||||
"Error rendering condition enabled template: "
|
||||
"ZeroDivisionError: division by zero"
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_test_condition_config_error(
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
condition: dict,
|
||||
expected_error: dict,
|
||||
) -> None:
|
||||
"""Test condition config errors are reported to the client without logging."""
|
||||
caplog.set_level(logging.ERROR)
|
||||
|
||||
await websocket_client.send_json_auto_id(
|
||||
{"type": "test_condition", "condition": condition}
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["type"] == const.TYPE_RESULT
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == expected_error
|
||||
|
||||
# The expected error is not logged by the default websocket error handler
|
||||
assert "Error handling message" not in caplog.text
|
||||
|
||||
|
||||
async def test_test_condition_check_error_not_logged(
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test errors raised while checking the condition are not logged.
|
||||
|
||||
The condition is valid and instantiates fine, but checking it raises (here
|
||||
the entity does not exist). The error is reported to the client without
|
||||
being logged by the default websocket error handler.
|
||||
"""
|
||||
caplog.set_level(logging.ERROR)
|
||||
|
||||
await websocket_client.send_json_auto_id(
|
||||
{
|
||||
"type": "test_condition",
|
||||
"condition": {
|
||||
"condition": "state",
|
||||
"entity_id": "hello.world",
|
||||
"state": "paulus",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["type"] == const.TYPE_RESULT
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == {
|
||||
"code": "home_assistant_error",
|
||||
"message": "In 'state':\n In 'state' condition: unknown entity hello.world",
|
||||
}
|
||||
|
||||
assert "Error handling message" not in caplog.text
|
||||
|
||||
|
||||
async def test_subscribe_condition(
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Tests for the domain constant checker."""
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.domain_constant import DomainConstantChecker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="domain_constant_checker")
|
||||
def domain_constant_checker_fixture(
|
||||
linter: UnittestLinter,
|
||||
) -> DomainConstantChecker:
|
||||
"""Fixture to provide a domain constant checker."""
|
||||
return DomainConstantChecker(linter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, DOMAIN, {})
|
||||
""",
|
||||
id="name_domain_constant",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, domain, {})
|
||||
""",
|
||||
id="name_domain_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, SENSOR_DOMAIN, {})
|
||||
""",
|
||||
id="name_prefixed_domain_constant",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, sensor_domain, {})
|
||||
""",
|
||||
id="name_prefixed_domain_variable",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, sensor.DOMAIN, {})
|
||||
""",
|
||||
id="attribute_domain_constant",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, config_entry.domain, {})
|
||||
""",
|
||||
id="attribute_domain",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, sensor.SENSOR_DOMAIN, {})
|
||||
""",
|
||||
id="attribute_prefixed_domain_constant",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, "sensor", {})
|
||||
""",
|
||||
id="string_literal",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, config["domain"], {})
|
||||
""",
|
||||
id="subscript_positional",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
MockConfigEntry(domain=entries["domain"])
|
||||
""",
|
||||
id="subscript_kwarg",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_mock_service(hass, DOMAIN, "service")
|
||||
""",
|
||||
id="async_mock_service",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
MockConfigEntry(domain=DOMAIN)
|
||||
""",
|
||||
id="mock_config_entry_kwarg",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
MockConfigEntry("other")
|
||||
""",
|
||||
id="mock_config_entry_positional_not_checked",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.services.async_call(DOMAIN, "service")
|
||||
""",
|
||||
id="services_async_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.services.call(DOMAIN, "service")
|
||||
""",
|
||||
id="services_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.config_entries.flow.async_init(DOMAIN)
|
||||
""",
|
||||
id="flow_async_init_positional",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.config_entries.flow.async_init(handler=DOMAIN)
|
||||
""",
|
||||
id="flow_async_init_kwarg",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
some_other_function(hass, "other", {})
|
||||
""",
|
||||
id="unrelated_function",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.services.unrelated("other")
|
||||
""",
|
||||
id="unrelated_method",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_no_warning(
|
||||
linter: UnittestLinter,
|
||||
domain_constant_checker: DomainConstantChecker,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "args"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, OTHER, {})
|
||||
""",
|
||||
("OTHER", "async_setup_component"),
|
||||
id="name_not_domain",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, sensor.OTHER, {})
|
||||
""",
|
||||
("sensor.OTHER", "async_setup_component"),
|
||||
id="attribute_not_domain",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_setup_component(hass, 5, {})
|
||||
""",
|
||||
("5", "async_setup_component"),
|
||||
id="non_string_constant",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async_mock_service(hass, OTHER, "service")
|
||||
""",
|
||||
("OTHER", "async_mock_service"),
|
||||
id="async_mock_service",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
MockConfigEntry(domain=OTHER)
|
||||
""",
|
||||
("OTHER", "MockConfigEntry"),
|
||||
id="mock_config_entry_kwarg",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.services.async_call(OTHER, "service")
|
||||
""",
|
||||
("OTHER", "hass.services.async_call"),
|
||||
id="services_async_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.services.call(OTHER, "service")
|
||||
""",
|
||||
("OTHER", "hass.services.call"),
|
||||
id="services_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.config_entries.flow.async_init(OTHER)
|
||||
""",
|
||||
("OTHER", "hass.config_entries.flow.async_init"),
|
||||
id="flow_async_init_positional",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
hass.config_entries.flow.async_init(handler=OTHER)
|
||||
""",
|
||||
("OTHER", "hass.config_entries.flow.async_init"),
|
||||
id="flow_async_init_kwarg",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_domain_argument_flagged(
|
||||
linter: UnittestLinter,
|
||||
domain_constant_checker: DomainConstantChecker,
|
||||
code: str,
|
||||
args: tuple[str, str],
|
||||
) -> None:
|
||||
"""Test that non-domain arguments are flagged."""
|
||||
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-domain-argument"
|
||||
assert messages[0].args == args
|
||||
|
||||
|
||||
def test_not_test_module_ignored(
|
||||
linter: UnittestLinter,
|
||||
domain_constant_checker: DomainConstantChecker,
|
||||
) -> None:
|
||||
"""Test that modules outside tests are ignored."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
async_setup_component(hass, OTHER, {})
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
Reference in New Issue
Block a user