Compare commits

..

18 Commits

Author SHA1 Message Date
Abílio Costa abd8d85225 Add validator subagents to github-pr-reviewer skill (#171370) 2026-05-20 14:48:29 +01:00
Jordan Harvey 626a1a5c87 Remove positional message strings when translation_key is set in nintendo_parental_controls (#171531) 2026-05-20 15:47:43 +02:00
Jarkko Pöyry 1b2e8ccc0f Avoid polling in wled integration (#161183)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-20 15:47:07 +02:00
Mike O'Driscoll 4da2cd465a casper_glow: fix missing translation for exception (#171534) 2026-05-20 15:47:01 +02:00
Thomas55555 e3c31a3482 Allow setting a custom laqi in Google Air Quality (#160681)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-05-20 15:41:45 +02:00
A. Gideonse a0b52e0f58 Bump indevolt-api to 1.8.1 (#171472) 2026-05-20 14:37:01 +01:00
Andrew Jackson 75dd509c7b Add translations to Mastodon exceptions (#171528) 2026-05-20 15:33:01 +02:00
Max Michels 6540ccd52a Replace duplicate constants with homeassistant.const imports in citybikes (#171478) 2026-05-20 15:18:12 +02:00
Ronald van der Meer a35ad41495 Fix untranslated config entry error in Duco (#171514) 2026-05-20 15:17:32 +02:00
Max Michels cedf5a5861 Replace duplicate constants with homeassistant.const imports hddtemp (#171517) 2026-05-20 15:15:55 +02:00
Alexey Masolov 16f4dc74bf Add TIMEOUT constant to CalDAV integration (#171463)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 15:08:42 +02:00
Franck Nijhof c5f22936e4 Use HA timezone for date in saj (#171450) 2026-05-20 14:57:34 +02:00
Jan Čermák aa23b3176c Bump base image to 2026.05.0 with Python 3.14.5, use 3.14.5 in CI (#171482) 2026-05-20 14:43:54 +02:00
Franck Nijhof a144bbab2b Fix Wyoming satellite crash when TTS is not configured (#171513) 2026-05-20 14:30:20 +02:00
Erik Montnemery 6a20b99252 Adjust device_registry.async_setup (#167653) 2026-05-20 14:28:10 +02:00
Franck Nijhof 8a12c06116 Fix PowerView cover crash when shade position is unavailable (#171471) 2026-05-20 13:50:49 +02:00
Alistair Francis 5dc057b36d husqvarna_automower_ble: Gracefully handle unreachable device (#171479)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2026-05-20 13:48:11 +02:00
Robert Resch 6d6f14a0aa Revert "Bump py-opendisplay to 7.0.0" (#171477) 2026-05-20 13:42:15 +02:00
55 changed files with 4470 additions and 141 deletions
@@ -18,6 +18,13 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
+1
View File
@@ -19,6 +19,7 @@ machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
requirements_test_all.txt linguist-generated=true
requirements_test_pre_commit.txt linguist-generated=true
script/hassfest/docker/Dockerfile linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true
+1 -1
View File
@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.04.0"
BASE_IMAGE_VERSION: "2026.05.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
+4 -2
View File
@@ -59,7 +59,7 @@ standards.
- Home Assistant uses `requirements_all.txt` (all integration packages),
`requirements.txt` (core packages), `requirements_test.txt` (test
dependencies) to
dependencies), and `requirements_test_all.txt` (all test dependencies) to
declare Python dependencies.
- Each integration lists its packages in `homeassistant/components/<name>/manifest.json`
under the `requirements` field.
@@ -80,6 +80,7 @@ lines that were added (`+`) or removed (`-`) in **any** of these files:
- `requirements.txt`
- `requirements_all.txt`
- `requirements_test.txt`
- `requirements_test_all.txt`
- `homeassistant/package_constraints.txt`
- `pyproject.toml`
@@ -399,7 +400,8 @@ Collapsed example (all checks passed):
- For packages that only appear in `homeassistant/package_constraints.txt` or
`pyproject.toml` without being tied to a specific integration, the PR
description link requirement still applies.
- When checking test-only packages (from `requirements_test.txt`), apply the same license, repository, and PR
- When checking test-only packages (from `requirements_test.txt` or
`requirements_test_all.txt`), apply the same license, repository, and PR
description checks as for production dependencies.
- A package that appears in both a production file and a test file should only
be reported once; use the production file entry as the canonical one.
+1 -1
View File
@@ -1 +1 @@
3.14.4
3.14.5
+3 -3
View File
@@ -132,7 +132,7 @@
"problemMatcher": []
},
{
"label": "Install all production Requirements",
"label": "Install all Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt",
"group": {
@@ -146,9 +146,9 @@
"problemMatcher": []
},
{
"label": "Install all (test & production) Requirements",
"label": "Install all Test Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
"group": {
"kind": "build",
"isDefault": true
+3 -1
View File
@@ -17,6 +17,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import TIMEOUT
type CalDavConfigEntry = ConfigEntry[caldav.DAVClient]
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
ssl_verify_cert=entry.data[CONF_VERIFY_SSL],
timeout=30,
timeout=TIMEOUT,
)
try:
await hass.async_add_executor_job(client.principal)
@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .const import DOMAIN, TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -65,6 +65,7 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
ssl_verify_cert=user_input[CONF_VERIFY_SSL],
timeout=TIMEOUT,
)
try:
await self.hass.async_add_executor_job(client.principal)
@@ -75,6 +76,9 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
# AuthorizationError can be raised if the url is incorrect or
# on some other unexpected server response.
return "cannot_connect"
except requests.Timeout as err:
_LOGGER.warning("Timeout connecting to CalDAV server: %s", err)
return "cannot_connect"
except requests.ConnectionError as err:
_LOGGER.warning("Connection Error connecting to CalDAV server: %s", err)
return "cannot_connect"
+1
View File
@@ -3,3 +3,4 @@
from typing import Final
DOMAIN: Final = "caldav"
TIMEOUT: Final = 30
@@ -7,6 +7,7 @@ from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [
@@ -24,7 +25,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Casper Glow device with address {address}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"address": address},
)
glow = CasperGlow(ble_device)
@@ -54,6 +54,9 @@
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Casper Glow: {error}"
},
"device_not_found": {
"message": "Could not find Casper Glow device with address {address}"
}
}
}
+2 -4
View File
@@ -17,6 +17,8 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
APPLICATION_NAME,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
@@ -45,10 +47,6 @@ HA_USER_AGENT = (
)
ATTR_UID = "uid"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LATITUDE = "latitude"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LONGITUDE = "longitude"
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_FREE_EBIKES = "free_ebikes"
ATTR_TIMESTAMP = "timestamp"
+5 -1
View File
@@ -60,7 +60,11 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
translation_placeholders={"error": repr(err)},
) from err
except DucoError as err:
raise ConfigEntryError(f"Duco API error: {err}") from err
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"error": repr(err)},
) from err
async def _async_update_data(self) -> DucoData:
"""Fetch node data from the Duco box."""
@@ -5,7 +5,11 @@ from typing import Any
from google_air_quality_api.api import GoogleAirQualityApi
from google_air_quality_api.auth import Auth
from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.exceptions import (
GoogleAirQualityApiError,
InvalidCustomLAQIConfigurationError,
)
from google_air_quality_api.mapping import AQICategoryMapping
import voluptuous as vol
from homeassistant.config_entries import (
@@ -18,6 +22,7 @@ from homeassistant.config_entries import (
)
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
@@ -26,11 +31,28 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import SectionConfig, section
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
from homeassistant.helpers.selector import (
CountrySelector,
LocationSelector,
LocationSelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CONF_REFERRER,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
SECTION_API_KEY_OPTIONS,
)
_LOGGER = logging.getLogger(__name__)
AIR_QUALITY_COVERAGE_URL = (
"https://developers.google.com/maps/documentation/air-quality/coverage"
)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
@@ -50,10 +72,31 @@ async def _validate_input(
description_placeholders: dict[str, str],
) -> bool:
try:
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS) or {}
enable_custom_laqi = custom_options.get(CONF_ENABLE_CUSTOM_LAQI)
if enable_custom_laqi:
country = custom_options.get(CONF_COUNTRY)
custom_laqi = custom_options.get(CUSTOM_LAQI)
# When custom LAQI is enabled, both country and custom_laqi must be provided
if not country or not custom_laqi:
errors[CUSTOM_LOCAL_AQI_OPTIONS] = "missing_custom_laqi_options"
return False
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
region_code=country,
custom_local_aqi=custom_laqi,
)
else:
await api.async_get_current_conditions(
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
)
except InvalidCustomLAQIConfigurationError:
errors["base"] = "mismatch_country_and_laqi"
except GoogleAirQualityApiError as err:
errors["base"] = "cannot_connect"
description_placeholders["error_message"] = str(err)
@@ -79,6 +122,25 @@ def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
CONF_LONGITUDE: hass.config.longitude,
},
): LocationSelector(LocationSelectorConfig(radius=False)),
vol.Optional(CUSTOM_LOCAL_AQI_OPTIONS): section(
vol.Schema(
{
vol.Required(CONF_ENABLE_CUSTOM_LAQI, default=False): bool,
vol.Optional(
CONF_COUNTRY, default=hass.config.country
): CountrySelector(),
vol.Optional(CUSTOM_LAQI): SelectSelector(
SelectSelectorConfig(
options=sorted(
AQICategoryMapping.get_all_laq_indices()
),
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
SectionConfig(collapsed=True),
),
}
)
@@ -123,6 +185,7 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"api_key_url": "https://developers.google.com/maps/documentation/air-quality/get-api-key",
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL,
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
}
if user_input is not None:
@@ -132,10 +195,13 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
return self.async_abort(reason="already_configured")
session = async_get_clientsession(self.hass)
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
auth = Auth(session, user_input[CONF_API_KEY], referrer=referrer)
api = GoogleAirQualityApi(auth)
if await _validate_input(user_input, api, errors, description_placeholders):
subentry_data = dict(user_input[CONF_LOCATION])
custom_opts = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_opts and custom_opts.get(CONF_ENABLE_CUSTOM_LAQI):
subentry_data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_opts
return self.async_create_entry(
title="Google Air Quality",
data={
@@ -145,7 +211,7 @@ class GoogleAirQualityConfigFlow(ConfigFlow, domain=DOMAIN):
subentries=[
{
"subentry_type": "location",
"data": user_input[CONF_LOCATION],
"data": subentry_data,
"title": user_input[CONF_NAME],
"unique_id": None,
},
@@ -185,7 +251,9 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
return self.async_abort(reason="entry_not_loaded")
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
description_placeholders: dict[str, str] = {
"air_quality_coverage_url": AIR_QUALITY_COVERAGE_URL
}
if user_input is not None:
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
errors["base"] = "location_already_configured"
@@ -202,9 +270,13 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
description_placeholders=description_placeholders,
)
if await _validate_input(user_input, api, errors, description_placeholders):
data = dict(user_input[CONF_LOCATION])
custom_options = user_input.get(CUSTOM_LOCAL_AQI_OPTIONS)
if custom_options and custom_options.get(CONF_ENABLE_CUSTOM_LAQI):
data[CUSTOM_LOCAL_AQI_OPTIONS] = custom_options
return self.async_create_entry(
title=user_input[CONF_NAME],
data=user_input[CONF_LOCATION],
data=data,
)
else:
user_input = {}
@@ -2,6 +2,9 @@
from typing import Final
DOMAIN = "google_air_quality"
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
CONF_ENABLE_CUSTOM_LAQI: Final = "enable_custom_laqi"
CONF_REFERRER: Final = "referrer"
CUSTOM_LAQI: Final = "custom_laqi"
CUSTOM_LOCAL_AQI_OPTIONS: Final = "custom_local_aqi_options"
DOMAIN: Final = "google_air_quality"
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
@@ -10,11 +10,16 @@ from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.model import AirQualityCurrentConditionsData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .const import (
CONF_ENABLE_CUSTOM_LAQI,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@@ -49,11 +54,27 @@ class GoogleAirQualityUpdateCoordinator(
subentry = config_entry.subentries[subentry_id]
self.lat = subentry.data[CONF_LATITUDE]
self.long = subentry.data[CONF_LONGITUDE]
self.custom_local_aqi: str | None = None
self.region_code: str | None = None
options = subentry.data.get(CUSTOM_LOCAL_AQI_OPTIONS)
if isinstance(options, dict) and options.get(CONF_ENABLE_CUSTOM_LAQI):
custom_laqi = options.get(CUSTOM_LAQI)
region_code = options.get(CONF_COUNTRY)
if custom_laqi is not None and region_code is not None:
self.custom_local_aqi = custom_laqi
self.region_code = region_code
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
"""Fetch air quality data for this coordinate."""
try:
return await self.client.async_get_current_conditions(self.lat, self.long)
return await self.client.async_get_current_conditions(
lat=self.lat,
lon=self.long,
region_code=self.region_code,
custom_local_aqi=self.custom_local_aqi,
)
except GoogleAirQualityApiError as ex:
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
raise UpdateFailed(
@@ -204,7 +204,6 @@ async def async_setup_entry(
for subentry_id, subentry in entry.subentries.items():
coordinator = coordinators[subentry_id]
_LOGGER.debug("subentry.data: %s", subentry.data)
async_add_entities(
(
AirQualitySensorEntity(coordinator, description, subentry_id, subentry)
@@ -15,6 +15,8 @@
},
"error": {
"cannot_connect": "Unable to connect to the Google Air Quality API:\n\n{error_message}",
"mismatch_country_and_laqi": "This local AQI is not available for the selected country. Please select an available combination.",
"missing_custom_laqi_options": "Please provide both country and custom local AQI when custom local AQI is enabled.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
@@ -39,6 +41,20 @@
"referrer": "Specify this only if the API key has a [website application restriction]({restricting_api_keys_url})."
},
"name": "Optional API key options"
},
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "Custom local AQI",
"enable_custom_laqi": "Enable custom local AQI"
},
"data_description": {
"country": "Country of the location",
"custom_laqi": "The target air quality index",
"enable_custom_laqi": "Select to enable a custom local air quality index"
},
"description": "Country and custom local AQI must match. You can find the available combinations here: {air_quality_coverage_url}",
"name": "Custom local AQI options"
}
}
}
@@ -51,8 +67,11 @@
},
"entry_type": "Air quality location",
"error": {
"cannot_connect": "[%key:component::google_air_quality::config::error::cannot_connect%]",
"location_already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"location_name_already_configured": "Location name already configured.",
"mismatch_country_and_laqi": "[%key:component::google_air_quality::config::error::mismatch_country_and_laqi%]",
"missing_custom_laqi_options": "[%key:component::google_air_quality::config::error::missing_custom_laqi_options%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
@@ -69,6 +88,22 @@
"name": "[%key:component::google_air_quality::config::step::user::data_description::name%]"
},
"description": "Select the coordinates for which you want to create an entry.",
"sections": {
"custom_local_aqi_options": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data::enable_custom_laqi%]"
},
"data_description": {
"country": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::country%]",
"custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::custom_laqi%]",
"enable_custom_laqi": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::data_description::enable_custom_laqi%]"
},
"description": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::description%]",
"name": "[%key:component::google_air_quality::config::step::user::sections::custom_local_aqi_options::name%]"
}
},
"title": "Air quality data location"
}
}
+1 -2
View File
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_MODEL,
CONF_DISKS,
CONF_HOST,
CONF_NAME,
@@ -28,8 +29,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 7634
@@ -133,6 +133,11 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
"""
return self._is_hard_wired
@property
def available(self) -> bool:
"""Return True if shade position data is available."""
return super().available and self.positions.primary is not None
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
@@ -156,6 +156,10 @@ class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN):
assert self.address
if device is None:
LOGGER.debug("Could not find device with address '%s'", self.address)
return None
try:
(manufacturer, device_type, _model) = await Mower(
channel_id, self.address
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["indevolt-api==1.8.0"]
"requirements": ["indevolt-api==1.8.1"]
}
@@ -53,7 +53,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: MastodonConfigEntry) ->
translation_key="auth_failed",
) from error
except MastodonError as ex:
raise ConfigEntryNotReady("Failed to connect") from ex
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_connect",
) from ex
assert entry.unique_id
@@ -62,6 +62,9 @@ class MastodonCoordinator(DataUpdateCoordinator[Account]):
translation_key="auth_failed",
) from error
except MastodonError as ex:
raise UpdateFailed(ex) from ex
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
) from ex
return account
@@ -101,6 +101,9 @@
"auth_failed": {
"message": "Authentication failed, please reauthenticate with Mastodon."
},
"failed_to_connect": {
"message": "Failed to connect."
},
"idempotency_key_too_short": {
"message": "Idempotency key must be at least 4 characters long."
},
@@ -130,6 +133,9 @@
},
"unable_to_upload_image": {
"message": "Unable to upload image {media_path}."
},
"update_failed": {
"message": "Update failed."
}
},
"selector": {
@@ -53,7 +53,8 @@ class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
return await self.api.update()
except InvalidOAuthConfigurationException as err:
raise ConfigEntryError(
err, translation_domain=DOMAIN, translation_key="invalid_auth"
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except NoDevicesFoundException as err:
raise ConfigEntryError(
@@ -15,5 +15,5 @@
"iot_class": "local_push",
"loggers": ["opendisplay"],
"quality_scale": "silver",
"requirements": ["py-opendisplay==7.0.0"]
"requirements": ["py-opendisplay==5.9.0"]
}
@@ -218,7 +218,7 @@ async def _async_upload_image(call: ServiceCall) -> None:
pil_image,
refresh_mode=refresh_mode,
dither_mode=dither_mode,
tone=tone_compression,
tone_compression=tone_compression,
fit=fit_mode,
rotate=rotation,
)
+2 -1
View File
@@ -34,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -123,7 +124,7 @@ async def async_setup_platform(
# Sensors with live values like "temperature" or "current_power"
# will also be reset to None.
if not success and (
(sensor.per_day_basis and date.today() > sensor.date_updated) # noqa: DTZ011
(sensor.per_day_basis and dt_util.now().date() > sensor.date_updated)
or (not sensor.per_day_basis and not sensor.per_total_basis)
):
state_unknown = True
+26 -20
View File
@@ -97,29 +97,35 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]):
async def listen() -> None:
"""Listen for state changes via WebSocket."""
try:
await self.wled.connect()
except WLEDError as err:
self.logger.info(err)
try:
await self.wled.connect()
except WLEDError as err:
self.logger.info(err)
return
try:
# Stop polling as long as we have a websocket. WS will push
# updates to us
self.update_interval = None
await self.wled.listen(callback=self.async_set_updated_data)
except WLEDConnectionClosedError as err:
self.last_update_success = False
self.logger.info(err)
except WLEDError as err:
self.last_update_success = False
self.async_update_listeners()
self.logger.error(err)
finally:
# Pull data immediately and restart polling
self.update_interval = SCAN_INTERVAL
self.hass.async_create_task(self.async_request_refresh())
# Ensure we are disconnected
await self.wled.disconnect()
finally:
if self.unsub:
self.unsub()
self.unsub = None
return
try:
await self.wled.listen(callback=self.async_set_updated_data)
except WLEDConnectionClosedError as err:
self.last_update_success = False
self.logger.info(err)
except WLEDError as err:
self.last_update_success = False
self.async_update_listeners()
self.logger.error(err)
# Ensure we are disconnected
await self.wled.disconnect()
if self.unsub:
self.unsub()
self.unsub = None
async def close_websocket(_: Event) -> None:
"""Close WebSocket connection."""
@@ -193,7 +193,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
return
if event.type == assist_pipeline.PipelineEventType.RUN_START:
if event.data and (tts_output := event.data["tts_output"]):
if event.data and (tts_output := event.data.get("tts_output")):
# Get stream token early.
# If "tts_start_streaming" is True in INTENT_PROGRESS event, we
# can start streaming TTS before the TTS_END event.
+12 -19
View File
@@ -35,7 +35,6 @@ from .deprecation import deprecated_function
from .frame import ReportBehavior, report_usage
from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment
from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType
from .singleton import singleton
from .typing import UNDEFINED, UndefinedType
if TYPE_CHECKING:
@@ -818,11 +817,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
devices: ActiveDeviceRegistryItems
deleted_devices: DeviceRegistryItems[DeletedDeviceEntry]
_device_data: dict[str, DeviceEntry]
_loaded_event: asyncio.Event | None = None
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the device registry."""
self.hass = hass
self._loaded_event = asyncio.Event()
self._store = DeviceRegistryStore(
hass,
STORAGE_VERSION_MAJOR,
@@ -832,11 +831,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
serialize_in_event_loop=False,
)
@callback
def async_setup(self) -> None:
"""Set up the registry."""
self._loaded_event = asyncio.Event()
@callback
def async_get(self, device_id: str) -> DeviceEntry | None:
"""Get device.
@@ -1522,8 +1516,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
async def _async_load(self) -> None:
"""Load the device registry."""
assert self._loaded_event is not None
assert not self._loaded_event.is_set()
if self._loaded_event.is_set():
raise RuntimeError("Device registry is already loaded")
async_setup_cleanup(self.hass, self)
@@ -1625,12 +1619,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self._loaded_event.set()
async def async_wait_loaded(self) -> None:
"""Wait until the device registry is fully loaded.
Will only wait if the registry had already been set up.
"""
if self._loaded_event is not None:
await self._loaded_event.wait()
"""Wait until the device registry is fully loaded."""
await self._loaded_event.wait()
@callback
def _data_to_save(self) -> dict[str, Any]:
@@ -1772,16 +1762,19 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
@callback
@singleton(DATA_REGISTRY)
def async_get(hass: HomeAssistant) -> DeviceRegistry:
"""Get device registry."""
return DeviceRegistry(hass)
try:
return hass.data[DATA_REGISTRY]
except KeyError as ex:
raise RuntimeError("Device registry not set up") from ex
def async_setup(hass: HomeAssistant) -> None:
"""Set up device registry."""
assert DATA_REGISTRY not in hass.data
async_get(hass).async_setup()
if DATA_REGISTRY in hass.data:
raise RuntimeError("Device registry is already set up")
hass.data[DATA_REGISTRY] = DeviceRegistry(hass)
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
+2 -2
View File
@@ -1347,7 +1347,7 @@ imgw_pib==2.2.0
incomfort-client==0.7.0
# homeassistant.components.indevolt
indevolt-api==1.8.0
indevolt-api==1.8.1
# homeassistant.components.influxdb
influxdb-client==1.50.0
@@ -1923,7 +1923,7 @@ py-nightscout==1.2.2
py-nymta==0.4.0
# homeassistant.components.opendisplay
py-opendisplay==7.0.0
py-opendisplay==5.9.0
# homeassistant.components.schluter
py-schluter==0.1.7
+2941
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -9,8 +9,7 @@ cd "$(realpath "$(dirname "$0")/..")"
echo "Installing development dependencies..."
uv pip install \
-e . \
-r requirements_all.txt \
-r requirements_test.txt \
-r requirements_test_all.txt \
colorlog \
--upgrade \
--config-settings editable_mode=compat
+40
View File
@@ -262,6 +262,19 @@ IGNORE_PRE_COMMIT_HOOK_ID = (
PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$")
def has_tests(module: str) -> bool:
"""Test if a module has tests.
Module format: homeassistant.components.hue
Test if exists: tests/components/hue/__init__.py
"""
path = (
Path(module.replace(".", "/").replace("homeassistant", "tests", 1))
/ "__init__.py"
)
return path.exists()
def explore_module(package: str, explore_children: bool) -> list[str]:
"""Explore the modules."""
module = importlib.import_module(package)
@@ -498,6 +511,31 @@ def requirements_all_action_output(reqs: dict[str, list[str]], action: str) -> s
return "".join(output)
def requirements_test_all_output(reqs: dict[str, list[str]]) -> str:
"""Generate output for test_requirements."""
output = [
"# Home Assistant tests, full dependency set\n",
GENERATED_MESSAGE,
"-r requirements_test.txt\n",
]
filtered = {
requirement: modules
for requirement, modules in reqs.items()
if any(
# Always install requirements that are not part of integrations
not mdl.startswith("homeassistant.components.")
or
# Install tests for integrations that have tests
has_tests(mdl)
for mdl in modules
)
}
output.append(generate_requirements_list(filtered))
return "".join(output)
def requirements_pre_commit_output() -> str:
"""Generate output for pre-commit dependencies."""
source = ".pre-commit-config.yaml"
@@ -571,6 +609,7 @@ def main(validate: bool, ci: bool) -> int:
action: requirements_all_action_output(data, action)
for action in OVERRIDDEN_REQUIREMENTS_ACTIONS
}
reqs_test_all_file = requirements_test_all_output(data)
# Always calling requirements_pre_commit_output is intentional to ensure
# the code is called by the pre-commit hooks.
reqs_pre_commit_file = requirements_pre_commit_output()
@@ -580,6 +619,7 @@ def main(validate: bool, ci: bool) -> int:
("requirements.txt", reqs_file),
("requirements_all.txt", reqs_all_file),
("requirements_test_pre_commit.txt", reqs_pre_commit_file),
("requirements_test_all.txt", reqs_test_all_file),
("homeassistant/package_constraints.txt", constraints),
]
if ci:
+1 -1
View File
@@ -2,7 +2,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
FROM python:3.14.4-alpine
FROM python:3.14.5-alpine
ENV \
UV_SYSTEM_PYTHON=true \
-1
View File
@@ -766,7 +766,6 @@ def mock_device_registry(
registry.deleted_devices = dr.DeviceRegistryItems()
hass.data[dr.DATA_REGISTRY] = registry
dr.async_get.cache_clear()
return registry
@@ -64,6 +64,7 @@ async def test_form(
("side_effect", "expected_error"),
[
(Exception(), "unknown"),
(requests.Timeout(), "cannot_connect"),
(requests.ConnectionError(), "cannot_connect"),
(DAVError(), "cannot_connect"),
(AuthorizationError(reason="Unauthorized"), "invalid_auth"),
+9
View File
@@ -58,6 +58,15 @@ async def test_setup_entry_error(
await hass.async_block_till_done()
assert mock_config_entry.state is expected_state
if (
method == "async_get_board_info"
and isinstance(exception, DucoError)
and expected_state is ConfigEntryState.SETUP_ERROR
):
assert mock_config_entry.error_reason_translation_key == "api_error"
assert mock_config_entry.error_reason_translation_placeholders == {
"error": repr(exception)
}
@pytest.mark.usefixtures("mock_duco_client")
@@ -8,9 +8,19 @@ from google_air_quality_api.model import AirQualityCurrentConditionsData
import pytest
from homeassistant.components.google_air_quality import CONF_REFERRER
from homeassistant.components.google_air_quality.const import DOMAIN
from homeassistant.components.google_air_quality.const import (
CONF_ENABLE_CUSTOM_LAQI,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
)
from homeassistant.config_entries import ConfigSubentryDataWithId
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_json_object_fixture
@@ -62,14 +72,53 @@ def mock_config_entry(
title=DOMAIN,
data={CONF_API_KEY: "test-api-key", CONF_REFERRER: None},
entry_id="123456789",
subentries_data=[*mock_subentries],
subentries_data=mock_subentries,
)
@pytest.fixture
def mock_subentries_with_custom_laqi() -> list[ConfigSubentryDataWithId]:
"""Fixture for subentries with custom LAQI enabled."""
return [
ConfigSubentryDataWithId(
data={
CONF_LATITUDE: 10.1,
CONF_LONGITUDE: 20.1,
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_ENABLE_CUSTOM_LAQI: True,
CUSTOM_LAQI: "deu_uba",
CONF_COUNTRY: "DE",
},
},
subentry_type="location",
title="Home",
subentry_id="home-subentry-id",
unique_id=None,
)
]
@pytest.fixture
def mock_config_entry_with_custom_laqi(
hass: HomeAssistant,
mock_subentries_with_custom_laqi: list[ConfigSubentryDataWithId],
) -> MockConfigEntry:
"""Fixture for config entry with custom LAQI."""
return MockConfigEntry(
domain=DOMAIN,
title=DOMAIN,
data={CONF_API_KEY: "test-api-key", CONF_REFERRER: None},
entry_id="123456789",
subentries_data=mock_subentries_with_custom_laqi,
)
@pytest.fixture(name="mock_api")
def mock_client_api() -> Generator[Mock]:
def mock_client_api(request: pytest.FixtureRequest) -> Generator[Mock]:
"""Set up fake Google Air Quality API responses from fixtures."""
responses = load_json_object_fixture("air_quality_data.json", DOMAIN)
filename = getattr(request, "param", "air_quality_data.json")
responses = load_json_object_fixture(filename, DOMAIN)
with (
patch(
"homeassistant.components.google_air_quality.GoogleAirQualityApi",
@@ -0,0 +1,88 @@
{
"dateTime": "2026-02-01T11:00:00Z",
"regionCode": "de",
"indexes": [
{
"code": "deu_lubw",
"displayName": "LuQx (DE)",
"aqi": 3,
"aqiDisplay": "3",
"color": {
"red": 0.6,
"green": 1,
"blue": 1
},
"category": "Satisfactory air quality",
"dominantPollutant": "pm10"
},
{
"code": "uaqi",
"displayName": "Universal AQI",
"aqi": 57,
"aqiDisplay": "57",
"color": {
"red": 0.827451,
"green": 0.93333334,
"blue": 0.06666667
},
"category": "Moderate air quality",
"dominantPollutant": "pm25"
}
],
"pollutants": [
{
"code": "no2",
"displayName": "NO2",
"fullName": "Nitrogen dioxide",
"concentration": {
"value": 14.25,
"units": "PARTS_PER_BILLION"
}
},
{
"code": "o3",
"displayName": "O3",
"fullName": "Ozone",
"concentration": {
"value": 8.4,
"units": "PARTS_PER_BILLION"
}
},
{
"code": "pm10",
"displayName": "PM10",
"fullName": "Inhalable particulate matter (\u003c10µm)",
"concentration": {
"value": 32.04,
"units": "MICROGRAMS_PER_CUBIC_METER"
}
},
{
"code": "pm25",
"displayName": "PM2.5",
"fullName": "Fine particulate matter (\u003c2.5µm)",
"concentration": {
"value": 27.23,
"units": "MICROGRAMS_PER_CUBIC_METER"
}
},
{
"code": "co",
"displayName": "CO",
"fullName": "Carbon monoxide",
"concentration": {
"value": 349.3,
"units": "PARTS_PER_BILLION"
}
},
{
"code": "so2",
"displayName": "SO2",
"fullName": "Sulfur dioxide",
"concentration": {
"value": 1.13,
"units": "PARTS_PER_BILLION"
}
}
]
}
@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_sensor_snapshot[sensor.home_ammonia-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_ammonia-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -38,7 +38,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_ammonia-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_ammonia-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -54,7 +54,7 @@
'state': '81.41',
})
# ---
# name: test_sensor_snapshot[sensor.home_benzene-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_benzene-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -93,7 +93,7 @@
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor_snapshot[sensor.home_benzene-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_benzene-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -109,7 +109,7 @@
'state': '0.24',
})
# ---
# name: test_sensor_snapshot[sensor.home_carbon_monoxide-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_carbon_monoxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -151,7 +151,7 @@
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensor_snapshot[sensor.home_carbon_monoxide-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_carbon_monoxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -168,7 +168,7 @@
'state': '0.26902',
})
# ---
# name: test_sensor_snapshot[sensor.home_lqi_de_category-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_lqi_de_category-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -213,7 +213,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.home_lqi_de_category-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_lqi_de_category-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -235,7 +235,7 @@
'state': 'good_air_quality',
})
# ---
# name: test_sensor_snapshot[sensor.home_lqi_de_dominant_pollutant-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_lqi_de_dominant_pollutant-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -279,7 +279,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.home_lqi_de_dominant_pollutant-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_lqi_de_dominant_pollutant-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -300,7 +300,7 @@
'state': 'no2',
})
# ---
# name: test_sensor_snapshot[sensor.home_nitrogen_dioxide-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_nitrogen_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -339,7 +339,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_nitrogen_dioxide-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_nitrogen_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -356,7 +356,7 @@
'state': '14.18',
})
# ---
# name: test_sensor_snapshot[sensor.home_nitrogen_monoxide-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_nitrogen_monoxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -395,7 +395,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_nitrogen_monoxide-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_nitrogen_monoxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -412,7 +412,7 @@
'state': '0.62',
})
# ---
# name: test_sensor_snapshot[sensor.home_non_methane_hydrocarbons-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_non_methane_hydrocarbons-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -451,7 +451,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_non_methane_hydrocarbons-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_non_methane_hydrocarbons-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -467,7 +467,7 @@
'state': '52.66',
})
# ---
# name: test_sensor_snapshot[sensor.home_ozone-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_ozone-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -506,7 +506,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_ozone-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_ozone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -523,7 +523,7 @@
'state': '24.94',
})
# ---
# name: test_sensor_snapshot[sensor.home_pm10-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm10-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -562,7 +562,7 @@
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor_snapshot[sensor.home_pm10-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm10-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -579,7 +579,7 @@
'state': '21.95',
})
# ---
# name: test_sensor_snapshot[sensor.home_pm2_5-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm2_5-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -618,7 +618,7 @@
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor_snapshot[sensor.home_pm2_5-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm2_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -635,7 +635,7 @@
'state': '10.6',
})
# ---
# name: test_sensor_snapshot[sensor.home_sulphur_dioxide-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_sulphur_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -674,7 +674,7 @@
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[sensor.home_sulphur_dioxide-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_sulphur_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -691,7 +691,7 @@
'state': '1.2',
})
# ---
# name: test_sensor_snapshot[sensor.home_uaqi_category-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_uaqi_category-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -736,7 +736,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.home_uaqi_category-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_uaqi_category-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -758,7 +758,7 @@
'state': 'excellent_air_quality',
})
# ---
# name: test_sensor_snapshot[sensor.home_uaqi_dominant_pollutant-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_uaqi_dominant_pollutant-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -804,7 +804,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.home_uaqi_dominant_pollutant-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_uaqi_dominant_pollutant-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -827,7 +827,7 @@
'state': 'o3',
})
# ---
# name: test_sensor_snapshot[sensor.home_universal_air_quality_index-entry]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_universal_air_quality_index-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
@@ -866,7 +866,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[sensor.home_universal_air_quality_index-state]
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_universal_air_quality_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
@@ -882,3 +882,724 @@
'state': '80',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_carbon_monoxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_carbon_monoxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Carbon monoxide',
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': 'ppm',
}),
}),
'original_device_class': <SensorDeviceClass.CO: 'carbon_monoxide'>,
'original_icon': None,
'original_name': 'Carbon monoxide',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'co_10.1_20.1',
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_carbon_monoxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'carbon_monoxide',
'friendly_name': 'Home Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.home_carbon_monoxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.3493',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_aqi-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_luqx_de_aqi',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LuQx (DE) AQI',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.AQI: 'aqi'>,
'original_icon': None,
'original_name': 'LuQx (DE) AQI',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'local_aqi',
'unique_id': 'local_aqi_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_aqi-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'aqi',
'friendly_name': 'Home LuQx (DE) AQI',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_luqx_de_aqi',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_category-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'options': list([
'very_good_air_quality',
'good_air_quality',
'satisfactory_air_quality',
'sufficient_air_quality',
'bad_air_quality',
'very_bad_air_quality',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_luqx_de_category',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LuQx (DE) category',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'LuQx (DE) category',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'local_category',
'unique_id': 'local_category_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_category-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'enum',
'friendly_name': 'Home LuQx (DE) category',
'options': list([
'very_good_air_quality',
'good_air_quality',
'satisfactory_air_quality',
'sufficient_air_quality',
'bad_air_quality',
'very_bad_air_quality',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.home_luqx_de_category',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'satisfactory_air_quality',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_dominant_pollutant-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'options': list([
'no2',
'so2',
'co',
'o3',
'pm10',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_luqx_de_dominant_pollutant',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'LuQx (DE) dominant pollutant',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'LuQx (DE) dominant pollutant',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'local_dominant_pollutant',
'unique_id': 'local_dominant_pollutant_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_luqx_de_dominant_pollutant-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'enum',
'friendly_name': 'Home LuQx (DE) dominant pollutant',
'options': list([
'no2',
'so2',
'co',
'o3',
'pm10',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.home_luqx_de_dominant_pollutant',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'pm10',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_nitrogen_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_nitrogen_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Nitrogen dioxide',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.NITROGEN_DIOXIDE: 'nitrogen_dioxide'>,
'original_icon': None,
'original_name': 'Nitrogen dioxide',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'no2_10.1_20.1',
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_nitrogen_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'nitrogen_dioxide',
'friendly_name': 'Home Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppb',
}),
'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '14.25',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_ozone-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_ozone',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Ozone',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.OZONE: 'ozone'>,
'original_icon': None,
'original_name': 'Ozone',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'o3_10.1_20.1',
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_ozone-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'ozone',
'friendly_name': 'Home Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppb',
}),
'context': <ANY>,
'entity_id': 'sensor.home_ozone',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '8.4',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm10-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_pm10',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'PM10',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
'original_icon': None,
'original_name': 'PM10',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm10_10.1_20.1',
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm10-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'pm10',
'friendly_name': 'Home PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm10',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '32.04',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm2_5-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_pm2_5',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'PM2.5',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
'original_icon': None,
'original_name': 'PM2.5',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm25_10.1_20.1',
'unit_of_measurement': 'μg/m³',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm2_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'pm25',
'friendly_name': 'Home PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm2_5',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '27.23',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_sulphur_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_sulphur_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sulphur dioxide',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SULPHUR_DIOXIDE: 'sulphur_dioxide'>,
'original_icon': None,
'original_name': 'Sulphur dioxide',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'so2_10.1_20.1',
'unit_of_measurement': 'ppb',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_sulphur_dioxide-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'sulphur_dioxide',
'friendly_name': 'Home Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppb',
}),
'context': <ANY>,
'entity_id': 'sensor.home_sulphur_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.13',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_uaqi_category-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'options': list([
'excellent_air_quality',
'good_air_quality',
'moderate_air_quality',
'low_air_quality',
'poor_air_quality',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_uaqi_category',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'UAQI category',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'UAQI category',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uaqi_category',
'unique_id': 'uaqi_category_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_uaqi_category-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'enum',
'friendly_name': 'Home UAQI category',
'options': list([
'excellent_air_quality',
'good_air_quality',
'moderate_air_quality',
'low_air_quality',
'poor_air_quality',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.home_uaqi_category',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'moderate_air_quality',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_uaqi_dominant_pollutant-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'options': list([
'co',
'no2',
'o3',
'pm10',
'pm25',
'so2',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_uaqi_dominant_pollutant',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'UAQI dominant pollutant',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'UAQI dominant pollutant',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uaqi_dominant_pollutant',
'unique_id': 'uaqi_dominant_pollutant_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_uaqi_dominant_pollutant-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'enum',
'friendly_name': 'Home UAQI dominant pollutant',
'options': list([
'co',
'no2',
'o3',
'pm10',
'pm25',
'so2',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.home_uaqi_dominant_pollutant',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'pm25',
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_universal_air_quality_index-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.home_universal_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Universal Air Quality Index',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.AQI: 'aqi'>,
'original_icon': None,
'original_name': 'Universal Air Quality Index',
'platform': 'google_air_quality',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uaqi',
'unique_id': 'uaqi_10.1_20.1',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_universal_air_quality_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by Google Air Quality',
'device_class': 'aqi',
'friendly_name': 'Home Universal Air Quality Index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_universal_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '57',
})
# ---
@@ -2,17 +2,24 @@
from unittest.mock import AsyncMock
from google_air_quality_api.exceptions import GoogleAirQualityApiError
from google_air_quality_api.exceptions import (
GoogleAirQualityApiError,
InvalidCustomLAQIConfigurationError,
)
import pytest
from homeassistant.components.google_air_quality.const import (
CONF_ENABLE_CUSTOM_LAQI,
CONF_REFERRER,
CUSTOM_LAQI,
CUSTOM_LOCAL_AQI_OPTIONS,
DOMAIN,
SECTION_API_KEY_OPTIONS,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
CONF_COUNTRY,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
@@ -110,6 +117,7 @@ async def test_form_with_referrer(
@pytest.mark.parametrize(
("api_exception", "expected_error"),
[
(InvalidCustomLAQIConfigurationError(), "mismatch_country_and_laqi"),
(GoogleAirQualityApiError(), "cannot_connect"),
(ValueError(), "unknown"),
],
@@ -301,6 +309,11 @@ async def test_subentry_flow(
CONF_LATITUDE: 30.1,
CONF_LONGITUDE: 40.1,
},
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CUSTOM_LAQI: "deu_uba",
CONF_ENABLE_CUSTOM_LAQI: False,
},
},
)
await hass.async_block_till_done()
@@ -404,3 +417,126 @@ async def test_subentry_flow_entry_not_loaded(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "entry_not_loaded"
async def test_create_entry_with_custom_laqi(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_api: AsyncMock,
) -> None:
"""Test creating a config entry with custom laqi."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: "test-name",
CONF_API_KEY: "test-api-key",
CONF_LOCATION: {CONF_LATITUDE: 10.1, CONF_LONGITUDE: 20.1},
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CONF_ENABLE_CUSTOM_LAQI: True,
},
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"][CUSTOM_LOCAL_AQI_OPTIONS] == "missing_custom_laqi_options"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_NAME: "test-name",
CONF_API_KEY: "test-api-key",
CONF_LOCATION: {CONF_LATITUDE: 10.1, CONF_LONGITUDE: 20.1},
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CUSTOM_LAQI: "deu_lubw",
CONF_ENABLE_CUSTOM_LAQI: True,
},
},
)
mock_api.async_get_current_conditions.assert_called_once_with(
lat=10.1, lon=20.1, region_code="DE", custom_local_aqi="deu_lubw"
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Google Air Quality"
assert result["data"] == {
CONF_API_KEY: "test-api-key",
CONF_REFERRER: None,
}
assert len(result["subentries"]) == 1
subentry = result["subentries"][0]
assert subentry["subentry_type"] == "location"
assert subentry["title"] == "test-name"
assert subentry["data"] == {
CONF_LATITUDE: 10.1,
CONF_LONGITUDE: 20.1,
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CUSTOM_LAQI: "deu_lubw",
CONF_ENABLE_CUSTOM_LAQI: True,
},
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_subentry_flow_with_custom_laqi(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_api: AsyncMock,
) -> None:
"""Test creating a location subentry with a custom local AQI."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_api.async_get_current_conditions.call_count == 1
result = await hass.config_entries.subentries.async_init(
(mock_config_entry.entry_id, "location"),
context={"source": "user"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "location"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
CONF_NAME: "Work",
CONF_LOCATION: {
CONF_LATITUDE: 30.1,
CONF_LONGITUDE: 40.1,
},
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CUSTOM_LAQI: "deu_lubw",
CONF_ENABLE_CUSTOM_LAQI: True,
},
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Work"
assert result["data"] == {
CONF_LATITUDE: 30.1,
CONF_LONGITUDE: 40.1,
CUSTOM_LOCAL_AQI_OPTIONS: {
CONF_COUNTRY: "DE",
CUSTOM_LAQI: "deu_lubw",
CONF_ENABLE_CUSTOM_LAQI: True,
},
}
# Initial setup: 1 of each API call
# Subentry flow validation: 1 current conditions call
# Reload with 2 subentries: 2 of each API call
assert mock_api.async_get_current_conditions.call_count == 1 + 1 + 2
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
assert len(entry.subentries) == 2
@@ -26,6 +26,22 @@ async def test_setup(
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_with_custom_laqi(
hass: HomeAssistant,
mock_config_entry_with_custom_laqi: MockConfigEntry,
mock_api: AsyncMock,
) -> None:
"""Test successful setup with custom LAQI and unload."""
mock_config_entry_with_custom_laqi.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_with_custom_laqi.entry_id)
assert mock_config_entry_with_custom_laqi.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry_with_custom_laqi.entry_id)
await hass.async_block_till_done()
assert mock_config_entry_with_custom_laqi.state is ConfigEntryState.NOT_LOADED
async def test_config_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
@@ -9,19 +10,32 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import snapshot_platform
@pytest.mark.parametrize(
("mock_api", "config_fixture_name"),
[
("air_quality_data.json", "mock_config_entry"),
("air_quality_data_custom_laqi.json", "mock_config_entry_with_custom_laqi"),
],
indirect=("mock_api",),
)
async def test_sensor_snapshot(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
request: pytest.FixtureRequest,
mock_api: AsyncMock,
snapshot: SnapshotAssertion,
config_fixture_name: str,
) -> None:
"""Snapshot test of the sensors."""
mock_config_entry = request.getfixturevalue(config_fixture_name)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
with patch(
@@ -1,7 +1,7 @@
"""Common fixtures for the Husqvarna Automower Bluetooth tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, Mock, patch
from automower_ble.protocol import ResponseResult
from gardena_bluetooth.parse import ManufacturerData
@@ -77,6 +77,8 @@ def mock_get_manufacturer_data(
@pytest.fixture(autouse=True)
def mock_automower_client(enable_bluetooth: None) -> Generator[AsyncMock]:
"""Mock a BleakClient client."""
mock_device = Mock()
mock_device.name = "Mower"
with (
patch(
"homeassistant.components.husqvarna_automower_ble.Mower",
@@ -86,6 +88,10 @@ def mock_automower_client(enable_bluetooth: None) -> Generator[AsyncMock]:
"homeassistant.components.husqvarna_automower_ble.config_flow.Mower",
new=mock_client,
),
patch(
"homeassistant.components.husqvarna_automower_ble.config_flow.bluetooth.async_ble_device_from_address",
return_value=mock_device,
),
):
client = mock_client.return_value
client.connect.return_value = ResponseResult.OK
@@ -428,6 +428,30 @@ async def test_successful_reauth(
assert mock_config_entry.data[CONF_PIN] == "1234"
async def test_user_device_not_found(hass: HomeAssistant) -> None:
"""Test we handle the device not being found gracefully."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.husqvarna_automower_ble.config_flow.bluetooth.async_ble_device_from_address",
return_value=None,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
CONF_PIN: "1234",
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_user_unable_to_connect(
hass: HomeAssistant,
mock_automower_client: Mock,
@@ -86,6 +86,7 @@
"9101": 32.8,
"9117": 31.9,
"9133": 33.0,
"9270": 31.2,
"9003": 150,
"11005": 35.5,
"7171": 1,
@@ -66,7 +66,6 @@
'11011': 85,
'11016': 0,
'11034': 100,
'11042': 32.1,
'142': 1.79,
'1501': 0,
'1502': 0,
@@ -148,6 +147,7 @@
'9206': 50.9,
'9216': 24.9,
'9218': '**REDACTED**',
'9270': 31.2,
'9279': 1,
}),
'device': dict({
@@ -2481,7 +2481,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_pack_1_mos_temperature',
'unique_id': 'SolidFlex2000-87654321_11042',
'unique_id': 'SolidFlex2000-87654321_9085',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@@ -2498,7 +2498,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '32.1',
'state': '31.5',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_battery_pack_1_sn-entry]
@@ -2818,7 +2818,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_pack_2_mos_temperature',
'unique_id': 'SolidFlex2000-87654321_9085',
'unique_id': 'SolidFlex2000-87654321_9101',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@@ -2835,7 +2835,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '31.5',
'state': '32.8',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_battery_pack_2_sn-entry]
@@ -3155,7 +3155,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_pack_3_mos_temperature',
'unique_id': 'SolidFlex2000-87654321_9101',
'unique_id': 'SolidFlex2000-87654321_9117',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@@ -3172,7 +3172,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '32.8',
'state': '31.9',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_battery_pack_3_sn-entry]
@@ -3492,7 +3492,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_pack_4_mos_temperature',
'unique_id': 'SolidFlex2000-87654321_9117',
'unique_id': 'SolidFlex2000-87654321_9133',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@@ -3509,7 +3509,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '31.9',
'state': '33.0',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_battery_pack_4_sn-entry]
@@ -3829,7 +3829,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_pack_5_mos_temperature',
'unique_id': 'SolidFlex2000-87654321_9133',
'unique_id': 'SolidFlex2000-87654321_9270',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
@@ -3846,7 +3846,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '33.0',
'state': '31.2',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_battery_pack_5_sn-entry]
+27 -3
View File
@@ -125,8 +125,10 @@ async def test_websocket(
assert mock_bus.async_listen_once.return_value.call_count == 0
# Send update from WebSocket
# Note we may still update() during websocket setup
# so update it too.
mock_wled.update.return_value.state.on = False
updated_device = deepcopy(mock_wled.update.return_value)
updated_device.state.on = False
callback(updated_device)
await hass.async_block_till_done()
@@ -135,7 +137,15 @@ async def test_websocket(
assert state
assert state.state == STATE_OFF
# Resolve Future with a connection losed.
# Listening for changes on websocket, polling is suspended
num_updates_before_websocket = mock_wled.update.call_count
for _scans in range(4):
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_wled.update.call_count == num_updates_before_websocket
# Resolve Future with a closed connection.
connection_finished.set_exception(WLEDConnectionClosedError)
await hass.async_block_till_done()
@@ -173,15 +183,29 @@ async def test_websocket_error(
async_fire_time_changed(hass)
await connection_connected
# Resolve Future with an error.
# Resolve listen() with an error. This causes polling
# to take over so fail polling update() too
mock_wled.update.side_effect = WLEDError
mock_wled.listen.side_effect = WLEDError
connection_finished.set_exception(WLEDError)
await hass.async_block_till_done()
# Light no longer available as an error occurred
# and polling couldn't take over.
state = hass.states.get("light.wled_websocket")
assert state
assert state.state == STATE_UNAVAILABLE
# Light becomes available after polling takes over
mock_wled.update.side_effect = None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("light.wled_websocket")
assert state
assert state.state == STATE_ON
@pytest.mark.parametrize("device_fixture", ["rgb_websocket"])
async def test_websocket_disconnect_on_home_assistant_stop(
@@ -817,6 +817,57 @@ async def test_on_pipeline_event_ignores_disconnected_client(
assert not mock_client.error_event.is_set()
async def test_run_start_without_tts(
hass: HomeAssistant,
) -> None:
"""Test RUN_START event without tts_output does not crash.
Regression test for https://github.com/home-assistant/core/issues/165734
"""
events: list[Event] = [
RunPipeline(
start_stage=PipelineStage.WAKE, end_stage=PipelineStage.TTS
).event(),
]
pipeline_event = asyncio.Event()
def _async_pipeline_from_audio_stream(*args: Any, **kwargs: Any) -> None:
pipeline_event.set()
with (
patch(
"homeassistant.components.wyoming.data.load_wyoming_info",
return_value=SATELLITE_INFO,
),
patch(
"homeassistant.components.wyoming.assist_satellite.AsyncTcpClient",
SatelliteAsyncTcpClient(events),
) as mock_client,
patch(
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
wraps=_async_pipeline_from_audio_stream,
) as mock_run_pipeline,
):
await setup_config_entry(hass)
async with asyncio.timeout(1):
await pipeline_event.wait()
await mock_client.connect_event.wait()
await mock_client.run_satellite_event.wait()
event_callback = mock_run_pipeline.call_args.kwargs["event_callback"]
# Fire RUN_START without tts_output (TTS not configured)
# must not raise KeyError
event_callback(
assist_pipeline.PipelineEvent(
assist_pipeline.PipelineEventType.RUN_START,
{"pipeline": "test", "language": "en"},
)
)
async def test_announce_raises_when_client_disconnected(
hass: HomeAssistant,
) -> None:
+17 -1
View File
@@ -303,6 +303,23 @@ async def test_multiple_config_subentries(
}
@pytest.mark.parametrize("load_registries", [False])
async def test_async_get_before_setup_raises(hass: HomeAssistant) -> None:
"""Test async_get raises when the registry has not been set up."""
with pytest.raises(RuntimeError, match="Device registry not set up"):
dr.async_get(hass)
dr.async_setup(hass)
assert isinstance(dr.async_get(hass), dr.DeviceRegistry)
async def test_async_load_twice_raises(hass: HomeAssistant) -> None:
"""Test loading the device registry twice raises."""
registry = dr.async_get(hass)
with pytest.raises(RuntimeError, match="Device registry is already loaded"):
await registry.async_load()
@pytest.mark.parametrize("load_registries", [False])
@pytest.mark.usefixtures("freezer")
async def test_loading_from_storage(
@@ -2637,7 +2654,6 @@ async def test_loading_saving_data(
# Now load written data in new registry
registry2 = dr.DeviceRegistry(hass)
await flush_store(device_registry._store)
registry2.async_setup()
await registry2.async_load()
# Ensure same order
+18
View File
@@ -789,6 +789,9 @@ async def test_filter_on_load(
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)
@@ -960,6 +963,9 @@ async def test_load_bad_data(
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)
@@ -1267,6 +1273,9 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any])
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)
@@ -1383,6 +1392,9 @@ async def test_migration_1_7(hass: HomeAssistant, hass_storage: dict[str, Any])
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)
@@ -1455,6 +1467,9 @@ async def test_migration_1_11(
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)
@@ -1622,6 +1637,9 @@ async def test_migration_1_18(
},
}
dr.async_setup(hass)
await dr.async_load(hass)
await er.async_load(hass)
registry = er.async_get(hass)