mirror of
https://github.com/home-assistant/core.git
synced 2026-01-21 06:57:01 +01:00
Compare commits
52 Commits
frontend-p
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a447c1b42e | ||
|
|
50211f75ed | ||
|
|
27117c9d17 | ||
|
|
7c4cdd57b6 | ||
|
|
6af5698645 | ||
|
|
75db2cde40 | ||
|
|
329dd05434 | ||
|
|
53c53d03e0 | ||
|
|
360b394d03 | ||
|
|
a663d55632 | ||
|
|
3fd266a513 | ||
|
|
442c1d6242 | ||
|
|
0e2aae02f6 | ||
|
|
3227a6e49f | ||
|
|
9d0cfb628b | ||
|
|
4578fe0260 | ||
|
|
0d92708108 | ||
|
|
cceb50071b | ||
|
|
62f296c9dd | ||
|
|
ea1f280494 | ||
|
|
67108a2fc8 | ||
|
|
1ccbd5124e | ||
|
|
818af90a7b | ||
|
|
23bc78fa25 | ||
|
|
0b1cc7638f | ||
|
|
c291a2fbc1 | ||
|
|
7379a4ff4b | ||
|
|
ddcf5cb749 | ||
|
|
4b10a542b0 | ||
|
|
beea9fa74b | ||
|
|
ce8fd16456 | ||
|
|
2172d15489 | ||
|
|
0cfa0ed670 | ||
|
|
f6839913d8 | ||
|
|
8fa01497ee | ||
|
|
e077c65a77 | ||
|
|
7c49656fa8 | ||
|
|
1730479c8d | ||
|
|
bc28c8fd3c | ||
|
|
c3616fd5df | ||
|
|
6b97f2ac06 | ||
|
|
deefcbcbe4 | ||
|
|
e84aeb9f99 | ||
|
|
ade3d8a657 | ||
|
|
a65d9032ff | ||
|
|
b950a4eaf4 | ||
|
|
3fe91751f5 | ||
|
|
6ee58b96ca | ||
|
|
d1404e7905 | ||
|
|
7c34191813 | ||
|
|
7540d04779 | ||
|
|
d828130670 |
@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this AdGuard Home instance."""
|
||||
if self._entry.source == SOURCE_HASSIO:
|
||||
config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
|
||||
config_url = "homeassistant://app/a0d7b954_adguard"
|
||||
elif self.adguard.tls:
|
||||
config_url = f"https://{self.adguard.host}:{self.adguard.port}"
|
||||
else:
|
||||
|
||||
@@ -127,6 +127,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"assist_satellite",
|
||||
"fan",
|
||||
"light",
|
||||
"siren",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
@@ -601,6 +602,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Return a set of referenced labels."""
|
||||
referenced = self.action_script.referenced_labels
|
||||
|
||||
if self._cond_func is not None:
|
||||
for conf in self._cond_func.config:
|
||||
referenced |= condition.async_extract_labels(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
|
||||
return referenced
|
||||
@@ -610,6 +615,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Return a set of referenced floors."""
|
||||
referenced = self.action_script.referenced_floors
|
||||
|
||||
if self._cond_func is not None:
|
||||
for conf in self._cond_func.config:
|
||||
referenced |= condition.async_extract_floors(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
|
||||
return referenced
|
||||
@@ -619,6 +628,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Return a set of referenced areas."""
|
||||
referenced = self.action_script.referenced_areas
|
||||
|
||||
if self._cond_func is not None:
|
||||
for conf in self._cond_func.config:
|
||||
referenced |= condition.async_extract_areas(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
|
||||
return referenced
|
||||
|
||||
@@ -26,7 +26,6 @@ EXCLUDE_FROM_BACKUP = [
|
||||
"tmp_backups/*.tar",
|
||||
"OZW_Log.txt",
|
||||
"tts/*",
|
||||
"frontend_development_pr/*",
|
||||
]
|
||||
|
||||
EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.10.0"],
|
||||
"requirements": ["hass-nabucasa==1.11.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "deconz"
|
||||
|
||||
HASSIO_CONFIGURATION_URL = "homeassistant://hassio/ingress/core_deconz"
|
||||
HASSIO_CONFIGURATION_URL = "homeassistant://app/core_deconz"
|
||||
|
||||
CONF_BRIDGE_ID = "bridgeid"
|
||||
CONF_GROUP_ID_BASE = "group_id_base"
|
||||
|
||||
@@ -1034,7 +1034,7 @@ def _async_setup_device_registry(
|
||||
and dashboard.data
|
||||
and dashboard.data.get(device_info.name)
|
||||
):
|
||||
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
|
||||
configuration_url = f"homeassistant://app/{dashboard.addon_slug}"
|
||||
|
||||
manufacturer = "espressif"
|
||||
if device_info.manufacturer:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.11"]
|
||||
"requirements": ["pyfirefly==0.1.12"]
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .pr_download import download_pr_artifact
|
||||
from .storage import async_setup_frontend_storage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -53,10 +52,6 @@ CONF_EXTRA_MODULE_URL = "extra_module_url"
|
||||
CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5"
|
||||
CONF_FRONTEND_REPO = "development_repo"
|
||||
CONF_JS_VERSION = "javascript_version"
|
||||
CONF_DEVELOPMENT_PR = "development_pr"
|
||||
CONF_GITHUB_TOKEN = "github_token"
|
||||
|
||||
PR_CACHE_DIR = "frontend_development_pr"
|
||||
|
||||
DEFAULT_THEME_COLOR = "#2980b9"
|
||||
|
||||
@@ -134,9 +129,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_FRONTEND_REPO): cv.path,
|
||||
vol.Optional(CONF_DEVELOPMENT_PR): cv.positive_int,
|
||||
vol.Optional(CONF_GITHUB_TOKEN): cv.string,
|
||||
vol.Optional(CONF_FRONTEND_REPO): cv.isdir,
|
||||
vol.Optional(CONF_THEMES): vol.All(dict, _validate_themes),
|
||||
vol.Optional(CONF_EXTRA_MODULE_URL): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
@@ -401,17 +394,7 @@ def add_manifest_json_key(key: str, val: Any) -> None:
|
||||
def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
|
||||
"""Return root path to the frontend files."""
|
||||
if dev_repo_path is not None:
|
||||
dev_frontend_path = pathlib.Path(dev_repo_path) / "hass_frontend"
|
||||
if dev_frontend_path.exists() and dev_frontend_path.is_dir():
|
||||
_LOGGER.info("Using frontend development repo: %s", dev_repo_path)
|
||||
return dev_frontend_path
|
||||
|
||||
_LOGGER.error(
|
||||
"Frontend development repo path does not exist: %s, "
|
||||
"falling back to the integrated frontend",
|
||||
dev_repo_path,
|
||||
)
|
||||
|
||||
return pathlib.Path(dev_repo_path) / "hass_frontend"
|
||||
# Keep import here so that we can import frontend without installing reqs
|
||||
import hass_frontend # noqa: PLC0415
|
||||
|
||||
@@ -438,40 +421,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
key,
|
||||
)
|
||||
|
||||
# Handle development configuration with priority
|
||||
repo_path = conf.get(CONF_FRONTEND_REPO)
|
||||
dev_pr_number = conf.get(CONF_DEVELOPMENT_PR)
|
||||
|
||||
# Priority: development_repo > development_pr > integrated
|
||||
if repo_path and dev_pr_number:
|
||||
_LOGGER.warning(
|
||||
"Both development_repo and development_pr are configured. "
|
||||
"Using development_repo (takes precedence). "
|
||||
"Remove development_repo to use automatic PR download"
|
||||
)
|
||||
dev_pr_number = None # Disable PR download
|
||||
|
||||
if dev_pr_number:
|
||||
pr_cache_dir = pathlib.Path(hass.config.config_dir) / PR_CACHE_DIR
|
||||
github_token = conf.get(CONF_GITHUB_TOKEN)
|
||||
|
||||
# Download PR artifact
|
||||
dev_pr_dir = await download_pr_artifact(
|
||||
hass, dev_pr_number, github_token, pr_cache_dir
|
||||
)
|
||||
|
||||
if dev_pr_dir is None:
|
||||
_LOGGER.error(
|
||||
"Failed to download PR #%s, falling back to the integrated frontend",
|
||||
dev_pr_number,
|
||||
)
|
||||
repo_path = None
|
||||
else:
|
||||
# frontend_dir is .../frontend_development_pr/<pr_number>/hass_frontend
|
||||
# We need to pass .../frontend_development_pr/<pr_number> to _frontend_root
|
||||
repo_path = str(dev_pr_dir.parent)
|
||||
_LOGGER.info("Using frontend from PR #%s", dev_pr_number)
|
||||
|
||||
is_dev = repo_path is not None
|
||||
root_path = _frontend_root(repo_path)
|
||||
|
||||
|
||||
@@ -23,8 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20260107.2",
|
||||
"aiogithubapi==24.6.0"
|
||||
]
|
||||
"requirements": ["home-assistant-frontend==20260107.2"]
|
||||
}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
"""GitHub PR artifact download functionality for frontend development."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import pathlib
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from aiogithubapi import (
|
||||
GitHubAPI,
|
||||
GitHubAuthenticationException,
|
||||
GitHubException,
|
||||
GitHubNotFoundException,
|
||||
GitHubPermissionException,
|
||||
GitHubRatelimitException,
|
||||
)
|
||||
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
GITHUB_REPO = "home-assistant/frontend"
|
||||
ARTIFACT_NAME = "frontend-build"
|
||||
CACHE_WARNING_SIZE_MB = 500
|
||||
|
||||
# Error messages
|
||||
ERROR_INVALID_TOKEN = (
|
||||
"GitHub token is invalid or expired. "
|
||||
"Please check your github_token in the frontend configuration. "
|
||||
"Generate a new token at https://github.com/settings/tokens"
|
||||
)
|
||||
ERROR_RATE_LIMIT = (
|
||||
"GitHub API rate limit exceeded or token lacks permissions. "
|
||||
"Ensure your token has 'repo' or 'public_repo' scope"
|
||||
)
|
||||
|
||||
|
||||
def _get_directory_size_mb(directory: pathlib.Path) -> float:
|
||||
"""Calculate total size of directory in MB (runs in executor)."""
|
||||
total = sum(f.stat().st_size for f in directory.rglob("*") if f.is_file())
|
||||
return total / (1024 * 1024)
|
||||
|
||||
|
||||
async def _get_pr_head_sha(client: GitHubAPI, pr_number: int) -> str:
|
||||
"""Get the head SHA for the PR."""
|
||||
try:
|
||||
response = await client.generic(
|
||||
endpoint=f"/repos/home-assistant/frontend/pulls/{pr_number}",
|
||||
)
|
||||
return str(response.data["head"]["sha"])
|
||||
except GitHubAuthenticationException as err:
|
||||
raise HomeAssistantError(ERROR_INVALID_TOKEN) from err
|
||||
except (GitHubRatelimitException, GitHubPermissionException) as err:
|
||||
raise HomeAssistantError(ERROR_RATE_LIMIT) from err
|
||||
except GitHubNotFoundException as err:
|
||||
raise HomeAssistantError(
|
||||
f"PR #{pr_number} does not exist in repository {GITHUB_REPO}"
|
||||
) from err
|
||||
except GitHubException as err:
|
||||
raise HomeAssistantError(f"GitHub API error: {err}") from err
|
||||
|
||||
|
||||
async def _find_pr_artifact(client: GitHubAPI, pr_number: int, head_sha: str) -> str:
|
||||
"""Find the build artifact for the given PR and commit SHA.
|
||||
|
||||
Returns the artifact download URL.
|
||||
"""
|
||||
try:
|
||||
# Get workflow runs for the commit
|
||||
response = await client.generic(
|
||||
endpoint="/repos/home-assistant/frontend/actions/workflows/ci.yaml/runs",
|
||||
params={"head_sha": head_sha, "per_page": 10},
|
||||
)
|
||||
|
||||
# Find the most recent successful run for this commit
|
||||
for run in response.data.get("workflow_runs", []):
|
||||
if run["status"] == "completed" and run["conclusion"] == "success":
|
||||
# Get artifacts for this run
|
||||
artifacts_response = await client.generic(
|
||||
endpoint=f"/repos/home-assistant/frontend/actions/runs/{run['id']}/artifacts",
|
||||
)
|
||||
|
||||
# Find the frontend-build artifact
|
||||
for artifact in artifacts_response.data.get("artifacts", []):
|
||||
if artifact["name"] == ARTIFACT_NAME:
|
||||
_LOGGER.info(
|
||||
"Found artifact '%s' from CI run #%s",
|
||||
ARTIFACT_NAME,
|
||||
run["id"],
|
||||
)
|
||||
return str(artifact["archive_download_url"])
|
||||
|
||||
raise HomeAssistantError(
|
||||
f"No '{ARTIFACT_NAME}' artifact found for PR #{pr_number}. "
|
||||
"Possible reasons: CI has not run yet or is running, "
|
||||
"or the build failed, or the PR artifact expired. "
|
||||
f"Check https://github.com/{GITHUB_REPO}/pull/{pr_number}/checks"
|
||||
)
|
||||
except GitHubAuthenticationException as err:
|
||||
raise HomeAssistantError(ERROR_INVALID_TOKEN) from err
|
||||
except (GitHubRatelimitException, GitHubPermissionException) as err:
|
||||
raise HomeAssistantError(ERROR_RATE_LIMIT) from err
|
||||
except GitHubException as err:
|
||||
raise HomeAssistantError(f"GitHub API error: {err}") from err
|
||||
|
||||
|
||||
async def _download_artifact_data(
|
||||
hass: HomeAssistant, artifact_url: str, github_token: str
|
||||
) -> bytes:
|
||||
"""Download artifact data from GitHub."""
|
||||
session = async_get_clientsession(hass)
|
||||
headers = {
|
||||
"Authorization": f"token {github_token}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = await session.get(
|
||||
artifact_url, headers=headers, timeout=ClientTimeout(total=60)
|
||||
)
|
||||
response.raise_for_status()
|
||||
return await response.read()
|
||||
except ClientResponseError as err:
|
||||
if err.status == 401:
|
||||
raise HomeAssistantError(ERROR_INVALID_TOKEN) from err
|
||||
if err.status == 403:
|
||||
raise HomeAssistantError(ERROR_RATE_LIMIT) from err
|
||||
raise HomeAssistantError(
|
||||
f"Failed to download artifact: HTTP {err.status}"
|
||||
) from err
|
||||
except TimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
"Timeout downloading artifact (>60s). Check your network connection"
|
||||
) from err
|
||||
except ClientError as err:
|
||||
raise HomeAssistantError(f"Network error downloading artifact: {err}") from err
|
||||
|
||||
|
||||
def _extract_artifact(
|
||||
artifact_data: bytes,
|
||||
pr_dir: pathlib.Path,
|
||||
frontend_dir: pathlib.Path,
|
||||
head_sha: str,
|
||||
) -> None:
|
||||
"""Extract artifact and save SHA (runs in executor)."""
|
||||
if pr_dir.exists():
|
||||
shutil.rmtree(pr_dir)
|
||||
frontend_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(artifact_data)) as zip_file:
|
||||
zip_file.extractall(str(frontend_dir))
|
||||
|
||||
# Save the commit SHA for cache validation
|
||||
sha_file = pr_dir / ".sha"
|
||||
sha_file.write_text(head_sha)
|
||||
|
||||
|
||||
async def download_pr_artifact(
|
||||
hass: HomeAssistant,
|
||||
pr_number: int,
|
||||
github_token: str | None,
|
||||
cache_dir: pathlib.Path,
|
||||
) -> pathlib.Path | None:
|
||||
"""Download and extract frontend PR artifact from GitHub.
|
||||
|
||||
Returns the path to the extracted hass_frontend directory, or None on failure.
|
||||
"""
|
||||
# GitHub token is required to download artifacts
|
||||
if not github_token:
|
||||
_LOGGER.error(
|
||||
"GitHub token is required to download PR artifacts. "
|
||||
"Add 'github_token' to your frontend configuration"
|
||||
)
|
||||
return None
|
||||
|
||||
# Create GitHub API client
|
||||
client = GitHubAPI(
|
||||
token=github_token,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
# Get the current head SHA for this PR
|
||||
try:
|
||||
head_sha = await _get_pr_head_sha(client, pr_number)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("%s", err)
|
||||
return None
|
||||
|
||||
# Check if we have this exact version cached
|
||||
pr_dir = cache_dir / str(pr_number)
|
||||
frontend_dir = pr_dir / "hass_frontend"
|
||||
sha_file = pr_dir / ".sha"
|
||||
|
||||
# Check if cached version matches current commit
|
||||
if frontend_dir.exists() and sha_file.exists():
|
||||
cached_sha = await hass.async_add_executor_job(sha_file.read_text)
|
||||
if cached_sha.strip() == head_sha:
|
||||
_LOGGER.info(
|
||||
"Using cached PR #%s (commit %s) from %s",
|
||||
pr_number,
|
||||
head_sha[:8],
|
||||
pr_dir,
|
||||
)
|
||||
return frontend_dir
|
||||
_LOGGER.info(
|
||||
"PR #%s has new commits (cached: %s, current: %s), re-downloading",
|
||||
pr_number,
|
||||
cached_sha[:8],
|
||||
head_sha[:8],
|
||||
)
|
||||
|
||||
try:
|
||||
# Find the artifact
|
||||
artifact_url = await _find_pr_artifact(client, pr_number, head_sha)
|
||||
|
||||
# Download artifact
|
||||
_LOGGER.info("Downloading frontend PR #%s artifact", pr_number)
|
||||
artifact_data = await _download_artifact_data(hass, artifact_url, github_token)
|
||||
|
||||
# Extract artifact
|
||||
await hass.async_add_executor_job(
|
||||
_extract_artifact, artifact_data, pr_dir, frontend_dir, head_sha
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Successfully downloaded and extracted PR #%s (commit %s) to %s",
|
||||
pr_number,
|
||||
head_sha[:8],
|
||||
pr_dir,
|
||||
)
|
||||
|
||||
size_mb = await hass.async_add_executor_job(_get_directory_size_mb, pr_dir)
|
||||
_LOGGER.info("PR #%s cache size: %.1f MB", pr_number, size_mb)
|
||||
|
||||
# Warn if total cache size exceeds threshold
|
||||
total_cache_size = await hass.async_add_executor_job(
|
||||
_get_directory_size_mb, cache_dir
|
||||
)
|
||||
if total_cache_size > CACHE_WARNING_SIZE_MB:
|
||||
_LOGGER.warning(
|
||||
"Frontend PR cache directory has grown to %.1f MB (threshold: %d MB). "
|
||||
"Consider manually cleaning up old PR caches in %s",
|
||||
total_cache_size,
|
||||
CACHE_WARNING_SIZE_MB,
|
||||
cache_dir,
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("%s", err)
|
||||
return None
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error downloading PR #%s", pr_number)
|
||||
return None
|
||||
else:
|
||||
return frontend_dir
|
||||
@@ -18,9 +18,6 @@
|
||||
},
|
||||
"ozone": {
|
||||
"default": "mdi:molecule"
|
||||
},
|
||||
"sulphur_dioxide": {
|
||||
"default": "mdi:molecule"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,8 +173,8 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
key="so2",
|
||||
translation_key="sulphur_dioxide",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
|
||||
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.so2.concentration.value,
|
||||
|
||||
@@ -217,9 +217,6 @@
|
||||
"ozone": {
|
||||
"name": "[%key:component::sensor::entity_component::ozone::name%]"
|
||||
},
|
||||
"sulphur_dioxide": {
|
||||
"name": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
|
||||
},
|
||||
"uaqi": {
|
||||
"name": "Universal Air Quality Index"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kostal"],
|
||||
"requirements": ["pykoplenti==1.3.0"]
|
||||
"requirements": ["pykoplenti==1.5.0"]
|
||||
}
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
"""Provides triggers for lights."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateAttributeChangedTriggerBase,
|
||||
EntityNumericalStateAttributeCrossedThresholdTriggerBase,
|
||||
Trigger,
|
||||
make_entity_numerical_state_attribute_changed_trigger,
|
||||
make_entity_numerical_state_attribute_crossed_threshold_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from . import ATTR_BRIGHTNESS
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def _convert_uint8_to_percentage(value: Any) -> float:
|
||||
"""Convert a uint8 value (0-255) to a percentage (0-100)."""
|
||||
return (float(value) / 255.0) * 100.0
|
||||
|
||||
|
||||
class BrightnessChangedTrigger(EntityNumericalStateAttributeChangedTriggerBase):
|
||||
"""Trigger for brightness changed."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
|
||||
_converter = staticmethod(_convert_uint8_to_percentage)
|
||||
|
||||
|
||||
class BrightnessCrossedThresholdTrigger(
|
||||
EntityNumericalStateAttributeCrossedThresholdTriggerBase
|
||||
):
|
||||
"""Trigger for brightness crossed threshold."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
_converter = staticmethod(_convert_uint8_to_percentage)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"brightness_changed": make_entity_numerical_state_attribute_changed_trigger(
|
||||
DOMAIN, ATTR_BRIGHTNESS
|
||||
),
|
||||
"brightness_crossed_threshold": make_entity_numerical_state_attribute_crossed_threshold_trigger(
|
||||
DOMAIN, ATTR_BRIGHTNESS
|
||||
),
|
||||
"brightness_changed": BrightnessChangedTrigger,
|
||||
"brightness_crossed_threshold": BrightnessCrossedThresholdTrigger,
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
max: 100
|
||||
min: 0
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
|
||||
@@ -50,7 +50,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Check connection to the Mealie API."""
|
||||
assert self.host is not None
|
||||
|
||||
if "/hassio/ingress/" in self.host:
|
||||
if "/app/" in self.host:
|
||||
return {"base": "ingress_url"}, None
|
||||
|
||||
client = MealieClient(
|
||||
|
||||
@@ -73,15 +73,6 @@ SHARED_OPTIONS = [
|
||||
CONF_STATE_TOPIC,
|
||||
]
|
||||
|
||||
MQTT_ORIGIN_INFO_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SW_VERSION): cv.string,
|
||||
vol.Optional(CONF_SUPPORT_URL): cv.configuration_url,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
_MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -125,7 +125,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CO = "carbon_monoxide"
|
||||
"""Carbon Monoxide gas concentration.
|
||||
|
||||
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
"""
|
||||
|
||||
CO2 = "carbon_dioxide"
|
||||
@@ -247,7 +247,7 @@ class NumberDeviceClass(StrEnum):
|
||||
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
||||
"""Amount of NO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `ppb` (parts per billion), `μg/m³`
|
||||
"""
|
||||
|
||||
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
||||
@@ -483,6 +483,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
NumberDeviceClass.BATTERY: {PERCENTAGE},
|
||||
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
|
||||
NumberDeviceClass.CO: {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -516,7 +517,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX},
|
||||
NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
|
||||
NumberDeviceClass.MOISTURE: {PERCENTAGE},
|
||||
NumberDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
NumberDeviceClass.NITROGEN_DIOXIDE: {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -178,6 +178,7 @@ class OneDriveBackupAgent(BackupAgent):
|
||||
file,
|
||||
upload_chunk_size=upload_chunk_size,
|
||||
session=async_get_clientsession(self._hass),
|
||||
smart_chunk_size=True,
|
||||
)
|
||||
except HashMismatchError as err:
|
||||
raise BackupAgentError(
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.0"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioqsw"],
|
||||
"requirements": ["aioqsw==0.4.2"]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"codeowners": ["@rabbit-air"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rabbitair",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-rabbitair==0.0.8"],
|
||||
"zeroconf": ["_rabbitair._udp.local."]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/radiotherm",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["radiotherm"],
|
||||
"requirements": ["radiotherm==2.1.0"]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aioraven==0.7.1"],
|
||||
"usb": [
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rapt_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["rapt-ble==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ from homeassistant.util.unit_conversion import (
|
||||
InformationConverter,
|
||||
MassConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
PowerConverter,
|
||||
PressureConverter,
|
||||
ReactiveEnergyConverter,
|
||||
@@ -225,6 +226,7 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
|
||||
|
||||
_SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
TemperatureDeltaConverter,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
]
|
||||
|
||||
@@ -33,6 +33,7 @@ from homeassistant.util.unit_conversion import (
|
||||
InformationConverter,
|
||||
MassConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
PowerConverter,
|
||||
PressureConverter,
|
||||
ReactiveEnergyConverter,
|
||||
@@ -90,6 +91,9 @@ UNIT_SCHEMA = vol.Schema(
|
||||
vol.Optional("energy_distance"): vol.In(EnergyDistanceConverter.VALID_UNITS),
|
||||
vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS),
|
||||
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
|
||||
vol.Optional("nitrogen_dioxide"): vol.In(
|
||||
NitrogenDioxideConcentrationConverter.VALID_UNITS
|
||||
),
|
||||
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),
|
||||
vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS),
|
||||
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ashionky"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/refoss",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["refoss-ha==1.2.5"],
|
||||
"single_config_entry": true
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rehlko",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiokem"],
|
||||
"quality_scale": "silver",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@jimmyd-be"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renson",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["renson-endura-delta==1.7.2"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["RFXtrx"],
|
||||
"requirements": ["pyRFXtrx==0.31.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@milanmeu", "@frenck", "@quebulm"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyrituals"],
|
||||
"requirements": ["pyrituals==0.0.7"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@xeniter"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/romy",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["romy==0.0.10"],
|
||||
"zeroconf": ["_aicu-http._tcp.local."]
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.9.0"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@pavoni"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/roon",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["roonapi"],
|
||||
"requirements": ["roonapi==0.1.6"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rova",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["rova"],
|
||||
"requirements": ["rova==0.4.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@noahhusby"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aioruuvigateway==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["ruuvitag-ble==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@OnFreund", "@elad-bar", "@maorcc"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rympro",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyrympro==0.0.9"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@shaiu", "@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysabnzbd"],
|
||||
"quality_scale": "bronze",
|
||||
|
||||
@@ -44,6 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
entry.runtime_data.client.close()
|
||||
await entry.runtime_data.client.async_close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -51,7 +51,7 @@ async def validate_input(data: dict[str, Any]) -> None:
|
||||
# Try to read data to verify communication
|
||||
await client.async_get_data()
|
||||
finally:
|
||||
client.close()
|
||||
await client.async_close()
|
||||
|
||||
|
||||
class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysaunum"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pysaunum==0.2.0"]
|
||||
"requirements": ["pysaunum==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@dknowles2"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyschlage==2025.9.0"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sense_energy"],
|
||||
"requirements": ["sense-energy==0.13.8"]
|
||||
|
||||
@@ -63,6 +63,7 @@ from homeassistant.util.unit_conversion import (
|
||||
InformationConverter,
|
||||
MassConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
PowerConverter,
|
||||
PressureConverter,
|
||||
ReactiveEnergyConverter,
|
||||
@@ -159,7 +160,7 @@ class SensorDeviceClass(StrEnum):
|
||||
CO = "carbon_monoxide"
|
||||
"""Carbon Monoxide gas concentration.
|
||||
|
||||
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
|
||||
"""
|
||||
|
||||
CO2 = "carbon_dioxide"
|
||||
@@ -283,7 +284,7 @@ class SensorDeviceClass(StrEnum):
|
||||
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
||||
"""Amount of NO2.
|
||||
|
||||
Unit of measurement: `μg/m³`
|
||||
Unit of measurement: `ppb` (parts per billion), `μg/m³`
|
||||
"""
|
||||
|
||||
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
||||
@@ -563,6 +564,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
|
||||
SensorDeviceClass.ENERGY_DISTANCE: EnergyDistanceConverter,
|
||||
SensorDeviceClass.ENERGY_STORAGE: EnergyConverter,
|
||||
SensorDeviceClass.GAS: VolumeConverter,
|
||||
SensorDeviceClass.NITROGEN_DIOXIDE: NitrogenDioxideConcentrationConverter,
|
||||
SensorDeviceClass.POWER: PowerConverter,
|
||||
SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter,
|
||||
SensorDeviceClass.PRECIPITATION: DistanceConverter,
|
||||
@@ -597,6 +599,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.BATTERY: {PERCENTAGE},
|
||||
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
|
||||
SensorDeviceClass.CO: {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -630,7 +633,10 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX},
|
||||
SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
|
||||
SensorDeviceClass.MOISTURE: {PERCENTAGE},
|
||||
SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.NITROGEN_DIOXIDE: {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
|
||||
17
homeassistant/components/siren/condition.py
Normal file
17
homeassistant/components/siren/condition.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Provides conditions for sirens."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the siren conditions."""
|
||||
return CONDITIONS
|
||||
17
homeassistant/components/siren/conditions.yaml
Normal file
17
homeassistant/components/siren/conditions.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: siren
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"condition": "mdi:bullhorn-outline"
|
||||
},
|
||||
"is_on": {
|
||||
"condition": "mdi:bullhorn"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:bullhorn"
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted sirens.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted sirens to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"description": "Tests if one or more sirens are off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::siren::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a siren is off"
|
||||
},
|
||||
"is_on": {
|
||||
"description": "Tests if one or more sirens are on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::siren::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a siren is on"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::siren::title%]",
|
||||
@@ -18,6 +42,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"data": {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Spotify."
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"data": {
|
||||
|
||||
@@ -185,6 +185,9 @@ async def make_device_data(
|
||||
"Smart Lock Lite",
|
||||
"Smart Lock Pro",
|
||||
"Smart Lock Ultra",
|
||||
"Smart Lock Vision",
|
||||
"Smart Lock Vision Pro",
|
||||
"Smart Lock Pro Wifi",
|
||||
]:
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
|
||||
@@ -92,6 +92,18 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
CALIBRATION_DESCRIPTION,
|
||||
DOOR_OPEN_DESCRIPTION,
|
||||
),
|
||||
"Smart Lock Vision": (
|
||||
CALIBRATION_DESCRIPTION,
|
||||
DOOR_OPEN_DESCRIPTION,
|
||||
),
|
||||
"Smart Lock Vision Pro": (
|
||||
CALIBRATION_DESCRIPTION,
|
||||
DOOR_OPEN_DESCRIPTION,
|
||||
),
|
||||
"Smart Lock Pro Wifi": (
|
||||
CALIBRATION_DESCRIPTION,
|
||||
DOOR_OPEN_DESCRIPTION,
|
||||
),
|
||||
"Curtain": (CALIBRATION_DESCRIPTION,),
|
||||
"Curtain3": (CALIBRATION_DESCRIPTION,),
|
||||
"Roller Shade": (CALIBRATION_DESCRIPTION,),
|
||||
|
||||
@@ -46,7 +46,7 @@ class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
|
||||
"""Set attributes from coordinator data."""
|
||||
if coord_data := self.coordinator.data:
|
||||
self._attr_is_locked = coord_data["lockState"] == "locked"
|
||||
if self.__model in LockV2Commands.get_supported_devices():
|
||||
if self.__model != "Smart Lock Lite":
|
||||
self._attr_supported_features = LockEntityFeature.OPEN
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -225,6 +225,9 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
"Smart Lock Lite": (BATTERY_DESCRIPTION,),
|
||||
"Smart Lock Pro": (BATTERY_DESCRIPTION,),
|
||||
"Smart Lock Ultra": (BATTERY_DESCRIPTION,),
|
||||
"Smart Lock Vision": (BATTERY_DESCRIPTION,),
|
||||
"Smart Lock Vision Pro": (BATTERY_DESCRIPTION,),
|
||||
"Smart Lock Pro Wifi": (BATTERY_DESCRIPTION,),
|
||||
"Relay Switch 2PM": (
|
||||
RELAY_SWITCH_2PM_POWER_DESCRIPTION,
|
||||
RELAY_SWITCH_2PM_VOLTAGE_DESCRIPTION,
|
||||
|
||||
@@ -165,7 +165,7 @@ class DeviceListener(SharingDeviceListener):
|
||||
self,
|
||||
device: CustomerDevice,
|
||||
updated_status_properties: list[str] | None = None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None = None,
|
||||
) -> None:
|
||||
"""Update device status with optional DP timestamps."""
|
||||
LOGGER.debug(
|
||||
|
||||
@@ -471,9 +471,11 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -57,7 +57,7 @@ class TuyaEntity(Entity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -218,10 +218,10 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
) or not (event_data := self._dpcode_wrapper.read_device_status(self.device)):
|
||||
return
|
||||
|
||||
|
||||
@@ -31,7 +31,10 @@ class DeviceWrapper[T]:
|
||||
options: list[str]
|
||||
|
||||
def skip_update(
|
||||
self, device: CustomerDevice, updated_status_properties: list[str] | None
|
||||
self,
|
||||
device: CustomerDevice,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> bool:
|
||||
"""Determine if the wrapper should skip an update.
|
||||
|
||||
@@ -62,7 +65,10 @@ class DPCodeWrapper(DeviceWrapper):
|
||||
self.dpcode = dpcode
|
||||
|
||||
def skip_update(
|
||||
self, device: CustomerDevice, updated_status_properties: list[str] | None
|
||||
self,
|
||||
device: CustomerDevice,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> bool:
|
||||
"""Determine if the wrapper should skip an update.
|
||||
|
||||
|
||||
@@ -554,10 +554,12 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -410,10 +410,12 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -1853,9 +1853,11 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -110,10 +110,12 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -1043,10 +1043,12 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -140,10 +140,12 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
updated_status_properties: list[str] | None,
|
||||
dp_timestamps: dict | None = None,
|
||||
dp_timestamps: dict[str, int] | None,
|
||||
) -> None:
|
||||
"""Handle state update, only if this entity's dpcode was actually updated."""
|
||||
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
|
||||
if self._dpcode_wrapper.skip_update(
|
||||
self.device, updated_status_properties, dp_timestamps
|
||||
):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_account": "Wrong account: Please authenticate with {username}."
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Twitch integration needs to re-authenticate your account",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==8.1.1", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==10.0.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Withings device on your network. Be aware that the setup of Withings is more complicated than many other integrations. Press **Submit** to continue setting up Withings."
|
||||
|
||||
@@ -47,6 +47,9 @@ async def async_setup_entry(
|
||||
class WyomingTtsProvider(tts.TextToSpeechEntity):
|
||||
"""Wyoming text-to-speech provider."""
|
||||
|
||||
_attr_default_options = {}
|
||||
_attr_supported_options = [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE, ATTR_SPEAKER]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -78,38 +81,13 @@ class WyomingTtsProvider(tts.TextToSpeechEntity):
|
||||
self._voices[language], key=lambda v: v.name
|
||||
)
|
||||
|
||||
self._supported_languages: list[str] = list(voice_languages)
|
||||
self._attr_supported_languages = list(voice_languages)
|
||||
if self._attr_supported_languages:
|
||||
self._attr_default_language = self._attr_supported_languages[0]
|
||||
|
||||
self._attr_name = self._tts_service.name
|
||||
self._attr_unique_id = f"{config_entry.entry_id}-tts"
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
"""Return default language."""
|
||||
if not self._supported_languages:
|
||||
return None
|
||||
|
||||
return self._supported_languages[0]
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
"""Return list of supported languages."""
|
||||
return self._supported_languages
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
"""Return list of supported options like voice, emotion."""
|
||||
return [
|
||||
tts.ATTR_AUDIO_OUTPUT,
|
||||
tts.ATTR_VOICE,
|
||||
ATTR_SPEAKER,
|
||||
]
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
"""Return a dict include default options."""
|
||||
return {}
|
||||
|
||||
@callback
|
||||
def async_get_supported_voices(self, language: str) -> list[tts.Voice] | None:
|
||||
"""Return a list of supported voices for a language."""
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found an Xbox device on your network. Press **Submit** to continue setting up the Xbox integration.",
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:common::config_flow::initiate_flow::account%]"
|
||||
},
|
||||
"step": {
|
||||
"channels": {
|
||||
"data": { "channels": "YouTube channels" },
|
||||
|
||||
@@ -5375,7 +5375,7 @@
|
||||
"name": "QNAP"
|
||||
},
|
||||
"qnap_qsw": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "QNAP QSW"
|
||||
@@ -5413,7 +5413,7 @@
|
||||
},
|
||||
"rabbitair": {
|
||||
"name": "Rabbit Air",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -5438,7 +5438,7 @@
|
||||
},
|
||||
"radiotherm": {
|
||||
"name": "Radio Thermostat",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -5473,7 +5473,7 @@
|
||||
},
|
||||
"rapt_ble": {
|
||||
"name": "RAPT Bluetooth",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -5571,7 +5571,7 @@
|
||||
},
|
||||
"renson": {
|
||||
"name": "Renson",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -5679,13 +5679,13 @@
|
||||
},
|
||||
"romy": {
|
||||
"name": "ROMY Vacuum Cleaner",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"roomba": {
|
||||
"name": "iRobot Roomba and Braava",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -5720,7 +5720,7 @@
|
||||
},
|
||||
"rova": {
|
||||
"name": "ROVA",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -5763,13 +5763,13 @@
|
||||
"name": "Ruuvi",
|
||||
"integrations": {
|
||||
"ruuvi_gateway": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Ruuvi Gateway"
|
||||
},
|
||||
"ruuvitag_ble": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Ruuvi BLE"
|
||||
@@ -5784,7 +5784,7 @@
|
||||
},
|
||||
"sabnzbd": {
|
||||
"name": "SABnzbd",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
Unpack,
|
||||
@@ -28,7 +29,10 @@ from typing import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_FLOOR_ID,
|
||||
ATTR_LABEL_ID,
|
||||
CONF_ABOVE,
|
||||
CONF_AFTER,
|
||||
CONF_ATTRIBUTE,
|
||||
@@ -1346,13 +1350,18 @@ def async_extract_entities(config: ConfigType | Template) -> set[str]:
|
||||
if entity_ids is not None:
|
||||
referenced.update(entity_ids)
|
||||
|
||||
if target_entities := _get_targets_from_condition_config(
|
||||
config, CONF_ENTITY_ID
|
||||
):
|
||||
referenced.update(target_entities)
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
@callback
|
||||
def async_extract_devices(config: ConfigType | Template) -> set[str]:
|
||||
"""Extract devices from a condition."""
|
||||
referenced = set()
|
||||
referenced: set[str] = set()
|
||||
to_process = deque([config])
|
||||
|
||||
while to_process:
|
||||
@@ -1366,15 +1375,75 @@ def async_extract_devices(config: ConfigType | Template) -> set[str]:
|
||||
to_process.extend(config["conditions"])
|
||||
continue
|
||||
|
||||
if condition != "device":
|
||||
if condition == "device":
|
||||
if (device_id := config.get(CONF_DEVICE_ID)) is not None:
|
||||
referenced.add(device_id)
|
||||
continue
|
||||
|
||||
if (device_id := config.get(CONF_DEVICE_ID)) is not None:
|
||||
referenced.add(device_id)
|
||||
if target_devices := _get_targets_from_condition_config(config, CONF_DEVICE_ID):
|
||||
referenced.update(target_devices)
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
@callback
|
||||
def async_extract_areas(config: ConfigType | Template) -> set[str]:
|
||||
"""Extract areas from a condition."""
|
||||
return _async_extract_targets(config, ATTR_AREA_ID)
|
||||
|
||||
|
||||
@callback
|
||||
def async_extract_floors(config: ConfigType | Template) -> set[str]:
|
||||
"""Extract floors from a condition."""
|
||||
return _async_extract_targets(config, ATTR_FLOOR_ID)
|
||||
|
||||
|
||||
@callback
|
||||
def async_extract_labels(config: ConfigType | Template) -> set[str]:
|
||||
"""Extract labels from a condition."""
|
||||
return _async_extract_targets(config, ATTR_LABEL_ID)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_extract_targets(
|
||||
config: ConfigType | Template,
|
||||
target_type: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
|
||||
) -> set[str]:
|
||||
"""Extract targets from a condition."""
|
||||
referenced: set[str] = set()
|
||||
to_process = deque([config])
|
||||
|
||||
while to_process:
|
||||
config = to_process.popleft()
|
||||
if isinstance(config, Template):
|
||||
continue
|
||||
|
||||
condition = config[CONF_CONDITION]
|
||||
|
||||
if condition in ("and", "not", "or"):
|
||||
to_process.extend(config["conditions"])
|
||||
continue
|
||||
|
||||
if targets := _get_targets_from_condition_config(config, target_type):
|
||||
referenced.update(targets)
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
@callback
|
||||
def _get_targets_from_condition_config(
|
||||
config: ConfigType,
|
||||
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
|
||||
) -> list[str]:
|
||||
"""Extract targets from a condition target config."""
|
||||
if not (target_conf := config.get(CONF_TARGET)):
|
||||
return []
|
||||
if not (targets := target_conf.get(target)):
|
||||
return []
|
||||
|
||||
return [targets] if isinstance(targets, str) else targets
|
||||
|
||||
|
||||
def _load_conditions_file(integration: Integration) -> dict[str, Any]:
|
||||
"""Load conditions file for an integration."""
|
||||
try:
|
||||
|
||||
@@ -594,6 +594,8 @@ class EntityNumericalStateAttributeChangedTriggerBase(EntityTriggerBase):
|
||||
_above: None | float | str
|
||||
_below: None | float | str
|
||||
|
||||
_converter: Callable[[Any], float] = float
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
@@ -616,7 +618,7 @@ class EntityNumericalStateAttributeChangedTriggerBase(EntityTriggerBase):
|
||||
return False
|
||||
|
||||
try:
|
||||
current_value = float(_attribute_value)
|
||||
current_value = self._converter(_attribute_value)
|
||||
except (TypeError, ValueError):
|
||||
# Attribute is not a valid number, don't trigger
|
||||
return False
|
||||
@@ -706,6 +708,8 @@ class EntityNumericalStateAttributeCrossedThresholdTriggerBase(EntityTriggerBase
|
||||
_upper_limit: float | str | None = None
|
||||
_threshold_type: ThresholdType
|
||||
|
||||
_converter: Callable[[Any], float] = float
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
@@ -741,7 +745,7 @@ class EntityNumericalStateAttributeCrossedThresholdTriggerBase(EntityTriggerBase
|
||||
return False
|
||||
|
||||
try:
|
||||
current_value = float(_attribute_value)
|
||||
current_value = self._converter(_attribute_value)
|
||||
except (TypeError, ValueError):
|
||||
# Attribute is not a valid number, don't trigger
|
||||
return False
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
aiodhcpwatcher==1.2.1
|
||||
aiodiscover==2.7.1
|
||||
aiodns==4.0.0
|
||||
aiogithubapi==24.6.0
|
||||
aiohasupervisor==0.3.3
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
@@ -37,7 +36,7 @@ fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.8.0
|
||||
hass-nabucasa==1.10.0
|
||||
hass-nabucasa==1.11.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260107.2
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"timeout_connect": "Timeout establishing connection",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"account": "Add account"
|
||||
},
|
||||
"title": {
|
||||
"oauth2_pick_implementation": "Pick authentication method",
|
||||
"reauth": "Authentication expired for {name}",
|
||||
|
||||
@@ -103,6 +103,7 @@ _AMBIENT_IDEAL_GAS_MOLAR_VOLUME = ( # m3⋅mol⁻¹
|
||||
)
|
||||
# Molar masses in g⋅mol⁻¹
|
||||
_CARBON_MONOXIDE_MOLAR_MASS = 28.01
|
||||
_NITROGEN_DIOXIDE_MOLAR_MASS = 46.0055
|
||||
_SULPHUR_DIOXIDE_MOLAR_MASS = 64.066
|
||||
|
||||
|
||||
@@ -194,6 +195,7 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "carbon_monoxide"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_PARTS_PER_MILLION: 1e6,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: (
|
||||
_CARBON_MONOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e3
|
||||
@@ -203,12 +205,29 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
class NitrogenDioxideConcentrationConverter(BaseUnitConverter):
|
||||
"""Convert nitrogen dioxide ratio to mass per volume."""
|
||||
|
||||
UNIT_CLASS = "nitrogen_dioxide"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
_NITROGEN_DIOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
class SulphurDioxideConcentrationConverter(BaseUnitConverter):
|
||||
"""Convert sulphur dioxide ratio to mass per volume."""
|
||||
|
||||
|
||||
@@ -168,7 +168,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = {
|
||||
"service_calls": "list[ServiceCall]",
|
||||
"snapshot": "SnapshotAssertion",
|
||||
"socket_enabled": "None",
|
||||
"stub_blueprint_populate": "None",
|
||||
"tmp_path": "Path",
|
||||
"tmpdir": "py.path.local",
|
||||
"tts_mutagen_mock": "MagicMock",
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.10.0",
|
||||
"hass-nabucasa==1.11.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -24,7 +24,7 @@ cronsim==2.7
|
||||
cryptography==46.0.2
|
||||
fnv-hash-fast==1.6.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.10.0
|
||||
hass-nabucasa==1.11.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.1.6
|
||||
|
||||
13
requirements_all.txt
generated
13
requirements_all.txt
generated
@@ -264,7 +264,6 @@ aioflo==2021.11.0
|
||||
# homeassistant.components.yi
|
||||
aioftp==0.21.3
|
||||
|
||||
# homeassistant.components.frontend
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==24.6.0
|
||||
|
||||
@@ -1172,7 +1171,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.10.0
|
||||
hass-nabucasa==1.11.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -1647,7 +1646,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.1.0
|
||||
onedrive-personal-sdk==0.1.1
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -2046,7 +2045,7 @@ pyfibaro==0.8.3
|
||||
pyfido==2.1.2
|
||||
|
||||
# homeassistant.components.firefly_iii
|
||||
pyfirefly==0.1.11
|
||||
pyfirefly==0.1.12
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.46
|
||||
@@ -2148,7 +2147,7 @@ pykmtronic==0.3.0
|
||||
pykodi==0.2.7
|
||||
|
||||
# homeassistant.components.kostal_plenticore
|
||||
pykoplenti==1.3.0
|
||||
pykoplenti==1.5.0
|
||||
|
||||
# homeassistant.components.kraken
|
||||
pykrakenapi==0.1.8
|
||||
@@ -2378,7 +2377,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.saunum
|
||||
pysaunum==0.2.0
|
||||
pysaunum==0.3.0
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2025.9.0
|
||||
@@ -3081,7 +3080,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==8.1.1
|
||||
uiprotect==10.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
13
requirements_test_all.txt
generated
13
requirements_test_all.txt
generated
@@ -252,7 +252,6 @@ aiofiles==24.1.0
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
||||
# homeassistant.components.frontend
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==24.6.0
|
||||
|
||||
@@ -1042,7 +1041,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.10.0
|
||||
hass-nabucasa==1.11.0
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
@@ -1430,7 +1429,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.1.0
|
||||
onedrive-personal-sdk==0.1.1
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1738,7 +1737,7 @@ pyfibaro==0.8.3
|
||||
pyfido==2.1.2
|
||||
|
||||
# homeassistant.components.firefly_iii
|
||||
pyfirefly==0.1.11
|
||||
pyfirefly==0.1.12
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.46
|
||||
@@ -1822,7 +1821,7 @@ pykmtronic==0.3.0
|
||||
pykodi==0.2.7
|
||||
|
||||
# homeassistant.components.kostal_plenticore
|
||||
pykoplenti==1.3.0
|
||||
pykoplenti==1.5.0
|
||||
|
||||
# homeassistant.components.kraken
|
||||
pykrakenapi==0.1.8
|
||||
@@ -2013,7 +2012,7 @@ pyrympro==0.0.9
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.saunum
|
||||
pysaunum==0.2.0
|
||||
pysaunum==0.3.0
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2025.9.0
|
||||
@@ -2578,7 +2577,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==8.1.1
|
||||
uiprotect==10.0.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
@@ -214,6 +214,10 @@ def gen_data_entry_schema(
|
||||
vol.Required("user"): translation_value_validator,
|
||||
str: translation_value_validator,
|
||||
}
|
||||
else:
|
||||
schema[vol.Optional("initiate_flow")] = {
|
||||
vol.Required("user"): translation_value_validator,
|
||||
}
|
||||
if flow_title == REQUIRED:
|
||||
schema[vol.Required("title")] = translation_value_validator
|
||||
elif flow_title == REMOVED:
|
||||
|
||||
@@ -100,6 +100,13 @@ async def target_entities(
|
||||
suggested_object_id=f"device_{domain}",
|
||||
device_id=device.id,
|
||||
)
|
||||
entity_reg.async_get_or_create(
|
||||
domain=domain,
|
||||
platform="test",
|
||||
unique_id=f"{domain}_device2",
|
||||
suggested_object_id=f"device2_{domain}",
|
||||
device_id=device.id,
|
||||
)
|
||||
entity_reg.async_get_or_create(
|
||||
domain=domain,
|
||||
platform="test",
|
||||
@@ -130,9 +137,11 @@ async def target_entities(
|
||||
return {
|
||||
"included": [
|
||||
f"{domain}.standalone_{domain}",
|
||||
f"{domain}.standalone2_{domain}",
|
||||
f"{domain}.label_{domain}",
|
||||
f"{domain}.area_{domain}",
|
||||
f"{domain}.device_{domain}",
|
||||
f"{domain}.device2_{domain}",
|
||||
],
|
||||
"excluded": [
|
||||
f"{domain}.standalone_{domain}_excluded",
|
||||
@@ -150,17 +159,22 @@ def parametrize_target_entities(domain: str) -> list[tuple[dict, str, int]]:
|
||||
"""
|
||||
return [
|
||||
(
|
||||
{CONF_ENTITY_ID: f"{domain}.standalone_{domain}"},
|
||||
{
|
||||
CONF_ENTITY_ID: [
|
||||
f"{domain}.standalone_{domain}",
|
||||
f"{domain}.standalone2_{domain}",
|
||||
]
|
||||
},
|
||||
f"{domain}.standalone_{domain}",
|
||||
1,
|
||||
2,
|
||||
),
|
||||
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 2),
|
||||
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 2),
|
||||
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 2),
|
||||
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 2),
|
||||
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 2),
|
||||
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 2),
|
||||
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 1),
|
||||
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 3),
|
||||
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 3),
|
||||
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 3),
|
||||
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 3),
|
||||
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 3),
|
||||
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 3),
|
||||
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 2),
|
||||
]
|
||||
|
||||
|
||||
@@ -184,18 +198,19 @@ class ConditionStateDescription(TypedDict):
|
||||
|
||||
included: _StateDescription # State for entities meant to be targeted
|
||||
excluded: _StateDescription # State for entities not meant to be targeted
|
||||
state_valid: bool # False if the state of the included entities is missing (None), unavailable or unknown
|
||||
|
||||
condition_true: bool # If the condition is expected to evaluate to true
|
||||
condition_true_first_entity: bool # If the condition is expected to evaluate to true for the first targeted entity
|
||||
|
||||
|
||||
def parametrize_condition_states(
|
||||
def _parametrize_condition_states(
|
||||
*,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
additional_attributes: dict | None,
|
||||
condition_true_if_invalid: bool,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize states and expected condition evaluations.
|
||||
|
||||
@@ -212,7 +227,7 @@ def parametrize_condition_states(
|
||||
def state_with_attributes(
|
||||
state: str | None | tuple[str | None, dict],
|
||||
condition_true: bool,
|
||||
state_valid: bool,
|
||||
condition_true_first_entity: bool,
|
||||
) -> ConditionStateDescription:
|
||||
"""Return ConditionStateDescription dict."""
|
||||
if isinstance(state, str) or state is None:
|
||||
@@ -226,7 +241,7 @@ def parametrize_condition_states(
|
||||
"attributes": {},
|
||||
},
|
||||
"condition_true": condition_true,
|
||||
"state_valid": state_valid,
|
||||
"condition_true_first_entity": condition_true_first_entity,
|
||||
}
|
||||
return {
|
||||
"included": {
|
||||
@@ -238,7 +253,7 @@ def parametrize_condition_states(
|
||||
"attributes": state[1],
|
||||
},
|
||||
"condition_true": condition_true,
|
||||
"state_valid": state_valid,
|
||||
"condition_true_first_entity": condition_true_first_entity,
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -247,11 +262,19 @@ def parametrize_condition_states(
|
||||
condition_options,
|
||||
list(
|
||||
itertools.chain(
|
||||
(state_with_attributes(None, False, False),),
|
||||
(state_with_attributes(STATE_UNAVAILABLE, False, False),),
|
||||
(state_with_attributes(STATE_UNKNOWN, False, False),),
|
||||
(state_with_attributes(None, condition_true_if_invalid, True),),
|
||||
(
|
||||
state_with_attributes(other_state, False, True)
|
||||
state_with_attributes(
|
||||
STATE_UNAVAILABLE, condition_true_if_invalid, True
|
||||
),
|
||||
),
|
||||
(
|
||||
state_with_attributes(
|
||||
STATE_UNKNOWN, condition_true_if_invalid, True
|
||||
),
|
||||
),
|
||||
(
|
||||
state_with_attributes(other_state, False, False)
|
||||
for other_state in other_states
|
||||
),
|
||||
),
|
||||
@@ -263,8 +286,8 @@ def parametrize_condition_states(
|
||||
condition,
|
||||
condition_options,
|
||||
[
|
||||
state_with_attributes(other_states[0], False, True),
|
||||
state_with_attributes(target_state, True, True),
|
||||
state_with_attributes(other_states[0], False, False),
|
||||
state_with_attributes(target_state, True, False),
|
||||
],
|
||||
)
|
||||
for target_state in target_states
|
||||
@@ -272,6 +295,60 @@ def parametrize_condition_states(
|
||||
]
|
||||
|
||||
|
||||
def parametrize_condition_states_any(
|
||||
*,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize states and expected condition evaluations.
|
||||
|
||||
The target_states and other_states iterables are either iterables of
|
||||
states or iterables of (state, attributes) tuples.
|
||||
|
||||
Returns a list of tuples with (condition, condition options, list of states),
|
||||
where states is a list of ConditionStateDescription dicts.
|
||||
"""
|
||||
|
||||
return _parametrize_condition_states(
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
target_states=target_states,
|
||||
other_states=other_states,
|
||||
additional_attributes=additional_attributes,
|
||||
condition_true_if_invalid=False,
|
||||
)
|
||||
|
||||
|
||||
def parametrize_condition_states_all(
|
||||
*,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any] | None = None,
|
||||
target_states: list[str | None | tuple[str | None, dict]],
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize states and expected condition evaluations.
|
||||
|
||||
The target_states and other_states iterables are either iterables of
|
||||
states or iterables of (state, attributes) tuples.
|
||||
|
||||
Returns a list of tuples with (condition, condition options, list of states),
|
||||
where states is a list of ConditionStateDescription dicts.
|
||||
"""
|
||||
|
||||
return _parametrize_condition_states(
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
target_states=target_states,
|
||||
other_states=other_states,
|
||||
additional_attributes=additional_attributes,
|
||||
condition_true_if_invalid=True,
|
||||
)
|
||||
|
||||
|
||||
def parametrize_trigger_states(
|
||||
*,
|
||||
trigger: str,
|
||||
|
||||
@@ -16,18 +16,14 @@ from tests.components import (
|
||||
assert_condition_gated_by_labs_flag,
|
||||
create_target_condition,
|
||||
other_states,
|
||||
parametrize_condition_states,
|
||||
parametrize_condition_states_all,
|
||||
parametrize_condition_states_any,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple alarm_control_panel entities associated with different targets."""
|
||||
@@ -61,7 +57,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_armed",
|
||||
target_states=[
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
@@ -78,7 +74,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_armed_away",
|
||||
target_states=[AlarmControlPanelState.ARMED_AWAY],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
|
||||
@@ -86,7 +82,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_armed_home",
|
||||
target_states=[AlarmControlPanelState.ARMED_HOME],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
|
||||
@@ -94,7 +90,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_armed_night",
|
||||
target_states=[AlarmControlPanelState.ARMED_NIGHT],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
|
||||
@@ -102,7 +98,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_armed_vacation",
|
||||
target_states=[AlarmControlPanelState.ARMED_VACATION],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
|
||||
@@ -110,12 +106,12 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_disarmed",
|
||||
target_states=[AlarmControlPanelState.DISARMED],
|
||||
other_states=other_states(AlarmControlPanelState.DISARMED),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="alarm_control_panel.is_triggered",
|
||||
target_states=[AlarmControlPanelState.TRIGGERED],
|
||||
other_states=other_states(AlarmControlPanelState.TRIGGERED),
|
||||
@@ -168,7 +164,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_armed",
|
||||
target_states=[
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
@@ -185,7 +181,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_armed_away",
|
||||
target_states=[AlarmControlPanelState.ARMED_AWAY],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
|
||||
@@ -193,7 +189,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_armed_home",
|
||||
target_states=[AlarmControlPanelState.ARMED_HOME],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
|
||||
@@ -201,7 +197,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_armed_night",
|
||||
target_states=[AlarmControlPanelState.ARMED_NIGHT],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
|
||||
@@ -209,7 +205,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_armed_vacation",
|
||||
target_states=[AlarmControlPanelState.ARMED_VACATION],
|
||||
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
|
||||
@@ -217,12 +213,12 @@ async def test_alarm_control_panel_state_condition_behavior_any(
|
||||
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_disarmed",
|
||||
target_states=[AlarmControlPanelState.DISARMED],
|
||||
other_states=other_states(AlarmControlPanelState.DISARMED),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="alarm_control_panel.is_triggered",
|
||||
target_states=[AlarmControlPanelState.TRIGGERED],
|
||||
other_states=other_states(AlarmControlPanelState.TRIGGERED),
|
||||
@@ -259,17 +255,10 @@ async def test_alarm_control_panel_state_condition_behavior_all(
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"])
|
||||
or (state["condition_true"] and entities_in_target == 1)
|
||||
)
|
||||
assert condition(hass) == state["condition_true_first_entity"]
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"]) or state["condition_true"]
|
||||
)
|
||||
assert condition(hass) == state["condition_true"]
|
||||
|
||||
@@ -25,11 +25,6 @@ from tests.common import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("set_state", "features_reg", "features_state", "expected_action_types"),
|
||||
[
|
||||
|
||||
@@ -18,11 +18,6 @@ from homeassistant.setup import async_setup_component
|
||||
from tests.common import MockConfigEntry, async_get_device_automations
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("set_state", "features_reg", "features_state", "expected_condition_types"),
|
||||
[
|
||||
|
||||
@@ -26,11 +26,6 @@ from tests.common import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("set_state", "features_reg", "features_state", "expected_trigger_types"),
|
||||
[
|
||||
|
||||
@@ -22,11 +22,6 @@ from tests.components import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple alarm control panel entities associated with different targets."""
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""The tests for Arcam FMJ Receiver control device triggers."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.arcam_fmj.const import DOMAIN
|
||||
from homeassistant.components.device_automation import DeviceAutomationType
|
||||
@@ -12,11 +10,6 @@ from homeassistant.setup import async_setup_component
|
||||
from tests.common import MockConfigEntry, async_get_device_automations
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
async def test_get_triggers(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
@@ -12,18 +12,14 @@ from tests.components import (
|
||||
assert_condition_gated_by_labs_flag,
|
||||
create_target_condition,
|
||||
other_states,
|
||||
parametrize_condition_states,
|
||||
parametrize_condition_states_all,
|
||||
parametrize_condition_states_any,
|
||||
parametrize_target_entities,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple assist satellite entities associated with different targets."""
|
||||
@@ -54,22 +50,22 @@ async def test_assist_satellite_conditions_gated_by_labs_flag(
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="assist_satellite.is_idle",
|
||||
target_states=[AssistSatelliteState.IDLE],
|
||||
other_states=other_states(AssistSatelliteState.IDLE),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="assist_satellite.is_listening",
|
||||
target_states=[AssistSatelliteState.LISTENING],
|
||||
other_states=other_states(AssistSatelliteState.LISTENING),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="assist_satellite.is_processing",
|
||||
target_states=[AssistSatelliteState.PROCESSING],
|
||||
other_states=other_states(AssistSatelliteState.PROCESSING),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_any(
|
||||
condition="assist_satellite.is_responding",
|
||||
target_states=[AssistSatelliteState.RESPONDING],
|
||||
other_states=other_states(AssistSatelliteState.RESPONDING),
|
||||
@@ -122,22 +118,22 @@ async def test_assist_satellite_state_condition_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="assist_satellite.is_idle",
|
||||
target_states=[AssistSatelliteState.IDLE],
|
||||
other_states=other_states(AssistSatelliteState.IDLE),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="assist_satellite.is_listening",
|
||||
target_states=[AssistSatelliteState.LISTENING],
|
||||
other_states=other_states(AssistSatelliteState.LISTENING),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="assist_satellite.is_processing",
|
||||
target_states=[AssistSatelliteState.PROCESSING],
|
||||
other_states=other_states(AssistSatelliteState.PROCESSING),
|
||||
),
|
||||
*parametrize_condition_states(
|
||||
*parametrize_condition_states_all(
|
||||
condition="assist_satellite.is_responding",
|
||||
target_states=[AssistSatelliteState.RESPONDING],
|
||||
other_states=other_states(AssistSatelliteState.RESPONDING),
|
||||
@@ -174,17 +170,10 @@ async def test_assist_satellite_state_condition_behavior_all(
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"])
|
||||
or (state["condition_true"] and entities_in_target == 1)
|
||||
)
|
||||
assert condition(hass) == state["condition_true_first_entity"]
|
||||
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The condition passes if all entities are either in a target state or invalid
|
||||
assert condition(hass) == (
|
||||
(not state["state_valid"]) or state["condition_true"]
|
||||
)
|
||||
assert condition(hass) == state["condition_true"]
|
||||
|
||||
@@ -19,11 +19,6 @@ from tests.components import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple assist satellite entities associated with different targets."""
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Conftest for automation tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
@@ -2232,7 +2232,7 @@ async def test_extraction_functions(
|
||||
assert automation.blueprint_in_automation(hass, "automation.test3") is None
|
||||
|
||||
|
||||
async def test_extraction_functions_with_targets(
|
||||
async def test_extraction_functions_with_trigger_targets(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
@@ -2428,6 +2428,211 @@ async def test_extraction_functions_with_targets(
|
||||
}
|
||||
|
||||
|
||||
async def test_extraction_functions_with_condition_targets(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test extraction functions with targets in conditions."""
|
||||
config_entry = MockConfigEntry(domain="fake_integration", data={})
|
||||
config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
condition_device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "00:00:00:00:00:02")},
|
||||
)
|
||||
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await async_setup_component(hass, "light", {"light": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Enable the new_triggers_conditions feature flag to allow new-style conditions
|
||||
assert await async_setup_component(hass, "labs", {})
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "labs/update",
|
||||
"domain": "automation",
|
||||
"preview_feature": "new_triggers_conditions",
|
||||
"enabled": True,
|
||||
}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: [
|
||||
{
|
||||
"alias": "test1",
|
||||
"triggers": [
|
||||
{"trigger": "state", "entity_id": "sensor.trigger_state"},
|
||||
],
|
||||
"conditions": [
|
||||
# Single entity_id in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {"entity_id": "light.condition_entity"},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Multiple entity_ids in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"entity_id": [
|
||||
"light.condition_entity_list1",
|
||||
"light.condition_entity_list2",
|
||||
]
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Single device_id in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {"device_id": condition_device.id},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Multiple device_ids in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"device_id": [
|
||||
"target-device-1",
|
||||
"target-device-2",
|
||||
]
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Single area_id in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {"area_id": "area-condition-single"},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Multiple area_ids in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"area_id": ["area-condition-1", "area-condition-2"]
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Single floor_id in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {"floor_id": "floor-condition-single"},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Multiple floor_ids in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"floor_id": ["floor-condition-1", "floor-condition-2"]
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Single label_id in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {"label_id": "label-condition-single"},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Multiple label_ids in target
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"label_id": ["label-condition-1", "label-condition-2"]
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
# Combined targets
|
||||
{
|
||||
"condition": "light.is_on",
|
||||
"target": {
|
||||
"entity_id": "light.combined_entity",
|
||||
"device_id": "combined-device",
|
||||
"area_id": "combined-area",
|
||||
"floor_id": "combined-floor",
|
||||
"label_id": "combined-label",
|
||||
},
|
||||
"options": {"behavior": "any"},
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"action": "test.script",
|
||||
"data": {"entity_id": "light.action_entity"},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# Test entity extraction from condition targets
|
||||
assert set(automation.entities_in_automation(hass, "automation.test1")) == {
|
||||
"sensor.trigger_state",
|
||||
"light.condition_entity",
|
||||
"light.condition_entity_list1",
|
||||
"light.condition_entity_list2",
|
||||
"light.combined_entity",
|
||||
"light.action_entity",
|
||||
}
|
||||
|
||||
# Test device extraction from condition targets
|
||||
assert set(automation.devices_in_automation(hass, "automation.test1")) == {
|
||||
condition_device.id,
|
||||
"target-device-1",
|
||||
"target-device-2",
|
||||
"combined-device",
|
||||
}
|
||||
|
||||
# Test area extraction from condition targets
|
||||
assert set(automation.areas_in_automation(hass, "automation.test1")) == {
|
||||
"area-condition-single",
|
||||
"area-condition-1",
|
||||
"area-condition-2",
|
||||
"combined-area",
|
||||
}
|
||||
|
||||
# Test floor extraction from condition targets
|
||||
assert set(automation.floors_in_automation(hass, "automation.test1")) == {
|
||||
"floor-condition-single",
|
||||
"floor-condition-1",
|
||||
"floor-condition-2",
|
||||
"combined-floor",
|
||||
}
|
||||
|
||||
# Test label extraction from condition targets
|
||||
assert set(automation.labels_in_automation(hass, "automation.test1")) == {
|
||||
"label-condition-single",
|
||||
"label-condition-1",
|
||||
"label-condition-2",
|
||||
"combined-label",
|
||||
}
|
||||
|
||||
# Test automations_with_* functions
|
||||
assert set(automation.automations_with_entity(hass, "light.condition_entity")) == {
|
||||
"automation.test1"
|
||||
}
|
||||
assert set(automation.automations_with_device(hass, condition_device.id)) == {
|
||||
"automation.test1"
|
||||
}
|
||||
assert set(automation.automations_with_area(hass, "area-condition-single")) == {
|
||||
"automation.test1"
|
||||
}
|
||||
assert set(automation.automations_with_floor(hass, "floor-condition-single")) == {
|
||||
"automation.test1"
|
||||
}
|
||||
assert set(automation.automations_with_label(hass, "label-condition-single")) == {
|
||||
"automation.test1"
|
||||
}
|
||||
|
||||
|
||||
async def test_logbook_humanify_automation_triggered_event(hass: HomeAssistant) -> None:
|
||||
"""Test humanifying Automation Trigger event."""
|
||||
hass.config.components.add("recorder")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user