Compare commits

..

52 Commits

Author SHA1 Message Date
Thomas55555
a447c1b42e Use SO2 device_class in Google Air Quality (#161349) 2026-01-21 06:55:02 +01:00
Raphael Hehl
50211f75ed Bump uiprotect to 10.0.0 (#161350) 2026-01-21 00:59:46 +01:00
Thomas55555
27117c9d17 Add ppb as a valid UOM for sensor/number NO2 device class (#159426)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-01-20 22:35:11 +00:00
Scott K Logan
7c4cdd57b6 Set integration_type for rainforest_raven to 'hub' (#161343) 2026-01-20 21:46:47 +01:00
Josef Zweck
6af5698645 Bump onedrive-personal-sdk to 0.1.1 (#161337) 2026-01-20 20:14:58 +00:00
Erik Montnemery
75db2cde40 Improve light brightness triggers (#161233) 2026-01-20 20:14:15 +00:00
stegm
329dd05434 Bump pykoplenti to 1.5.0 (#161305) 2026-01-20 21:12:49 +01:00
Joost Lekkerkerker
53c53d03e0 Add integration_type hub to rituals_perfume_genie (#161312) 2026-01-20 21:10:11 +01:00
Joost Lekkerkerker
360b394d03 Add integration_type hub to rfxtrx (#161311) 2026-01-20 21:09:09 +01:00
Joost Lekkerkerker
a663d55632 Add integration_type device to renson (#161310) 2026-01-20 21:07:50 +01:00
Joost Lekkerkerker
3fd266a513 Add integration_type hub to rehlko (#161309) 2026-01-20 21:07:21 +01:00
Joost Lekkerkerker
442c1d6242 Add integration_type hub to refoss (#161308) 2026-01-20 21:06:51 +01:00
Joost Lekkerkerker
0e2aae02f6 Add integration_type device to rapt_ble (#161307) 2026-01-20 21:04:45 +01:00
Joost Lekkerkerker
3227a6e49f Add integration_type device to rainforest_raven (#161306) 2026-01-20 21:04:10 +01:00
Joost Lekkerkerker
9d0cfb628b Add integration_type device to radiotherm (#161302) 2026-01-20 21:00:50 +01:00
Joost Lekkerkerker
4578fe0260 Add integration_type device to rabbitair (#161300) 2026-01-20 20:55:41 +01:00
Joost Lekkerkerker
0d92708108 Add integration_type device to qnap_qsw (#161299) 2026-01-20 20:54:58 +01:00
Joost Lekkerkerker
cceb50071b Add integration_type device to ruuvitag_ble (#161319) 2026-01-20 20:53:14 +01:00
Joost Lekkerkerker
62f296c9dd Add integration_type device to ruuvi_gateway (#161318) 2026-01-20 20:52:32 +01:00
Joost Lekkerkerker
ea1f280494 Add integration_type hub to russound_rio (#161317) 2026-01-20 20:48:32 +01:00
Joost Lekkerkerker
67108a2fc8 Add integration_type service to rova (#161316) 2026-01-20 20:47:36 +01:00
Joost Lekkerkerker
1ccbd5124e Add integration_type hub to roon (#161315) 2026-01-20 20:47:07 +01:00
Joost Lekkerkerker
818af90a7b Add integration_type device to roomba (#161314) 2026-01-20 20:45:35 +01:00
Joost Lekkerkerker
23bc78fa25 Add integration_type device to romy (#161313) 2026-01-20 20:44:38 +01:00
Josef Zweck
0b1cc7638f Enable smart chunk size in onedrive (#161170) 2026-01-20 20:41:48 +01:00
Joost Lekkerkerker
c291a2fbc1 Add translation for add entry to NYT Games (#161327) 2026-01-20 20:35:50 +01:00
Joost Lekkerkerker
7379a4ff4b Add integration_type hub to sense (#161325) 2026-01-20 20:33:18 +01:00
Joost Lekkerkerker
ddcf5cb749 Add integration_type hub to schlage (#161323) 2026-01-20 20:29:23 +01:00
Joost Lekkerkerker
4b10a542b0 Add integration_type hub to rympro (#161320) 2026-01-20 20:27:35 +01:00
Joost Lekkerkerker
beea9fa74b Add integration_type service to sabnzbd (#161321) 2026-01-20 20:20:54 +01:00
Joost Lekkerkerker
ce8fd16456 Add translation for add entry to Twitch (#161332) 2026-01-20 20:11:20 +01:00
Joost Lekkerkerker
2172d15489 Add translation for add entry to SmartThings (#161331) 2026-01-20 20:10:47 +01:00
Joost Lekkerkerker
0cfa0ed670 Add translation for add entry to Withings (#161333) 2026-01-20 20:07:54 +01:00
Joost Lekkerkerker
f6839913d8 Add translation for add entry to Youtube (#161334) 2026-01-20 20:07:33 +01:00
Manu
8fa01497ee Add translation for add entry to Xbox integration (#161296) 2026-01-20 19:14:49 +01:00
Abílio Costa
e077c65a77 Support target conditions in automation relation extraction (#161016) 2026-01-20 17:34:21 +00:00
Manu
7c49656fa8 Add translation for add entry to PlayStation Network integration (#161298) 2026-01-20 18:29:23 +01:00
Erik Montnemery
1730479c8d Remove reference of removed stub_blueprint_populate fixture from siren tests (#161294) 2026-01-20 16:16:22 +01:00
Thomas55555
bc28c8fd3c Add ppb as a valid uom for sensor/number CO device class (#159554) 2026-01-20 16:07:24 +01:00
Erik Montnemery
c3616fd5df Add siren conditions (#161021) 2026-01-20 16:05:21 +01:00
epenet
6b97f2ac06 Use shorthand attributes in wyoming TTS (#161286) 2026-01-20 15:59:49 +01:00
Erik Montnemery
deefcbcbe4 Remove stub_blueprint_populate test fixture (#161288) 2026-01-20 15:46:06 +01:00
Samuel Xiao
e84aeb9f99 Switchbot Cloud: Add new supported Lock (#161276) 2026-01-20 15:27:27 +01:00
epenet
ade3d8a657 Pass timestamps to Tuya wrapper skip_update (#161271) 2026-01-20 15:24:49 +01:00
Krisjanis Lejejs
a65d9032ff Bump hass-nabucasa from 1.10.0 to 1.11.0 (#161283) 2026-01-20 14:50:00 +01:00
Martin Hjelmare
b950a4eaf4 Fix nobo_hub options flow unload mocking (#161287) 2026-01-20 14:49:46 +01:00
Joost Lekkerkerker
3fe91751f5 Make add entry translatable (#159901) 2026-01-20 14:25:33 +01:00
Erwin Douna
6ee58b96ca Bump pyfirefly to 0.1.12 (#161278) 2026-01-20 13:51:09 +01:00
Erik Montnemery
d1404e7905 Simplify logic in condition tests (#161239) 2026-01-20 10:39:47 +01:00
Paulus Schoutsen
7c34191813 Use new app panel instead of ingress page (#161264)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-01-20 10:54:56 +02:00
PolarBearEs
7540d04779 Remove duplicated MQTT_ORIGIN_INFO_SCHEMA in schemas.py (#161263) 2026-01-20 08:41:40 +01:00
mettolen
d828130670 Bump pysaunum to 0.3.0 (#161255) 2026-01-20 08:31:48 +01:00
199 changed files with 1194 additions and 1340 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -26,7 +26,6 @@ EXCLUDE_FROM_BACKUP = [
"tmp_backups/*.tar",
"OZW_Log.txt",
"tts/*",
"frontend_development_pr/*",
]
EXCLUDE_DATABASE_FROM_BACKUP = [

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.11"]
"requirements": ["pyfirefly==0.1.12"]
}

View File

@@ -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)

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -18,9 +18,6 @@
},
"ozone": {
"default": "mdi:molecule"
},
"sulphur_dioxide": {
"default": "mdi:molecule"
}
}
}

View File

@@ -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,

View File

@@ -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"
},

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["kostal"],
"requirements": ["pykoplenti==1.3.0"]
"requirements": ["pykoplenti==1.5.0"]
}

View File

@@ -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),
}

View File

@@ -22,7 +22,10 @@
number:
selector:
number:
max: 100
min: 0
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:

View File

@@ -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(

View File

@@ -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(
{

View File

@@ -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},

View File

@@ -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": {

View File

@@ -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(

View File

@@ -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"]
}

View File

@@ -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": {

View File

@@ -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"]

View File

@@ -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."]

View File

@@ -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"]

View File

@@ -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": [

View File

@@ -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"]
}

View File

@@ -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,
]

View File

@@ -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),

View File

@@ -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

View File

@@ -10,6 +10,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/rehlko",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aiokem"],
"quality_scale": "silver",

View File

@@ -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"]
}

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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."]

View File

@@ -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"],

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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",

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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):

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["pysaunum"],
"quality_scale": "platinum",
"requirements": ["pysaunum==0.2.0"]
"requirements": ["pysaunum==0.3.0"]
}

View File

@@ -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"]
}

View File

@@ -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"]

View File

@@ -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},

View 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

View 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

View File

@@ -1,4 +1,12 @@
{
"conditions": {
"is_off": {
"condition": "mdi:bullhorn-outline"
},
"is_on": {
"condition": "mdi:bullhorn"
}
},
"entity_component": {
"_": {
"default": "mdi:bullhorn"

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -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,),

View File

@@ -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:

View File

@@ -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,

View File

@@ -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(

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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",

View File

@@ -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",

View File

@@ -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."

View File

@@ -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."""

View File

@@ -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.",

View File

@@ -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" },

View File

@@ -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"
},

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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}",

View File

@@ -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."""

View File

@@ -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",

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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"),
[

View File

@@ -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"),
[

View File

@@ -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"),
[

View File

@@ -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."""

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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