mirror of
https://github.com/home-assistant/core.git
synced 2026-05-28 11:43:16 +02:00
Compare commits
23 Commits
edifier_ir1
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
| fce17c8e6f | |||
| 51d1d4aa9e | |||
| 8184b93151 | |||
| 403cb85bc8 | |||
| 4bf3a5b4bd | |||
| 5a73d78c90 | |||
| ebd9934213 | |||
| 73898c29e2 | |||
| 3372bf45ec | |||
| 9744388a4e | |||
| 75c52a382e | |||
| f8a65a7c6f | |||
| b2d934fae1 | |||
| eb72a72182 | |||
| a4b9de867c | |||
| 3a4e697414 | |||
| 00010a7508 | |||
| c5e4e97fa9 | |||
| 3f6e323b48 | |||
| b9639ec9f6 | |||
| 31bce13d16 | |||
| 3523a26abd | |||
| a6fcc9f3ff |
@@ -43,6 +43,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
||||
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
||||
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
||||
- Hardcoded `entity_id`s in tests are fine. If the same one is repeated, use a constant.
|
||||
|
||||
## Good practices
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues (-1)
|
||||
- name: 60 days stale PRs policy
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: 90 days stale issues
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
days-before-stale: 90
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
only-labels: "needs-more-information"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.13
|
||||
rev: v0.15.14
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -33,6 +33,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
||||
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
||||
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
||||
- Hardcoded `entity_id`s in tests are fine. If the same one is repeated, use a constant.
|
||||
|
||||
## Good practices
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.components.select import (
|
||||
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
|
||||
SelectEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -19,9 +19,6 @@ from .hub import AdsHub
|
||||
|
||||
DEFAULT_NAME = "ADS select"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_OPTIONS = "options"
|
||||
|
||||
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
|
||||
EntityNumericalStateChangedTriggerBase,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerBase,
|
||||
@@ -26,7 +26,7 @@ from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
CONF_HVAC_MODE = "hvac_mode"
|
||||
|
||||
HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONF_CONSIDER_HOME,
|
||||
CONF_NEW_DEVICE_DEFAULTS,
|
||||
CONF_SCAN_INTERVAL,
|
||||
|
||||
@@ -36,6 +36,8 @@ DEFAULT_CONSIDER_HOME: Final = timedelta(seconds=180)
|
||||
|
||||
CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults"
|
||||
|
||||
CONF_ASSOCIATED_ZONE: Final = "associated_zone"
|
||||
|
||||
ATTR_ATTRIBUTES: Final = "attributes"
|
||||
ATTR_BATTERY: Final = "battery"
|
||||
ATTR_DEV_ID: Final = "dev_id"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, final
|
||||
from typing import TYPE_CHECKING, Any, final
|
||||
|
||||
from propcache.api import cached_property
|
||||
|
||||
@@ -16,8 +16,19 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
EventDeviceRegistryUpdatedData,
|
||||
@@ -25,6 +36,7 @@ from homeassistant.helpers.device_registry import (
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
@@ -33,6 +45,7 @@ from .const import (
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONNECTED_DEVICE_REGISTERED,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
@@ -319,14 +332,120 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
addresses being used to identify the device.
|
||||
"""
|
||||
|
||||
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
|
||||
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
|
||||
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the scanner entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
if not self.registry_entry:
|
||||
return
|
||||
self._async_read_entity_options()
|
||||
|
||||
async def async_internal_will_remove_from_hass(self) -> None:
|
||||
"""Call when the scanner entity is about to be removed from hass."""
|
||||
await super().async_internal_will_remove_from_hass()
|
||||
if not self.registry_entry:
|
||||
return
|
||||
if self._scanner_option_associated_zone_unsub is not None:
|
||||
self._scanner_option_associated_zone_unsub()
|
||||
self._scanner_option_associated_zone_unsub = None
|
||||
self._async_clear_associated_zone_issue()
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self) -> None:
|
||||
"""Run when the entity registry entry has been updated."""
|
||||
self._async_read_entity_options()
|
||||
|
||||
@callback
|
||||
def _async_read_entity_options(self) -> None:
|
||||
"""Read entity options from the entity registry.
|
||||
|
||||
Called when the entity registry entry has been updated and before the
|
||||
scanner entity is added to the state machine.
|
||||
"""
|
||||
assert self.registry_entry
|
||||
if (scanner_options := self.registry_entry.options.get(DOMAIN)) and (
|
||||
associated_zone := scanner_options.get(CONF_ASSOCIATED_ZONE)
|
||||
):
|
||||
new_zone = associated_zone
|
||||
else:
|
||||
new_zone = zone.ENTITY_ID_HOME
|
||||
|
||||
if new_zone == self._scanner_option_associated_zone:
|
||||
return
|
||||
|
||||
# Tear down tracking for the previous zone.
|
||||
if self._scanner_option_associated_zone_unsub is not None:
|
||||
self._scanner_option_associated_zone_unsub()
|
||||
self._scanner_option_associated_zone_unsub = None
|
||||
self._async_clear_associated_zone_issue()
|
||||
|
||||
self._scanner_option_associated_zone = new_zone
|
||||
|
||||
# zone.home is always present so no tracking or issue handling needed.
|
||||
if new_zone == zone.ENTITY_ID_HOME:
|
||||
return
|
||||
|
||||
self._scanner_option_associated_zone_unsub = async_track_state_change_event(
|
||||
self.hass, new_zone, self._async_associated_zone_state_changed
|
||||
)
|
||||
if self.hass.states.get(new_zone) is None:
|
||||
self._async_create_associated_zone_issue()
|
||||
|
||||
@callback
|
||||
def _async_associated_zone_state_changed(
|
||||
self, event: Event[EventStateChangedData]
|
||||
) -> None:
|
||||
"""Open or clear the repair issue when the associated zone appears or disappears."""
|
||||
if event.data["new_state"] is None:
|
||||
self._async_create_associated_zone_issue()
|
||||
else:
|
||||
self._async_clear_associated_zone_issue()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_create_associated_zone_issue(self) -> None:
|
||||
"""Create a repair issue prompting the user to reconfigure the scanner."""
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._associated_zone_issue_id,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="associated_zone_missing",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"zone": self._scanner_option_associated_zone,
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_clear_associated_zone_issue(self) -> None:
|
||||
"""Clear the associated-zone-missing repair issue if it exists."""
|
||||
ir.async_delete_issue(self.hass, DOMAIN, self._associated_zone_issue_id)
|
||||
|
||||
@property
|
||||
def _associated_zone_issue_id(self) -> str:
|
||||
"""Return the issue id for the associated-zone-missing repair."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.registry_entry
|
||||
return f"associated_zone_missing_{self.registry_entry.id}"
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the device."""
|
||||
if self.is_connected is None:
|
||||
return None
|
||||
if self.is_connected:
|
||||
if not self.is_connected:
|
||||
return STATE_NOT_HOME
|
||||
associated_zone = self._scanner_option_associated_zone
|
||||
if associated_zone == zone.ENTITY_ID_HOME:
|
||||
return STATE_HOME
|
||||
return STATE_NOT_HOME
|
||||
if zone_state := self.hass.states.get(associated_zone):
|
||||
return zone_state.name
|
||||
# Configured zone has been removed; state is unknown.
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool | None:
|
||||
@@ -343,9 +462,18 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
if not self.is_connected:
|
||||
return attr
|
||||
|
||||
associated_zone = self._scanner_option_associated_zone
|
||||
# If the configured zone has been removed, in_zones stays empty so the
|
||||
# attribute does not claim membership in a zone that no longer exists.
|
||||
if (
|
||||
associated_zone != zone.ENTITY_ID_HOME
|
||||
and self.hass.states.get(associated_zone) is None
|
||||
):
|
||||
return attr
|
||||
|
||||
attr[ATTR_IN_ZONES] = [
|
||||
zone.ENTITY_ID_HOME,
|
||||
*zone.async_get_enclosing_zones(self.hass, zone.ENTITY_ID_HOME),
|
||||
associated_zone,
|
||||
*zone.async_get_enclosing_zones(self.hass, associated_zone),
|
||||
]
|
||||
|
||||
return attr
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"associated_zone_missing": {
|
||||
"description": "The scanner entity `{entity_id}` is associated with the zone `{zone}`, but that zone has been removed.\n\nTo fix this, reconfigure the scanner to use a different zone or recreate the missing zone.",
|
||||
"title": "Scanner is associated with a removed zone"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"see": {
|
||||
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260429.4"]
|
||||
"requirements": ["home-assistant-frontend==20260527.0"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Hardware",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["usb"],
|
||||
"dependencies": ["repairs", "usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Repairs for the Home Assistant Hardware integration."""
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
ISSUE_MULTI_PAN_MIGRATION = "multi_pan_migration"
|
||||
|
||||
|
||||
@callback
|
||||
def _multi_pan_issue_id(config_entry: ConfigEntry) -> str:
|
||||
"""Return the issue id for the multi-PAN migration issue of an entry."""
|
||||
return f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Create a repair issue to guide migration away from Multi-PAN."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=domain,
|
||||
issue_id=_multi_pan_issue_id(config_entry),
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": config_entry.title},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_delete_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Delete the multi-PAN migration repair issue for this entry."""
|
||||
ir.async_delete_issue(hass, domain, _multi_pan_issue_id(config_entry))
|
||||
|
||||
|
||||
class MultiPanMigrationRepairFlow(RepairsFlow):
|
||||
"""Reuse the multi-PAN options flow uninstall steps as a repair flow.
|
||||
|
||||
Subclass this together with the hardware-specific
|
||||
``MultiPanOptionsFlowHandler`` in each hardware integration's repairs
|
||||
module.
|
||||
|
||||
The repair flow runs in the repairs flow manager where ``self.handler``
|
||||
is the integration domain rather than the hardware config entry id, so
|
||||
the ``config_entry`` accessor of ``OptionsFlow`` must be overridden.
|
||||
"""
|
||||
|
||||
_repair_config_entry: ConfigEntry
|
||||
|
||||
@property
|
||||
def config_entry(self) -> ConfigEntry:
|
||||
"""Return the hardware config entry to migrate."""
|
||||
return self._repair_config_entry
|
||||
|
||||
async def _async_step_start_migration(self) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step of the migration flow.
|
||||
|
||||
The repair flow's init data is the issue context, not user form input,
|
||||
so pass None to render the uninstall confirmation form.
|
||||
"""
|
||||
return await self.async_step_uninstall_addon() # type: ignore[attr-defined, no-any-return]
|
||||
@@ -6,6 +6,8 @@ import dataclasses
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
@@ -25,6 +27,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.integration_platform import (
|
||||
async_process_integration_platforms,
|
||||
@@ -37,15 +40,18 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .const import DOMAIN, LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
WaitingAddonManager,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
||||
DATA_FLASHER_ADDON_MANAGER = "silabs_flasher"
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
||||
CONF_ADDON_DEVICE = "device"
|
||||
@@ -71,53 +77,6 @@ async def get_multiprotocol_addon_manager(
|
||||
return manager
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state is AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class MultiprotocolAddonManager(WaitingAddonManager):
|
||||
"""Silicon Labs Multiprotocol add-on manager."""
|
||||
|
||||
@@ -265,18 +224,6 @@ class MultipanProtocol(Protocol):
|
||||
"""
|
||||
|
||||
|
||||
@singleton(DATA_FLASHER_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||
"""Get the flasher add-on manager."""
|
||||
return WaitingAddonManager(
|
||||
hass,
|
||||
LOGGER,
|
||||
"Silicon Labs Flasher",
|
||||
SILABS_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SerialPortSettings:
|
||||
"""Serial port settings."""
|
||||
@@ -339,6 +286,19 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
def _zha_name(self) -> str:
|
||||
"""Return the ZHA name."""
|
||||
|
||||
@abstractmethod
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
|
||||
@abstractmethod
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier (e.g. 'yellow_zigbee_ncp')."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
|
||||
@property
|
||||
def flow_manager(self) -> OptionsFlowManager:
|
||||
"""Return the correct flow manager."""
|
||||
@@ -686,61 +646,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
async def async_step_firmware_revert(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install the flasher addon, if necessary."""
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_flasher_addon()
|
||||
|
||||
if addon_info.state is AddonState.NOT_RUNNING:
|
||||
return await self.async_step_configure_flasher_addon()
|
||||
|
||||
# If the addon is already installed and running, fail
|
||||
return self.async_abort(
|
||||
reason="addon_already_running",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_install_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show progress dialog for installing flasher addon."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
_LOGGER.debug("Flasher addon state: %s", addon_info)
|
||||
|
||||
if not self.install_task:
|
||||
self.install_task = self.hass.async_create_task(
|
||||
flasher_manager.async_install_addon_waiting(),
|
||||
"SiLabs Flasher addon install",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_flasher_addon",
|
||||
progress_action="install_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="install_failed")
|
||||
finally:
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="configure_flasher_addon")
|
||||
|
||||
async def async_step_configure_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform initial backup and reconfigure ZHA."""
|
||||
"""Initiate ZHA backup and start multiprotocol addon uninstall."""
|
||||
# pylint: disable=home-assistant-component-root-import
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
@@ -782,17 +688,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
_LOGGER.exception("Unexpected exception during ZHA migration")
|
||||
raise AbortFlow("zha_migration_failed") from err
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": new_settings.device,
|
||||
"flow_control": new_settings.flow_control,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config, flasher_manager)
|
||||
|
||||
return await self.async_step_uninstall_multiprotocol_addon()
|
||||
|
||||
async def async_step_uninstall_multiprotocol_addon(
|
||||
@@ -821,62 +716,93 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
finally:
|
||||
self.stop_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="start_flasher_addon")
|
||||
return self.async_show_progress_done(next_step_id="install_zigbee_firmware")
|
||||
|
||||
async def async_step_start_flasher_addon(
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start Silicon Labs Flasher add-on."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Flash Zigbee firmware directly onto the radio."""
|
||||
if not self.install_task:
|
||||
|
||||
if not self.start_task:
|
||||
async def _flash_firmware() -> None:
|
||||
serial_port_settings = await self._async_serial_port_settings()
|
||||
device = serial_port_settings.device
|
||||
|
||||
async def start_and_wait_until_done() -> None:
|
||||
await flasher_manager.async_start_addon_waiting()
|
||||
# Now that the addon is running, wait for it to finish
|
||||
await flasher_manager.async_wait_until_addon_state(
|
||||
AddonState.NOT_RUNNING
|
||||
)
|
||||
# For the duration of firmware flashing, hint to other integrations
|
||||
# (i.e. ZHA) that the hardware is in use and should not be accessed.
|
||||
async with async_firmware_flashing_context(self.hass, device, DOMAIN):
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = FirmwareUpdateClient(self._firmware_update_url(), session)
|
||||
|
||||
self.start_task = self.hass.async_create_task(
|
||||
start_and_wait_until_done(), eager_start=False
|
||||
try:
|
||||
manifest = await client.async_update_data()
|
||||
fw_manifest = next(
|
||||
fw
|
||||
for fw in manifest.firmwares
|
||||
if fw.filename.startswith(self._zigbee_firmware_type())
|
||||
)
|
||||
fw_data = await client.async_fetch_firmware(fw_manifest)
|
||||
except (
|
||||
StopIteration,
|
||||
TimeoutError,
|
||||
ClientError,
|
||||
ManifestMissing,
|
||||
ValueError,
|
||||
) as err:
|
||||
raise HomeAssistantError(
|
||||
"Failed to fetch Zigbee firmware"
|
||||
) from err
|
||||
|
||||
await async_flash_silabs_firmware(
|
||||
hass=self.hass,
|
||||
device=device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
||||
progress_callback=lambda offset, total: (
|
||||
self.async_update_progress(offset / total)
|
||||
),
|
||||
)
|
||||
|
||||
self.install_task = self.hass.async_create_task(
|
||||
_flash_firmware(),
|
||||
"Flash Zigbee firmware",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.start_task.done():
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="start_flasher_addon",
|
||||
progress_action="start_flasher_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.start_task,
|
||||
step_id="install_zigbee_firmware",
|
||||
progress_action="install_zigbee_firmware",
|
||||
description_placeholders={
|
||||
"hardware_name": self._hardware_name(),
|
||||
},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="flasher_failed")
|
||||
await self.install_task
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Failed to flash Zigbee firmware: %s", err)
|
||||
return self.async_show_progress_done(next_step_id="firmware_flash_failed")
|
||||
finally:
|
||||
self.start_task = None
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="flashing_complete")
|
||||
|
||||
async def async_step_flasher_failed(
|
||||
async def async_step_firmware_flash_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Flasher add-on start failed."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Firmware flashing failed."""
|
||||
return self.async_abort(
|
||||
reason="addon_start_failed",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
reason="fw_install_failed",
|
||||
description_placeholders={"firmware_name": "Zigbee"},
|
||||
)
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Finish flashing and update the config entry."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
await flasher_manager.async_uninstall_addon_waiting()
|
||||
|
||||
# Finish ZHA migration if needed
|
||||
if self._zha_migration_mgr:
|
||||
try:
|
||||
|
||||
@@ -102,7 +102,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||
"install_zigbee_firmware": "Please wait while Zigbee-only firmware is installed on your {hardware_name}. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds.",
|
||||
"uninstall_multiprotocol_addon": "Please wait while the {addon_name} app is uninstalled."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -37,13 +37,59 @@ from .const import (
|
||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
from .helpers import async_firmware_update_context
|
||||
from .silabs_multiprotocol_addon import (
|
||||
WaitingAddonManager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class ApplicationType(StrEnum):
|
||||
"""Application type running on a device."""
|
||||
@@ -279,6 +325,11 @@ async def guess_hardware_owners(
|
||||
assert otbr_addon_fw_info is not None
|
||||
device_guesses[otbr_path].append(otbr_addon_fw_info)
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
from .silabs_multiprotocol_addon import ( # noqa: PLC0415
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
||||
|
||||
try:
|
||||
|
||||
@@ -7,6 +7,13 @@ import os.path
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
@@ -92,6 +99,16 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
try:
|
||||
uses_multi_pan = await multi_pan_addon_using_device(hass, device_path)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if uses_multi_pan:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
|
||||
@@ -248,6 +248,19 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return self._hw_variant.full_name
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "skyconnect_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return Zbt1Flasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Repairs for the Home Assistant SkyConnect integration."""
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import (
|
||||
ConfirmRepairFlow,
|
||||
RepairsFlow,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class SkyConnectMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant SkyConnect."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantSkyConnectMultiPanOptionsFlowHandler.__init__(self, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_init( # type: ignore[override]
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a SkyConnect repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return SkyConnectMultiPanMigrationRepairFlow(entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -106,6 +106,37 @@
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -130,8 +161,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -7,8 +7,13 @@ from homeassistant.components.hassio import HassioNotReadyError, get_os_info
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
check_multi_pan_addon,
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
@@ -27,6 +32,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
@@ -77,6 +83,16 @@ async def async_setup_entry(
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
try:
|
||||
multipan_using_device = await multi_pan_addon_using_device(hass, RADIO_DEVICE)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if multipan_using_device:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
if firmware is ApplicationType.EZSP:
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
|
||||
@@ -319,6 +319,19 @@ class HomeAssistantYellowMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return BOARD_NAME
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "yellow_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return YellowFlasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Repairs for the Home Assistant Yellow integration."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import (
|
||||
ConfirmRepairFlow,
|
||||
RepairsFlow,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class YellowMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant Yellow."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantYellowMultiPanOptionsFlowHandler.__init__(self, hass, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_main_menu( # type: ignore[override]
|
||||
self, _: None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a Yellow repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return YellowMultiPanMigrationRepairFlow(hass, entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -11,6 +11,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -37,8 +68,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.trigger import (
|
||||
|
||||
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
|
||||
|
||||
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -15,7 +15,6 @@ CONF_INFO = "info"
|
||||
CONF_INVERTING = "inverting"
|
||||
CONF_LIGHT = "light"
|
||||
CONF_NODE = "node"
|
||||
CONF_NOTE = "note"
|
||||
CONF_OFF_ID = "off_id"
|
||||
CONF_ON_ID = "on_id"
|
||||
CONF_POSITION = "position"
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_NOTE,
|
||||
CONF_PASSWORD,
|
||||
CONF_TYPE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -25,7 +26,6 @@ from .const import (
|
||||
CONF_INFO,
|
||||
CONF_INVERTING,
|
||||
CONF_LIGHT,
|
||||
CONF_NOTE,
|
||||
CONF_OFF_ID,
|
||||
CONF_ON_ID,
|
||||
CONF_POSITION,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -81,7 +81,6 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
||||
def async_on_update(self, event: NodeProperty) -> None:
|
||||
"""Save brightness in the update event from the ISY Node."""
|
||||
if self._node.status not in (0, ISY_VALUE_UNKNOWN):
|
||||
self._last_brightness = self._node.status
|
||||
if self._node.uom == UOM_PERCENTAGE:
|
||||
self._last_brightness = round(self._node.status * 255.0 / 100.0)
|
||||
else:
|
||||
|
||||
@@ -279,10 +279,6 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
|
||||
if uom in (UOM_INDEX, UOM_ON_OFF):
|
||||
return cast(str, self.target.formatted)
|
||||
|
||||
# Check if this is an index type and get formatted value
|
||||
if uom == UOM_INDEX and hasattr(self.target, "formatted"):
|
||||
return cast(str, self.target.formatted)
|
||||
|
||||
# Handle ISY precision and rounding
|
||||
value = convert_isy_value_to_hass(value, uom, self.target.prec)
|
||||
if value is None:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -50,8 +50,10 @@ class QbusWeatherDescription(SensorEntityDescription):
|
||||
"""Description for Qbus weather entities."""
|
||||
|
||||
property: str
|
||||
scale_factor: int | None = None
|
||||
|
||||
|
||||
# Qbus reports illuminance in klux, HA only supports lux.
|
||||
_WEATHER_DESCRIPTIONS = (
|
||||
QbusWeatherDescription(
|
||||
key="daylight",
|
||||
@@ -60,6 +62,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light",
|
||||
@@ -67,6 +70,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_east",
|
||||
@@ -75,6 +79,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_south",
|
||||
@@ -83,6 +88,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="light_west",
|
||||
@@ -91,6 +97,7 @@ _WEATHER_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
scale_factor=1000,
|
||||
),
|
||||
QbusWeatherDescription(
|
||||
key="temperature",
|
||||
@@ -400,4 +407,8 @@ class QbusWeatherSensor(QbusEntity, SensorEntity):
|
||||
|
||||
async def _handle_state_received(self, state: QbusMqttWeatherState) -> None:
|
||||
if value := state.read_property(self.entity_description.property, None):
|
||||
self.native_value = value
|
||||
self.native_value = (
|
||||
value * self.entity_description.scale_factor
|
||||
if self.entity_description.scale_factor is not None
|
||||
else value
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.components.blueprint import (
|
||||
)
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
|
||||
@@ -59,6 +60,7 @@ from . import (
|
||||
binary_sensor as binary_sensor_platform,
|
||||
button as button_platform,
|
||||
cover as cover_platform,
|
||||
device_tracker as device_tracker_platform,
|
||||
event as event_platform,
|
||||
fan as fan_platform,
|
||||
image as image_platform,
|
||||
@@ -199,6 +201,9 @@ CONFIG_SECTION_SCHEMA = vol.All(
|
||||
vol.Optional(COVER_DOMAIN): vol.All(
|
||||
cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(DEVICE_TRACKER_DOMAIN): vol.All(
|
||||
cv.ensure_list, [device_tracker_platform.TRACKER_YAML_SCHEMA]
|
||||
),
|
||||
vol.Optional(EVENT_DOMAIN): vol.All(
|
||||
cv.ensure_list, [event_platform.EVENT_YAML_SCHEMA]
|
||||
),
|
||||
|
||||
@@ -23,6 +23,8 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -74,6 +76,11 @@ from .cover import (
|
||||
STOP_ACTION,
|
||||
async_create_preview_cover,
|
||||
)
|
||||
from .device_tracker import (
|
||||
CONF_IN_ZONES,
|
||||
CONF_LOCATION_ACCURACY,
|
||||
async_create_preview_tracker,
|
||||
)
|
||||
from .event import CONF_EVENT_TYPE, CONF_EVENT_TYPES, async_create_preview_event
|
||||
from .fan import (
|
||||
CONF_OFF_ACTION,
|
||||
@@ -150,6 +157,7 @@ _SCHEMA_STATE: dict[vol.Marker, Any] = {
|
||||
def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
"""Generate schema."""
|
||||
schema: dict[vol.Marker, Any] = {}
|
||||
advanced_options: dict[vol.Marker, Any] = {}
|
||||
|
||||
if flow_type == "config":
|
||||
schema = {vol.Required(CONF_NAME): selector.TextSelector()}
|
||||
@@ -226,6 +234,16 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
)
|
||||
}
|
||||
|
||||
if domain == Platform.DEVICE_TRACKER:
|
||||
schema |= {
|
||||
vol.Optional(CONF_IN_ZONES): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_LATITUDE): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_LONGITUDE): selector.TemplateSelector(),
|
||||
}
|
||||
advanced_options |= {
|
||||
vol.Optional(CONF_LOCATION_ACCURACY): selector.TemplateSelector(),
|
||||
}
|
||||
|
||||
if domain == Platform.EVENT:
|
||||
schema |= {
|
||||
vol.Required(CONF_EVENT_TYPE): selector.TemplateSelector(),
|
||||
@@ -431,6 +449,7 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AVAILABILITY): selector.TemplateSelector(),
|
||||
**advanced_options,
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
@@ -540,6 +559,7 @@ TEMPLATE_TYPES = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.IMAGE,
|
||||
@@ -575,6 +595,11 @@ CONFIG_FLOW = {
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.COVER),
|
||||
),
|
||||
Platform.DEVICE_TRACKER: SchemaFlowFormStep(
|
||||
config_schema(Platform.DEVICE_TRACKER),
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.DEVICE_TRACKER),
|
||||
),
|
||||
Platform.EVENT: SchemaFlowFormStep(
|
||||
config_schema(Platform.EVENT),
|
||||
preview="template",
|
||||
@@ -660,6 +685,11 @@ OPTIONS_FLOW = {
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.COVER),
|
||||
),
|
||||
Platform.DEVICE_TRACKER: SchemaFlowFormStep(
|
||||
options_schema(Platform.DEVICE_TRACKER),
|
||||
preview="template",
|
||||
validate_user_input=validate_user_input(Platform.DEVICE_TRACKER),
|
||||
),
|
||||
Platform.EVENT: SchemaFlowFormStep(
|
||||
options_schema(Platform.EVENT),
|
||||
preview="template",
|
||||
@@ -730,6 +760,7 @@ CREATE_PREVIEW_ENTITY: dict[
|
||||
Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel,
|
||||
Platform.BINARY_SENSOR: async_create_preview_binary_sensor,
|
||||
Platform.COVER: async_create_preview_cover,
|
||||
Platform.DEVICE_TRACKER: async_create_preview_tracker,
|
||||
Platform.EVENT: async_create_preview_event,
|
||||
Platform.FAN: async_create_preview_fan,
|
||||
Platform.LIGHT: async_create_preview_light,
|
||||
|
||||
@@ -26,6 +26,7 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.EVENT,
|
||||
Platform.FAN,
|
||||
Platform.IMAGE,
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Support for device trackers which integrates with other components."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
TrackerEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator, validators as template_validators
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
async_setup_template_preview,
|
||||
)
|
||||
from .schemas import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
from .template_entity import TemplateEntity
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
DEFAULT_NAME = "Template Device Tracker"
|
||||
|
||||
CONF_IN_ZONES = "in_zones"
|
||||
CONF_LOCATION_ACCURACY = "location_accuracy"
|
||||
|
||||
|
||||
def _validate_in_zones_or_lat_and_lon(obj: dict) -> dict:
|
||||
if CONF_IN_ZONES not in obj:
|
||||
if CONF_LATITUDE not in obj or CONF_LONGITUDE not in obj:
|
||||
raise vol.Invalid(
|
||||
f"Either '{CONF_IN_ZONES}' or both '{CONF_LATITUDE}' and '{CONF_LONGITUDE}' must be specified"
|
||||
)
|
||||
elif (CONF_LATITUDE in obj and CONF_LONGITUDE not in obj) or (
|
||||
CONF_LATITUDE not in obj and CONF_LONGITUDE in obj
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"Both '{CONF_LATITUDE}' and '{CONF_LONGITUDE}' must be specified"
|
||||
)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def validate_in_zones(
|
||||
entity: AbstractTemplateTracker,
|
||||
) -> Callable[[Any], list[str] | None]:
|
||||
"""Convert the result to a list of entity_ids.
|
||||
|
||||
This ensures the result is a list of zone entity_ids.
|
||||
All other values that are not lists will result in None.
|
||||
"""
|
||||
|
||||
def convert(result: Any) -> list[str] | None:
|
||||
if template_validators.check_result_for_none(result):
|
||||
return None
|
||||
|
||||
if not isinstance(result, list):
|
||||
template_validators.log_validation_result_error(
|
||||
entity,
|
||||
CONF_IN_ZONES,
|
||||
result,
|
||||
"expected a list of zone entity_ids",
|
||||
)
|
||||
return None
|
||||
|
||||
zone_entity_ids = []
|
||||
failed = []
|
||||
for v in result:
|
||||
try:
|
||||
zone_entity_ids.append(
|
||||
vol.All(cv.entity_id, cv.entity_domain(zone.DOMAIN))(v)
|
||||
)
|
||||
except vol.Invalid:
|
||||
failed.append(v)
|
||||
|
||||
if failed:
|
||||
template_validators.log_validation_result_error(
|
||||
entity,
|
||||
CONF_IN_ZONES,
|
||||
failed,
|
||||
"expected a list of zone entity_ids",
|
||||
)
|
||||
|
||||
return zone_entity_ids
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
TRACKER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_IN_ZONES): cv.template,
|
||||
vol.Optional(CONF_LATITUDE): cv.template,
|
||||
vol.Optional(CONF_LOCATION_ACCURACY): cv.template,
|
||||
vol.Optional(CONF_LONGITUDE): cv.template,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
TRACKER_YAML_SCHEMA = vol.All(
|
||||
_validate_in_zones_or_lat_and_lon,
|
||||
TRACKER_COMMON_SCHEMA.extend(
|
||||
make_template_entity_common_modern_schema(
|
||||
DEVICE_TRACKER_DOMAIN, DEFAULT_NAME
|
||||
).schema
|
||||
),
|
||||
)
|
||||
|
||||
TRACKER_CONFIG_ENTRY_SCHEMA = vol.All(
|
||||
_validate_in_zones_or_lat_and_lon,
|
||||
TRACKER_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the template device trackers."""
|
||||
await async_setup_template_platform(
|
||||
hass,
|
||||
DEVICE_TRACKER_DOMAIN,
|
||||
config,
|
||||
StateTrackerEntity,
|
||||
TriggerTrackerEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize config entry."""
|
||||
await async_setup_template_entry(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
StateTrackerEntity,
|
||||
TRACKER_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_tracker(
|
||||
hass: HomeAssistant, name: str, config: dict[str, Any]
|
||||
) -> StateTrackerEntity:
|
||||
"""Create a preview device tracker."""
|
||||
return async_setup_template_preview(
|
||||
hass,
|
||||
name,
|
||||
config,
|
||||
StateTrackerEntity,
|
||||
TRACKER_CONFIG_ENTRY_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class AbstractTemplateTracker(AbstractTemplateEntity, TrackerEntity):
|
||||
"""Representation of a template device tracker features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
|
||||
# The super init is not called because TemplateEntity
|
||||
# and TriggerEntity will call
|
||||
# AbstractTemplateEntity.__init__. This ensures that
|
||||
# the __init__ on AbstractTemplateEntity is not
|
||||
# called twice.
|
||||
def __init__(self) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
|
||||
self.setup_template(
|
||||
CONF_IN_ZONES,
|
||||
"_attr_in_zones",
|
||||
validate_in_zones(self),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LATITUDE,
|
||||
"_attr_latitude",
|
||||
template_validators.number(self, CONF_LATITUDE, -90.0, 90.0),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LONGITUDE,
|
||||
"_attr_longitude",
|
||||
template_validators.number(self, CONF_LONGITUDE, -180.0, 180.0),
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LOCATION_ACCURACY,
|
||||
"_attr_location_accuracy",
|
||||
on_update=self._update_location_accuracy,
|
||||
none_on_template_error=False,
|
||||
)
|
||||
|
||||
self._location_accuracy_validator = template_validators.number(
|
||||
self, CONF_LOCATION_ACCURACY, 0.0
|
||||
)
|
||||
|
||||
def _update_location_accuracy(self, value: float | None) -> None:
|
||||
"""Update the location accuracy."""
|
||||
self._attr_location_accuracy = self._location_accuracy_validator(value) or 0.0
|
||||
|
||||
|
||||
class StateTrackerEntity(TemplateEntity, AbstractTemplateTracker):
|
||||
"""Representation of a Template device tracker."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the Template device tracker."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateTracker.__init__(self)
|
||||
|
||||
|
||||
class TriggerTrackerEntity(TriggerEntity, AbstractTemplateTracker):
|
||||
"""Tracker entity based on trigger data."""
|
||||
|
||||
domain = DEVICE_TRACKER_DOMAIN
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: TriggerUpdateCoordinator,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateTracker.__init__(self)
|
||||
@@ -136,6 +136,34 @@
|
||||
},
|
||||
"title": "Template cover"
|
||||
},
|
||||
"device_tracker": {
|
||||
"data": {
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"latitude": "Defines a template to get the latitude of the device tracker. Valid values are numbers between `-90` and `90`.",
|
||||
"longitude": "Defines a template to get the longitude of the device tracker. Valid values are numbers between `-180` and `180`.",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]",
|
||||
"location_accuracy": "Location accuracy"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]",
|
||||
"location_accuracy": "Defines a template to get the accuracy of the device tracker's location in meters. Valid values are numbers greater than or equal to `0`."
|
||||
},
|
||||
"name": "[%key:component::template::common::advanced_options%]"
|
||||
}
|
||||
},
|
||||
"title": "Template device tracker"
|
||||
},
|
||||
"event": {
|
||||
"data": {
|
||||
"device_class": "[%key:component::template::common::device_class%]",
|
||||
@@ -454,6 +482,7 @@
|
||||
"binary_sensor": "[%key:component::binary_sensor::title%]",
|
||||
"button": "[%key:component::button::title%]",
|
||||
"cover": "[%key:component::cover::title%]",
|
||||
"device_tracker": "[%key:component::device_tracker::title%]",
|
||||
"event": "[%key:component::event::title%]",
|
||||
"fan": "[%key:component::fan::title%]",
|
||||
"image": "[%key:component::image::title%]",
|
||||
@@ -651,7 +680,6 @@
|
||||
},
|
||||
"title": "[%key:component::template::config::step::button::title%]"
|
||||
},
|
||||
|
||||
"cover": {
|
||||
"data": {
|
||||
"close_cover": "[%key:component::template::config::step::cover::data::close_cover%]",
|
||||
@@ -684,6 +712,32 @@
|
||||
},
|
||||
"title": "[%key:component::template::config::step::cover::title%]"
|
||||
},
|
||||
"device_tracker": {
|
||||
"data": {
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"latitude": "[%key:component::template::config::step::device_tracker::data::latitude%]",
|
||||
"longitude": "[%key:component::template::config::step::device_tracker::data::longitude%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"latitude": "[%key:component::template::config::step::device_tracker::data_description::latitude%]",
|
||||
"longitude": "[%key:component::template::config::step::device_tracker::data_description::longitude%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]",
|
||||
"location_accuracy": "[%key:component::template::config::step::device_tracker::sections::advanced_options::data::location_accuracy%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]",
|
||||
"location_accuracy": "[%key:component::template::config::step::device_tracker::sections::advanced_options::data_description::location_accuracy%]"
|
||||
},
|
||||
"name": "[%key:component::template::common::advanced_options%]"
|
||||
}
|
||||
},
|
||||
"title": "[%key:component::template::config::step::device_tracker::title%]"
|
||||
},
|
||||
"event": {
|
||||
"data": {
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_NAME, UnitOfTime
|
||||
from homeassistant.helpers import selector
|
||||
@@ -27,6 +28,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
ALLOWED_DOMAINS = [COUNTER_DOMAIN, SENSOR_DOMAIN]
|
||||
|
||||
|
||||
async def get_base_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Get base options schema."""
|
||||
@@ -90,7 +93,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SENSOR_DOMAIN, multiple=False),
|
||||
selector.EntitySelectorConfig(domain=ALLOWED_DOMAINS, multiple=False),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "trend",
|
||||
"name": "Trend",
|
||||
"after_dependencies": ["sensor", "counter"],
|
||||
"codeowners": ["@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trend",
|
||||
|
||||
@@ -202,9 +202,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
|
||||
# Determine preset modes (ignore if empty options)
|
||||
if definition.preset_wrapper and definition.preset_wrapper.options:
|
||||
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
|
||||
self._attr_preset_modes = definition.preset_wrapper.options
|
||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||
if description.switch_only_hvac_mode not in self._attr_hvac_modes:
|
||||
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
|
||||
|
||||
# Determine dpcode to use for setting the humidity
|
||||
if definition.target_humidity_wrapper:
|
||||
|
||||
@@ -939,6 +939,7 @@ class DPCode(StrEnum):
|
||||
TEMP_INDOOR = "temp_indoor" # Indoor temperature in °C
|
||||
TEMP_SET = "temp_set" # Set the temperature in °C
|
||||
TEMP_SET_F = "temp_set_f" # Set the temperature in °F
|
||||
TEMP_SETTING_QUICK_C = "temp_setting_quick_c"
|
||||
TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching
|
||||
TEMP_VALUE = "temp_value" # Color temperature
|
||||
TEMP_VALUE_V2 = "temp_value_v2"
|
||||
@@ -992,6 +993,7 @@ class DPCode(StrEnum):
|
||||
WORK_POWER = "work_power"
|
||||
WORK_STATE = "work_state"
|
||||
WORK_STATE_E = "work_state_e"
|
||||
WORK_TYPE = "work_type"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["tuya_sharing"],
|
||||
"requirements": [
|
||||
"tuya-device-handlers==0.0.19",
|
||||
"tuya-device-handlers==0.0.21",
|
||||
"tuya-device-sharing-sdk==0.2.8"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,18 @@ from .entity import TuyaEntity
|
||||
# All descriptions can be found here. Mostly the Enum data types in the
|
||||
# default instructions set of each category end up being a select.
|
||||
SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
|
||||
DeviceCategory.BH: (
|
||||
SelectEntityDescription(
|
||||
key=DPCode.TEMP_SETTING_QUICK_C,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="quick_heat_temperature",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.WORK_TYPE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="kettle_work_mode",
|
||||
),
|
||||
),
|
||||
DeviceCategory.CL: (
|
||||
SelectEntityDescription(
|
||||
key=DPCode.CONTROL_BACK_MODE,
|
||||
|
||||
@@ -478,6 +478,15 @@
|
||||
"1": "Continuous working mode"
|
||||
}
|
||||
},
|
||||
"kettle_work_mode": {
|
||||
"name": "Work mode",
|
||||
"state": {
|
||||
"boiling_quick": "Quick boil",
|
||||
"setting_quick": "Quick heat",
|
||||
"temp_boiling": "Boil and keep warm",
|
||||
"temp_setting": "Heat and keep warm"
|
||||
}
|
||||
},
|
||||
"led_type": {
|
||||
"name": "Light source type",
|
||||
"state": {
|
||||
@@ -515,6 +524,16 @@
|
||||
"smart": "Smart"
|
||||
}
|
||||
},
|
||||
"quick_heat_temperature": {
|
||||
"name": "Quick heat temperature",
|
||||
"state": {
|
||||
"80": "80 °C",
|
||||
"85": "85 °C",
|
||||
"90": "90 °C",
|
||||
"95": "95 °C",
|
||||
"100": "100 °C"
|
||||
}
|
||||
},
|
||||
"record_mode": {
|
||||
"name": "Record mode",
|
||||
"state": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityNumericalStateTriggerWithUnitBase,
|
||||
@@ -28,14 +28,16 @@ from .const import DOMAIN
|
||||
|
||||
CONF_OPERATION_MODE = "operation_mode"
|
||||
|
||||
_OPERATION_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_OPERATION_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [str]
|
||||
),
|
||||
},
|
||||
}
|
||||
_OPERATION_MODE_CHANGED_TRIGGER_SCHEMA = (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_OPERATION_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [str]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.trigger_common_fields: &trigger_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
default: each
|
||||
selector:
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
@@ -91,7 +91,6 @@ CONF_COMMAND_ON: Final = "command_on"
|
||||
CONF_COMMAND_OPEN: Final = "command_open"
|
||||
CONF_COMMAND_STATE: Final = "command_state"
|
||||
CONF_COMMAND_STOP: Final = "command_stop"
|
||||
CONF_COMMENT: Final = "comment"
|
||||
CONF_CONDITION: Final = "condition"
|
||||
CONF_CONDITIONS: Final = "conditions"
|
||||
CONF_CONTINUE_ON_ERROR: Final = "continue_on_error"
|
||||
@@ -171,6 +170,7 @@ CONF_MODEL_ID: Final = "model_id"
|
||||
CONF_MONITORED_CONDITIONS: Final = "monitored_conditions"
|
||||
CONF_MONITORED_VARIABLES: Final = "monitored_variables"
|
||||
CONF_NAME: Final = "name"
|
||||
CONF_NOTE: Final = "note"
|
||||
CONF_OFFSET: Final = "offset"
|
||||
CONF_OPTIMISTIC: Final = "optimistic"
|
||||
CONF_OPTIONS: Final = "options"
|
||||
|
||||
@@ -813,7 +813,7 @@ class EntityNumericalConditionBase(EntityConditionBase):
|
||||
if lower_limit is None or upper_limit is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
between = lower_limit < value < upper_limit
|
||||
between = lower_limit <= value <= upper_limit
|
||||
if self._threshold_type == NumericThresholdType.BETWEEN:
|
||||
return between
|
||||
return not between
|
||||
|
||||
@@ -38,7 +38,6 @@ from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_BELOW,
|
||||
CONF_CHOOSE,
|
||||
CONF_COMMENT,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_CONTINUE_ON_ERROR,
|
||||
@@ -61,6 +60,7 @@ from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_IF,
|
||||
CONF_MATCH,
|
||||
CONF_NOTE,
|
||||
CONF_PARALLEL,
|
||||
CONF_PLATFORM,
|
||||
CONF_REPEAT,
|
||||
@@ -1459,7 +1459,7 @@ SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
|
||||
|
||||
SCRIPT_ACTION_BASE_SCHEMA: VolDictType = {
|
||||
vol.Optional(CONF_ALIAS): string,
|
||||
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
|
||||
vol.Remove(CONF_NOTE): str, # Is only used in frontend
|
||||
vol.Optional(CONF_CONTINUE_ON_ERROR): boolean,
|
||||
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
|
||||
}
|
||||
@@ -1527,7 +1527,7 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any(
|
||||
|
||||
CONDITION_BASE_SCHEMA: VolDictType = {
|
||||
vol.Optional(CONF_ALIAS): string,
|
||||
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
|
||||
vol.Remove(CONF_NOTE): str, # Is only used in frontend
|
||||
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
|
||||
}
|
||||
|
||||
@@ -1862,7 +1862,7 @@ TRIGGER_BASE_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_ID): str,
|
||||
vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
|
||||
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
|
||||
vol.Remove(CONF_NOTE): str, # Is only used in frontend
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -59,13 +59,17 @@ def request_handler_factory(
|
||||
# Import here to avoid circular dependency with network.py
|
||||
from .network import NoURLAvailableError, get_url # noqa: PLC0415
|
||||
|
||||
# Get the current request header to include as resource metadata
|
||||
# endpoint for RFC9728. We currently prefer external since this
|
||||
# is likely most used by remote OAuth clients
|
||||
try:
|
||||
url_prefix = get_url(hass, require_current_request=True)
|
||||
url_prefix = get_url(
|
||||
hass, require_current_request=True, prefer_external=True
|
||||
)
|
||||
except NoURLAvailableError:
|
||||
# Omit header to avoid leaking configured URLs
|
||||
raise HTTPUnauthorized from None
|
||||
raise HTTPUnauthorized(
|
||||
# Include resource metadata endpoint for RFC9728
|
||||
headers={
|
||||
"WWW-Authenticate": (
|
||||
f'Bearer resource_metadata="{url_prefix}'
|
||||
|
||||
@@ -432,7 +432,7 @@ class AutomationBehavior(StrEnum):
|
||||
|
||||
ALL = "all"
|
||||
FIRST = "first"
|
||||
LAST = "last"
|
||||
EACH = "each"
|
||||
ANY = "any"
|
||||
|
||||
|
||||
@@ -446,8 +446,8 @@ class AutomationBehaviorSelectorMode(StrEnum):
|
||||
_AUTOMATION_BEHAVIOR_MODES: dict[AutomationBehaviorSelectorMode, list[str]] = {
|
||||
AutomationBehaviorSelectorMode.TRIGGER: [
|
||||
AutomationBehavior.FIRST,
|
||||
AutomationBehavior.LAST,
|
||||
AutomationBehavior.ANY,
|
||||
AutomationBehavior.ALL,
|
||||
AutomationBehavior.EACH,
|
||||
],
|
||||
AutomationBehaviorSelectorMode.CONDITION: [
|
||||
AutomationBehavior.ALL,
|
||||
|
||||
@@ -327,8 +327,8 @@ class Trigger(abc.ABC):
|
||||
|
||||
ATTR_BEHAVIOR: Final = "behavior"
|
||||
BEHAVIOR_FIRST: Final = "first"
|
||||
BEHAVIOR_LAST: Final = "last"
|
||||
BEHAVIOR_ANY: Final = "any"
|
||||
BEHAVIOR_ALL: Final = "all"
|
||||
BEHAVIOR_EACH: Final = "each"
|
||||
|
||||
ENTITY_STATE_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -337,11 +337,11 @@ ENTITY_STATE_TRIGGER_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default={}): {
|
||||
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
|
||||
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_EACH): vol.In(
|
||||
[BEHAVIOR_FIRST, BEHAVIOR_ALL, BEHAVIOR_EACH]
|
||||
),
|
||||
vol.Optional(CONF_FOR): cv.positive_time_period,
|
||||
},
|
||||
@@ -361,7 +361,7 @@ class EntityTriggerBase(Trigger):
|
||||
# `_excluded_states`. Subclasses can override to relax the origin
|
||||
# check.
|
||||
_excluded_from_states: ClassVar[frozenset[str]] = _excluded_states
|
||||
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST
|
||||
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR
|
||||
# When True, indirect target expansion (via device/area/floor) skips
|
||||
# entities with an entity_category.
|
||||
_primary_entities_only: ClassVar[bool] = True
|
||||
@@ -454,7 +454,7 @@ class EntityTriggerBase(Trigger):
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
behavior: str = self._options.get(ATTR_BEHAVIOR, BEHAVIOR_ANY)
|
||||
behavior: str = self._options.get(ATTR_BEHAVIOR, BEHAVIOR_EACH)
|
||||
unsub_track_same: dict[str, Callable[[], None]] = {}
|
||||
|
||||
@callback
|
||||
@@ -474,10 +474,10 @@ class EntityTriggerBase(Trigger):
|
||||
|
||||
Called by async_track_same_state on each state change to
|
||||
determine whether to cancel the timer.
|
||||
For behavior any, checks the individual entity's state.
|
||||
For behavior first/last, checks the combined state.
|
||||
For behavior each, checks the individual entity's state.
|
||||
For behavior first/all, checks the combined state.
|
||||
"""
|
||||
if behavior == BEHAVIOR_LAST:
|
||||
if behavior == BEHAVIOR_ALL:
|
||||
matches, included = self.count_matches(
|
||||
target_state_change_data.targeted_entity_ids
|
||||
)
|
||||
@@ -492,7 +492,7 @@ class EntityTriggerBase(Trigger):
|
||||
target_state_change_data.targeted_entity_ids
|
||||
)
|
||||
return matches >= 1
|
||||
# Behavior any: check the individual entity's state
|
||||
# Behavior each: check the individual entity's state
|
||||
if not to_state or to_state.state in self._excluded_states:
|
||||
return False
|
||||
return self.is_valid_state(to_state)
|
||||
@@ -515,7 +515,7 @@ class EntityTriggerBase(Trigger):
|
||||
):
|
||||
return
|
||||
|
||||
if behavior == BEHAVIOR_LAST:
|
||||
if behavior == BEHAVIOR_ALL:
|
||||
matches, included = self.count_matches(
|
||||
target_state_change_data.targeted_entity_ids
|
||||
)
|
||||
@@ -553,7 +553,7 @@ class EntityTriggerBase(Trigger):
|
||||
call_action()
|
||||
return
|
||||
|
||||
subscription_key = entity_id if behavior == BEHAVIOR_ANY else behavior
|
||||
subscription_key = entity_id if behavior == BEHAVIOR_EACH else behavior
|
||||
if subscription_key in unsub_track_same:
|
||||
unsub_track_same.pop(subscription_key)()
|
||||
unsub_track_same[subscription_key] = async_track_same_state(
|
||||
@@ -563,7 +563,7 @@ class EntityTriggerBase(Trigger):
|
||||
state_still_valid,
|
||||
entity_ids=(
|
||||
entity_id
|
||||
if behavior == BEHAVIOR_ANY
|
||||
if behavior == BEHAVIOR_EACH
|
||||
else target_state_change_data.targeted_entity_ids
|
||||
),
|
||||
)
|
||||
@@ -760,7 +760,7 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
|
||||
if lower_limit is None or upper_limit is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
between = lower_limit < current_value < upper_limit
|
||||
between = lower_limit <= current_value <= upper_limit
|
||||
if self._threshold_type == NumericThresholdType.BETWEEN:
|
||||
return between
|
||||
return not between
|
||||
@@ -871,7 +871,7 @@ class EntityNumericalStateChangedTriggerWithUnitBase(
|
||||
|
||||
|
||||
NUMERICAL_ATTRIBUTE_CROSSED_THRESHOLD_SCHEMA = (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required("threshold"): NumericThresholdSelector(
|
||||
@@ -905,7 +905,7 @@ def _make_numerical_state_crossed_threshold_with_unit_schema(
|
||||
This trigger only fires when the observed attribute
|
||||
changes from not within to within the defined threshold.
|
||||
"""
|
||||
return ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||
return ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default={}): {
|
||||
vol.Required("threshold"): NumericThresholdSelector(
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.7.9
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260429.4
|
||||
home-assistant-frontend==20260527.0
|
||||
home-assistant-intents==2026.5.5
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -70,7 +70,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.15
|
||||
uv==0.11.16
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260429.4"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.0"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.0.dev0"
|
||||
version = "2026.6.0b0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -74,7 +74,7 @@ dependencies = [
|
||||
"typing-extensions>=4.15.0,<5.0",
|
||||
"ulid-transform==2.2.9",
|
||||
"urllib3>=2.0",
|
||||
"uv==0.11.15",
|
||||
"uv==0.11.16",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.7.0",
|
||||
"voluptuous-openapi==0.3.0",
|
||||
@@ -648,7 +648,7 @@ exclude_lines = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
required-version = ">=0.15.13"
|
||||
required-version = ">=0.15.14"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
|
||||
Generated
+1
-1
@@ -55,7 +55,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.15.0,<5.0
|
||||
ulid-transform==2.2.9
|
||||
urllib3>=2.0
|
||||
uv==0.11.15
|
||||
uv==0.11.16
|
||||
voluptuous-openapi==0.3.0
|
||||
voluptuous-serialize==2.7.0
|
||||
voluptuous==0.15.2
|
||||
|
||||
Generated
+2
-2
@@ -1266,7 +1266,7 @@ hole==0.9.0
|
||||
holidays==0.97
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260429.4
|
||||
home-assistant-frontend==20260527.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.5.5
|
||||
@@ -3207,7 +3207,7 @@ ttls==1.8.3
|
||||
ttn_client==1.3.0
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-handlers==0.0.19
|
||||
tuya-device-handlers==0.0.21
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.2.8
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
|
||||
|
||||
codespell==2.4.2
|
||||
ruff==0.15.13
|
||||
ruff==0.15.14
|
||||
yamllint==1.38.0
|
||||
zizmor==1.24.1
|
||||
|
||||
@@ -21,9 +21,9 @@ from homeassistant.core import HomeAssistant
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_numerical_state_value_changed_trigger_states,
|
||||
@@ -229,7 +229,7 @@ async def test_air_quality_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_trigger_binary_sensor_behavior_any(
|
||||
async def test_air_quality_trigger_binary_sensor_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -243,7 +243,7 @@ async def test_air_quality_trigger_binary_sensor_behavior_any(
|
||||
|
||||
Covers gas, CO, and smoke device classes.
|
||||
"""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -390,7 +390,7 @@ async def test_air_quality_trigger_binary_sensor_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_trigger_binary_sensor_behavior_last(
|
||||
async def test_air_quality_trigger_binary_sensor_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -400,8 +400,8 @@ async def test_air_quality_trigger_binary_sensor_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality trigger fires when the last binary_sensor changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test air quality trigger fires when all binary_sensors have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -570,7 +570,7 @@ async def test_air_quality_trigger_binary_sensor_behavior_last(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_trigger_sensor_behavior_any(
|
||||
async def test_air_quality_trigger_sensor_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -581,7 +581,7 @@ async def test_air_quality_trigger_sensor_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality trigger fires for sensor entities."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -789,7 +789,7 @@ async def test_air_quality_trigger_sensor_crossed_threshold_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_trigger_sensor_crossed_threshold_behavior_last(
|
||||
async def test_air_quality_trigger_sensor_crossed_threshold_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -803,7 +803,7 @@ async def test_air_quality_trigger_sensor_crossed_threshold_behavior_last(
|
||||
|
||||
Fires when the last sensor changes state.
|
||||
"""
|
||||
await assert_trigger_behavior_last(
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
other_states,
|
||||
@@ -153,7 +153,7 @@ async def test_alarm_control_panel_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_alarm_control_panel_state_trigger_behavior_any(
|
||||
async def test_alarm_control_panel_state_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_alarm_control_panels: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -168,7 +168,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
|
||||
Fires when any alarm control panel state changes to a
|
||||
specific state.
|
||||
"""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_alarm_control_panels,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -353,7 +353,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_alarm_control_panel_state_trigger_behavior_last(
|
||||
async def test_alarm_control_panel_state_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_alarm_control_panels: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -368,7 +368,7 @@ async def test_alarm_control_panel_state_trigger_behavior_last(
|
||||
Fires when the last alarm_control_panel changes to a
|
||||
specific state.
|
||||
"""
|
||||
await assert_trigger_behavior_last(
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_alarm_control_panels,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
other_states,
|
||||
@@ -100,7 +100,7 @@ async def test_assist_satellite_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_assist_satellite_state_trigger_behavior_any(
|
||||
async def test_assist_satellite_state_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_assist_satellites: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -111,7 +111,7 @@ async def test_assist_satellite_state_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test assist satellite trigger fires when any satellite changes state."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_assist_satellites,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -206,7 +206,7 @@ async def test_assist_satellite_state_trigger_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_assist_satellite_state_trigger_behavior_last(
|
||||
async def test_assist_satellite_state_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_assist_satellites: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -217,7 +217,7 @@ async def test_assist_satellite_state_trigger_behavior_last(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test assist satellite trigger fires when last satellite changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_assist_satellites,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -16,9 +16,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_numerical_state_value_changed_trigger_states,
|
||||
@@ -134,7 +134,7 @@ async def test_battery_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_binary_sensor_trigger_behavior_any(
|
||||
async def test_battery_binary_sensor_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -144,8 +144,8 @@ async def test_battery_binary_sensor_trigger_behavior_any(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test the battery binary sensor triggers with 'any' behavior."""
|
||||
await assert_trigger_behavior_any(
|
||||
"""Test the battery binary sensor triggers with 'each' behavior."""
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -256,7 +256,7 @@ async def test_battery_binary_sensor_trigger_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_binary_sensor_trigger_behavior_last(
|
||||
async def test_battery_binary_sensor_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -266,8 +266,8 @@ async def test_battery_binary_sensor_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test the battery binary sensor triggers with 'last' behavior."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test the battery binary sensor triggers with 'all' behavior."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -299,7 +299,7 @@ async def test_battery_binary_sensor_trigger_behavior_last(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_sensor_trigger_behavior_any(
|
||||
async def test_battery_sensor_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -309,8 +309,8 @@ async def test_battery_sensor_trigger_behavior_any(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test battery sensor triggers with 'any' behavior."""
|
||||
await assert_trigger_behavior_any(
|
||||
"""Test battery sensor triggers with 'each' behavior."""
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -375,7 +375,7 @@ async def test_battery_level_crossed_threshold_sensor_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_level_crossed_threshold_sensor_behavior_last(
|
||||
async def test_battery_level_crossed_threshold_sensor_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -385,8 +385,8 @@ async def test_battery_level_crossed_threshold_sensor_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test trigger fires when the last sensor changes."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test trigger fires when all sensors have changed."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -25,9 +25,9 @@ from homeassistant.helpers.trigger import async_validate_trigger_config
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
other_states,
|
||||
@@ -211,7 +211,7 @@ async def test_climate_trigger_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_trigger_behavior_any(
|
||||
async def test_climate_state_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -222,7 +222,7 @@ async def test_climate_state_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test climate trigger fires on any state change."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -285,7 +285,7 @@ async def test_climate_state_trigger_behavior_any(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_attribute_trigger_behavior_any(
|
||||
async def test_climate_state_attribute_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -296,7 +296,7 @@ async def test_climate_state_attribute_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test climate attribute trigger fires on any change."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -462,7 +462,7 @@ async def test_climate_state_attribute_trigger_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_trigger_behavior_last(
|
||||
async def test_climate_state_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -472,8 +472,8 @@ async def test_climate_state_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test climate trigger fires on last state change."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test climate trigger fires when all climate entities have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -523,7 +523,7 @@ async def test_climate_state_trigger_behavior_last(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_attribute_trigger_behavior_last(
|
||||
async def test_climate_state_attribute_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_climates: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -533,8 +533,8 @@ async def test_climate_state_attribute_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[tuple[tuple[str, dict], int]],
|
||||
) -> None:
|
||||
"""Test climate attribute trigger fires on last change."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test climate attribute trigger fires when all climate entities have changed."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_climates,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
+29
-29
@@ -1062,8 +1062,8 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * s} | unit_attributes),
|
||||
(state, {attribute: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
other_invalid_attr,
|
||||
@@ -1092,8 +1092,8 @@ def parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
],
|
||||
other_states=[
|
||||
other_invalid_attr,
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 60 * s} | unit_attributes),
|
||||
(state, {attribute: 10 * s} | unit_attributes),
|
||||
(state, {attribute: 90 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -1262,7 +1262,7 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
},
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[("50", unit_attributes), ("60", unit_attributes)],
|
||||
target_states=[("10", unit_attributes), ("90", unit_attributes)],
|
||||
other_states=[
|
||||
("none", unit_attributes),
|
||||
("0", unit_attributes),
|
||||
@@ -1287,8 +1287,8 @@ def parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
target_states=[("0", unit_attributes), ("100", unit_attributes)],
|
||||
other_states=[
|
||||
("none", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("60", unit_attributes),
|
||||
("10", unit_attributes),
|
||||
("90", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
trigger_from_none=False,
|
||||
@@ -1632,7 +1632,7 @@ async def assert_trigger_options_supported(
|
||||
return {**(base_options or {}), **extra}
|
||||
|
||||
# Behavior
|
||||
for behavior in ("any", "first", "last"):
|
||||
for behavior in ("each", "first", "all"):
|
||||
await _validate_trigger_options(
|
||||
hass, trigger, _merge({"behavior": behavior}), valid=supports_behavior
|
||||
)
|
||||
@@ -1750,7 +1750,7 @@ async def assert_condition_behavior_all(
|
||||
assert cond.async_check() == state["condition_true"]
|
||||
|
||||
|
||||
async def assert_trigger_behavior_any(
|
||||
async def assert_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
target_entities: dict[str, list[str]],
|
||||
@@ -1761,7 +1761,7 @@ async def assert_trigger_behavior_any(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test trigger fires in mode any."""
|
||||
"""Test trigger fires in mode each."""
|
||||
calls: list[str] = []
|
||||
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
|
||||
@@ -1855,7 +1855,7 @@ async def assert_trigger_behavior_first(
|
||||
assert len(calls) == 0
|
||||
|
||||
|
||||
async def assert_trigger_behavior_last(
|
||||
async def assert_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
target_entities: dict[str, list[str]],
|
||||
@@ -1866,7 +1866,7 @@ async def assert_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test trigger fires in mode last."""
|
||||
"""Test trigger fires in mode all."""
|
||||
calls: list[str] = []
|
||||
other_entity_ids = set(target_entities["included_entities"]) - {entity_id}
|
||||
excluded_entity_ids = set(target_entities["excluded_entities"]) - {entity_id}
|
||||
@@ -1881,7 +1881,7 @@ async def assert_trigger_behavior_last(
|
||||
await arm_trigger(
|
||||
hass,
|
||||
trigger,
|
||||
{"behavior": "last"} | trigger_options,
|
||||
{"behavior": "all"} | trigger_options,
|
||||
trigger_target_config,
|
||||
calls,
|
||||
)
|
||||
@@ -2016,14 +2016,14 @@ def parametrize_numerical_condition_above_below_any(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
("21", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("100", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -2134,14 +2134,14 @@ def parametrize_numerical_condition_above_below_all(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
("21", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("50", unit_attributes),
|
||||
("79", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
("0", unit_attributes),
|
||||
("20", unit_attributes),
|
||||
("80", unit_attributes),
|
||||
("19", unit_attributes),
|
||||
("81", unit_attributes),
|
||||
("100", unit_attributes),
|
||||
],
|
||||
required_filter_attributes=required_filter_attributes,
|
||||
@@ -2281,14 +2281,14 @@ def parametrize_numerical_attribute_condition_above_below_any(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 100 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
@@ -2428,14 +2428,14 @@ def parametrize_numerical_attribute_condition_above_below_all(
|
||||
threshold_unit,
|
||||
),
|
||||
target_states=[
|
||||
(state, {attribute: 21 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 50 * s} | unit_attributes),
|
||||
(state, {attribute: 79 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0 * s} | unit_attributes),
|
||||
(state, {attribute: 20 * s} | unit_attributes),
|
||||
(state, {attribute: 80 * s} | unit_attributes),
|
||||
(state, {attribute: 19 * s} | unit_attributes),
|
||||
(state, {attribute: 81 * s} | unit_attributes),
|
||||
(state, {attribute: 100 * s} | unit_attributes),
|
||||
],
|
||||
extra_excluded_states=extra_excluded_states,
|
||||
|
||||
@@ -98,8 +98,8 @@ async def test_counter_condition_options_validation(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "100"],
|
||||
target_states=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="counter.is_value",
|
||||
@@ -110,8 +110,8 @@ async def test_counter_condition_options_validation(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
target_states=["0", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -171,8 +171,8 @@ async def test_counter_is_value_condition_behavior_any(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["11", "20", "29"],
|
||||
other_states=["0", "10", "30", "100"],
|
||||
target_states=["10", "11", "20", "29", "30"],
|
||||
other_states=["0", "9", "31", "100"],
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="counter.is_value",
|
||||
@@ -183,8 +183,8 @@ async def test_counter_is_value_condition_behavior_any(
|
||||
"value_max": {"number": 30},
|
||||
},
|
||||
},
|
||||
target_states=["0", "10", "30", "100"],
|
||||
other_states=["11", "20", "29"],
|
||||
target_states=["0", "9", "31", "100"],
|
||||
other_states=["10", "11", "20", "29", "30"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@ from tests.components.common import (
|
||||
BasicTriggerStateDescription,
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_target_entities,
|
||||
@@ -191,7 +191,7 @@ async def test_counter_state_trigger(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"), BEHAVIOR_AWARE_TRIGGERS
|
||||
)
|
||||
async def test_counter_state_trigger_behavior_any(
|
||||
async def test_counter_state_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_counters: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -202,7 +202,7 @@ async def test_counter_state_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test counter trigger fires on any state change."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_counters,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -253,7 +253,7 @@ async def test_counter_state_trigger_behavior_first(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"), BEHAVIOR_AWARE_TRIGGERS
|
||||
)
|
||||
async def test_counter_state_trigger_behavior_last(
|
||||
async def test_counter_state_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_counters: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -263,8 +263,8 @@ async def test_counter_state_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test counter trigger fires on last state change."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test counter trigger fires when all counters have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_counters,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -10,9 +10,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_target_entities,
|
||||
@@ -139,7 +139,7 @@ async def test_cover_trigger_options_validation(
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_cover_trigger_behavior_any(
|
||||
async def test_cover_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -150,7 +150,7 @@ async def test_cover_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test cover trigger fires for cover entities with matching device_class."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -283,7 +283,7 @@ async def test_cover_trigger_behavior_first(
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_cover_trigger_behavior_last(
|
||||
async def test_cover_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -293,8 +293,8 @@ async def test_cover_trigger_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test cover trigger fires when the last cover changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test cover trigger fires when all covers have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import (
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONNECTED_DEVICE_REGISTERED,
|
||||
DOMAIN,
|
||||
BaseScannerEntity,
|
||||
@@ -33,7 +34,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -907,6 +912,328 @@ async def test_base_scanner_entity_in_zones_when_connected(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_option(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the associated_zone entity option overrides which zone in_zones reports.
|
||||
|
||||
The scanner reports being connected to a non-default zone; state and in_zones
|
||||
must follow the configured zone, and a zone enclosing the configured one is
|
||||
included in in_zones too.
|
||||
"""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Default: no option set -> associated with zone.home.
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]
|
||||
|
||||
# Set the option -> associated_zone replaces zone.home; zone.home now shows
|
||||
# up via the enclosing-zones lookup.
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
# zone.kitchen is the configured zone -> state is the zone's name.
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
|
||||
# Clearing the option falls back to zone.home.
|
||||
entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.home"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.home"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_removed_after_set(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test scanner state and repair issue when associated zone is removed.
|
||||
|
||||
When the user picks a zone via the associated_zone option and then deletes
|
||||
that zone, the scanner falls back to ``state == "unknown"`` and a repair
|
||||
issue is opened prompting the user to reconfigure.
|
||||
"""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Sanity check before removal.
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
issue_id = f"associated_zone_missing_{entity_entry.id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
# Remove the associated zone.
|
||||
hass.states.async_remove("zone.kitchen")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNKNOWN
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == []
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue is not None
|
||||
assert issue.severity is ir.IssueSeverity.WARNING
|
||||
assert issue.translation_key == "associated_zone_missing"
|
||||
assert issue.translation_placeholders == {
|
||||
"entity_id": entity_id,
|
||||
"zone": "zone.kitchen",
|
||||
}
|
||||
|
||||
# Restore the zone -> issue is cleared, state recovers.
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_missing_at_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test repair issue is created when the configured zone is missing at setup."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Pre-register the entity option pointing at a zone that does not exist.
|
||||
entity_entry = entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
TEST_DOMAIN,
|
||||
base_scanner_entity.unique_id,
|
||||
suggested_object_id="entity1",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_UNKNOWN
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == []
|
||||
issue_id = f"associated_zone_missing_{entity_entry.id}"
|
||||
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert issue is not None
|
||||
assert issue.translation_placeholders == {
|
||||
"entity_id": entity_id,
|
||||
"zone": "zone.never_existed",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_issue_cleared_on_option_change(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the repair issue is cleared when the user clears the option."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
issue_id = f"associated_zone_missing_{entity_entry.id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# Clearing the option restores the default and clears the repair issue.
|
||||
entity_registry.async_update_entity_options(entity_id, DOMAIN, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == STATE_HOME
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_issue_cleared_on_unload(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test the repair issue is cleared when the entity is removed from hass."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.never_existed"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
assert entity_entry
|
||||
issue_id = f"associated_zone_missing_{entity_entry.id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unique_id", ["unique_scanner"])
|
||||
async def test_base_scanner_entity_associated_zone_option_set_before_add(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
base_scanner_entity: MockBaseScannerEntity,
|
||||
) -> None:
|
||||
"""Test associated_zone option set before the entity is added is honored."""
|
||||
hass.states.async_set(
|
||||
"zone.home",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 1000},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"zone.kitchen",
|
||||
"0",
|
||||
{ATTR_LATITUDE: 50.0, ATTR_LONGITUDE: 60.0, ATTR_RADIUS: 50},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Pre-register the entity with the option set before the platform is set up.
|
||||
entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
TEST_DOMAIN,
|
||||
base_scanner_entity.unique_id,
|
||||
suggested_object_id="entity1",
|
||||
)
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
DOMAIN,
|
||||
{CONF_ASSOCIATED_ZONE: "zone.kitchen"},
|
||||
)
|
||||
|
||||
base_scanner_entity._connected = True
|
||||
config_entry = await create_mock_platform(hass, config_entry, [base_scanner_entity])
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert base_scanner_entity._scanner_option_associated_zone == "zone.kitchen"
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == "kitchen"
|
||||
assert entity_state.attributes[ATTR_IN_ZONES] == ["zone.kitchen", "zone.home"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ip_address", "mac_address", "hostname"),
|
||||
[("0.0.0.0", "ad:de:ef:be:ed:fe", "test.hostname.org")],
|
||||
|
||||
@@ -11,9 +11,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_target_entities,
|
||||
@@ -101,7 +101,7 @@ async def test_door_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_door_trigger_binary_sensor_behavior_any(
|
||||
async def test_door_trigger_binary_sensor_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -112,7 +112,7 @@ async def test_door_trigger_binary_sensor_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test door trigger fires for binary_sensor entities with device_class door."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -169,7 +169,7 @@ async def test_door_trigger_binary_sensor_behavior_any(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_door_trigger_cover_behavior_any(
|
||||
async def test_door_trigger_cover_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -180,7 +180,7 @@ async def test_door_trigger_cover_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test door trigger fires for cover entities with device_class door."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -271,7 +271,7 @@ async def test_door_trigger_binary_sensor_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_door_trigger_binary_sensor_behavior_last(
|
||||
async def test_door_trigger_binary_sensor_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -281,8 +281,8 @@ async def test_door_trigger_binary_sensor_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test door trigger fires when the last binary_sensor changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test door trigger fires when all binary_sensors have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -407,7 +407,7 @@ async def test_door_trigger_cover_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_door_trigger_cover_behavior_last(
|
||||
async def test_door_trigger_cover_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_covers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -417,8 +417,8 @@ async def test_door_trigger_cover_behavior_last(
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test door trigger fires when the last cover changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
"""Test door trigger fires when all covers have changed state."""
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_covers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
@@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
TriggerStateDescription,
|
||||
assert_trigger_behavior_any,
|
||||
assert_trigger_behavior_all,
|
||||
assert_trigger_behavior_each,
|
||||
assert_trigger_behavior_first,
|
||||
assert_trigger_behavior_last,
|
||||
assert_trigger_gated_by_labs_flag,
|
||||
assert_trigger_options_supported,
|
||||
parametrize_target_entities,
|
||||
@@ -85,7 +85,7 @@ async def test_fan_trigger_options_validation(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_fan_state_trigger_behavior_any(
|
||||
async def test_fan_state_trigger_behavior_each(
|
||||
hass: HomeAssistant,
|
||||
target_fans: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -96,7 +96,7 @@ async def test_fan_state_trigger_behavior_any(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test the fan state trigger fires on any fan state change."""
|
||||
await assert_trigger_behavior_any(
|
||||
await assert_trigger_behavior_each(
|
||||
hass,
|
||||
target_entities=target_fans,
|
||||
trigger_target_config=trigger_target_config,
|
||||
@@ -171,7 +171,7 @@ async def test_fan_state_trigger_behavior_first(
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_fan_state_trigger_behavior_last(
|
||||
async def test_fan_state_trigger_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_fans: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
@@ -182,7 +182,7 @@ async def test_fan_state_trigger_behavior_last(
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test the fan trigger fires on the last fan state change."""
|
||||
await assert_trigger_behavior_last(
|
||||
await assert_trigger_behavior_all(
|
||||
hass,
|
||||
target_entities=target_fans,
|
||||
trigger_target_config=trigger_target_config,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user