Compare commits

...

46 Commits

Author SHA1 Message Date
Markus Adrario 5c73ad0310 Homee: exception-translations (#171995) 2026-05-25 09:13:38 +02:00
J. Nick Koston 4a04a271ec Bump cached-ipaddress to 1.1.1 (#172110) 2026-05-25 08:40:05 +02:00
J. Nick Koston 52c27bdea5 Bump inkbird-ble to 1.2.3 (#172113) 2026-05-25 08:39:42 +02:00
J. Nick Koston 6fdc52c002 Bump dbus-fast to 5.0.9 (#172118) 2026-05-25 08:39:19 +02:00
J. Nick Koston e560bbc103 Bump aiodhcpwatcher to 1.2.6 (#172105) 2026-05-24 23:10:11 -05:00
J. Nick Koston b8c573685f Trigger active scan when picking an idasen_desk device in the config flow (#172068) 2026-05-24 23:58:11 -04:00
J. Nick Koston 3764b70b90 Bump bleak, habluetooth, and bleak-retry-connector for BlueZ backend fix (#172094) 2026-05-24 23:57:29 -04:00
Paulus Schoutsen 5d2de6f82b Prefer local file access for streaming in AppleTV (#172102) 2026-05-24 22:56:35 -04:00
fdebrus 64d17f44fa Add aquarite integration (#168051)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-25 00:48:48 +02:00
Sebastian Lövdahl 6f67d44cfe Fix swallowed exceptions in vallox action handlers (#170839) 2026-05-25 00:42:13 +02:00
Robert Svensson def3befb0e Use discovered Axis name for config entry title and device name (#171894) 2026-05-25 00:35:56 +02:00
renovate[bot] 05716ae196 Update infrared-protocols to 5.6.0 (#171916) 2026-05-25 00:29:13 +02:00
rlrghb c0a864297f Update aiolichess to 1.3.0 (#172082)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:26:04 +02:00
J. Nick Koston 04bb84cd03 Add AUTO bluetooth scanner mode to Shelly (#172008) 2026-05-24 14:53:54 -05:00
J. Nick Koston cb55accc3b Use latest service info for INKBIRD fallback poll recency check (#172041) 2026-05-24 14:43:58 -05:00
Erwin Douna d21c227804 SMA refactor validate input (#171956) 2026-05-24 19:59:31 +02:00
Cyrill Raccaud 1ebccd9fa2 Update cookidoo API requirement to version 0.17.2 (#171793) 2026-05-24 19:57:44 +02:00
rlrghb cfbd0f3217 Add puzzles to Lichess integration (#171987)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-24 19:55:07 +02:00
Kamil Breguła 4afb7c0997 Use explicit translation keys in WLED number entities (#171984)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-24 19:49:48 +02:00
Allen Porter 105caccc51 Add shared rainbird device lock and increase calendar timeout (#172002) 2026-05-24 19:38:07 +02:00
J. Nick Koston 6419551117 Trigger active scan when picking an inkbird device in the config flow (#172048) 2026-05-24 19:30:42 +02:00
J. Nick Koston 585bd6616a Trigger active scan when picking a switchbot device in the config flow (#172046) 2026-05-24 19:30:33 +02:00
Max Michels b8dd97cf21 Replace duplicate constants in google_generative_ai_conversation with homeassistant.const imports (#172050) 2026-05-24 19:29:24 +02:00
J. Nick Koston 68fc4aed78 Trigger active scan when picking a govee_ble device in the config flow (#172051) 2026-05-24 19:29:03 +02:00
Max Michels 7dbb259625 Replace duplicate constants in aws_s3 with homeassistant.const imports (#172055) 2026-05-24 19:27:40 +02:00
J. Nick Koston 057eac7fb6 Trigger active scan when picking a rapt_ble device in the config flow (#172054) 2026-05-24 19:27:07 +02:00
Max Michels 31c9cdf742 Replace duplicate constants in xthings_cloud with homeassistant.const imports (#172076) 2026-05-24 19:25:39 +02:00
J. Nick Koston 3147104132 Trigger active scan when picking a togrill device in the config flow (#172072) 2026-05-24 19:22:52 +02:00
Max Michels d6d0f37b52 Replace duplicate constants in clicksend_tts with homeassistant.const imports (#172058) 2026-05-24 19:21:39 +02:00
G Johansson 75e48745a8 Remove useless test from trafikverket_camera (#172059) 2026-05-24 19:21:29 +02:00
J. Nick Koston 533417778c Trigger active scan when picking a sensirion_ble device in the config flow (#172056) 2026-05-24 19:21:21 +02:00
J. Nick Koston e49fd4ebbd Trigger active scan when picking a victron_ble device in the config flow (#172057) 2026-05-24 19:21:18 +02:00
Max Michels 8412b029b1 Replace duplicate constants in cloudflare_r2 with homeassistant.const imports (#172060) 2026-05-24 19:21:08 +02:00
J. Nick Koston c65de7521f Trigger active scan when picking a tilt_ble device in the config flow (#172053) 2026-05-24 19:20:48 +02:00
J. Nick Koston 752c17917e Trigger active scan when picking a ruuvitag_ble device in the config flow (#172062) 2026-05-24 19:19:58 +02:00
Max Michels f643c7ddc6 Replace duplicate constants in intent_script with homeassistant.const imports (#172066) 2026-05-24 19:19:29 +02:00
J. Nick Koston 6f5d4cf991 Trigger active scan when picking a ld2410_ble device in the config flow (#172061) 2026-05-24 19:18:55 +02:00
Max Michels b52466fed1 Replace duplicate constants in linux_battery with homeassistant.const imports (#172070) 2026-05-24 19:18:42 +02:00
J. Nick Koston 189534e32b Trigger active scan when picking a eufylife_ble device in the config flow (#172067) 2026-05-24 19:18:30 +02:00
J. Nick Koston 684ae23b18 Trigger active scan when picking a thermopro device in the config flow (#172052) 2026-05-24 19:17:35 +02:00
J. Nick Koston f4d2f65602 Trigger active scan when picking a qingping device in the config flow (#172071) 2026-05-24 19:17:05 +02:00
J. Nick Koston 65879ff37b Trigger active scan when picking a xiaomi_ble device in the config flow (#172074) 2026-05-24 19:16:57 +02:00
J. Nick Koston d902104bee Trigger active scan when picking a keymitt_ble device in the config flow (#172075) 2026-05-24 19:16:43 +02:00
J. Nick Koston 7bad27c412 Trigger active scan when picking a snooz device in the config flow (#172073) 2026-05-24 19:16:28 +02:00
Max Michels 74a7102cf6 Replace duplicate constants in altruist with homeassistant.const imports (#172078) 2026-05-24 19:14:36 +02:00
Max Michels e88fb03388 Replace duplicate constants in husqvarna_automower with homeassistant.const imports (#172064) 2026-05-24 19:13:21 +02:00
148 changed files with 2608 additions and 408 deletions
+1
View File
@@ -609,6 +609,7 @@ homeassistant.components.valve.*
homeassistant.components.velbus.*
homeassistant.components.velux.*
homeassistant.components.victron_gx.*
homeassistant.components.vistapool.*
homeassistant.components.vivotek.*
homeassistant.components.vlc_telnet.*
homeassistant.components.vodafone_station.*
Generated
+2
View File
@@ -1930,6 +1930,8 @@ CLAUDE.md @home-assistant/core
/tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vistapool/ @fdebrus
/tests/components/vistapool/ @fdebrus
/homeassistant/components/vivotek/ @HarlemSquirrel
/tests/components/vivotek/ @HarlemSquirrel
/homeassistant/components/vizio/ @raman325
@@ -7,10 +7,11 @@ from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_HOST, DOMAIN
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -1,6 +1,3 @@
"""Constants for the Altruist integration."""
DOMAIN = "altruist"
# pylint: disable-next=home-assistant-duplicate-const
CONF_HOST = "host"
@@ -10,13 +10,12 @@ import logging
from altruistclient import AltruistClient, AltruistError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_HOST
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=15)
@@ -345,7 +345,10 @@ class AppleTvMediaPlayer(
play_item = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id
)
media_id = async_process_play_media_url(self.hass, play_item.url)
if play_item.path and self._is_feature_available(FeatureName.StreamFile):
media_id = str(play_item.path)
else:
media_id = async_process_play_media_url(self.hass, play_item.url)
media_type = MediaType.MUSIC
if self._is_feature_available(FeatureName.StreamFile) and (
+2 -1
View File
@@ -17,10 +17,11 @@ from homeassistant.components.backup import (
OnProgressCallback,
suggested_filename,
)
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from . import S3ConfigEntry
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_list_backups_from_s3
_LOGGER = logging.getLogger(__name__)
@@ -8,6 +8,7 @@ from botocore.exceptions import ClientError, ConnectionError, ParamValidationErr
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PREFIX
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
@@ -20,7 +21,6 @@ from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DEFAULT_ENDPOINT_URL,
DESCRIPTION_AWS_S3_DOCS_URL,
-2
View File
@@ -11,8 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
AWS_DOMAIN = "amazonaws.com"
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
@@ -8,10 +8,11 @@ from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_BUCKET, CONF_PREFIX, DOMAIN
from .const import CONF_BUCKET, DOMAIN
from .helpers import async_list_backups_from_s3
SCAN_INTERVAL = timedelta(hours=6)
@@ -5,15 +5,10 @@ from typing import Any
from homeassistant.components.backup import DATA_MANAGER as BACKUP_DATA_MANAGER
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .const import CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_SECRET_ACCESS_KEY, DOMAIN
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
+6 -17
View File
@@ -8,7 +8,6 @@ from urllib.parse import urlsplit
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigEntry,
@@ -139,25 +138,15 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
async def _create_entry(self, serial: str) -> ConfigFlowResult:
"""Create entry for device.
Generate a name to be used as a prefix for device entities.
Use the discovered device name when available.
"""
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]
name = model
for idx in range(len(same_model) + 1):
name = f"{model} {idx}"
if name not in same_model:
break
if (title_placeholders := self.context.get("title_placeholders")) is not None:
name = title_placeholders[CONF_NAME]
else:
name = f"{self.config[CONF_MODEL]} - {serial}"
self.config[CONF_NAME] = name
title = f"{model} - {serial}"
return self.async_create_entry(title=title, data=self.config)
return self.async_create_entry(title=name, data=self.config)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==2.1.1",
"bleak-retry-connector==4.6.0",
"bleak==3.0.2",
"bleak-retry-connector==4.6.1",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.3",
"habluetooth==6.7.2"
"dbus-fast==5.0.9",
"habluetooth==6.7.3"
]
}
@@ -14,6 +14,7 @@ from homeassistant.components.notify import (
)
from homeassistant.const import (
CONF_API_KEY,
CONF_LANGUAGE,
CONF_NAME,
CONF_RECIPIENT,
CONF_USERNAME,
@@ -29,8 +30,6 @@ BASE_API_URL = "https://rest.clicksend.com/v3"
HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
# pylint: disable-next=home-assistant-duplicate-const
CONF_LANGUAGE = "language"
CONF_VOICE = "voice"
MALE_VOICE = "male"
@@ -17,10 +17,11 @@ from homeassistant.components.backup import (
OnProgressCallback,
suggested_filename,
)
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from . import R2ConfigEntry
from .const import CONF_BUCKET, CONF_PREFIX, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
_LOGGER = logging.getLogger(__name__)
CACHE_TTL = 300
@@ -13,6 +13,7 @@ from botocore.exceptions import (
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PREFIX
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
@@ -25,7 +26,6 @@ from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DEFAULT_ENDPOINT_URL,
DESCRIPTION_R2_AUTH_DOCS_URL,
@@ -11,8 +11,6 @@ CONF_ACCESS_KEY_ID = "access_key_id"
CONF_SECRET_ACCESS_KEY = "secret_access_key"
CONF_ENDPOINT_URL = "endpoint_url"
CONF_BUCKET = "bucket"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PREFIX = "prefix"
# R2 is S3-compatible. Endpoint should be like:
# https://<accountid>.r2.cloudflarestorage.com
+53 -28
View File
@@ -42,6 +42,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: CookidooConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
def _migrate_identifiers(
hass: HomeAssistant,
config_entry: CookidooConfigEntry,
old_prefix: str,
new_unique_id: str,
) -> None:
"""Migrate device identifiers and entity unique_ids from old to new prefix."""
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=config_entry.entry_id
)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
new_identifiers = {
(DOMAIN, new_unique_id) if domain == DOMAIN else (domain, identifier)
for domain, identifier in dev.identifiers
}
device_registry.async_update_device(dev.id, new_identifiers=new_identifiers)
for ent in entity_entries:
if ent.unique_id and ent.unique_id.startswith(f"{old_prefix}_"):
entity_registry.async_update_entity(
ent.entity_id,
new_unique_id=f"{new_unique_id}{ent.unique_id[len(old_prefix) :]}",
)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: CookidooConfigEntry
) -> bool:
@@ -49,41 +78,37 @@ async def async_migrate_entry(
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1 and config_entry.minor_version == 1:
# Add the unique uuid
# Add the unique uuid (first migration, entities used config_entry_id as prefix)
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
try:
auth_data = await cookidoo.login()
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
_LOGGER.error(
"Could not migrate config config_entry: %s",
str(e),
)
_LOGGER.error("Could not migrate config entry: %s", e)
return False
unique_id = auth_data.sub
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=config_entry.entry_id
)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
device_registry.async_update_device(
dev.id, new_identifiers={(DOMAIN, unique_id)}
)
for ent in entity_entries:
assert ent.config_entry_id
entity_registry.async_update_entity(
ent.entity_id,
new_unique_id=ent.unique_id.replace(ent.config_entry_id, unique_id),
)
_migrate_identifiers(hass, config_entry, config_entry.entry_id, user_info.id)
hass.config_entries.async_update_entry(
config_entry, unique_id=auth_data.sub, minor_version=2
config_entry, unique_id=user_info.id, minor_version=3
)
if config_entry.version == 1 and config_entry.minor_version == 2:
# Migrate unique_id from old CIAM sub to community profile id
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
try:
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
_LOGGER.error("Could not migrate config entry: %s", e)
return False
old_unique_id = config_entry.unique_id
if old_unique_id:
_migrate_identifiers(hass, config_entry, old_unique_id, user_info.id)
hass.config_entries.async_update_entry(
config_entry, unique_id=user_info.id, minor_version=3
)
_LOGGER.debug(
+12 -2
View File
@@ -3,7 +3,11 @@
from datetime import date, datetime, timedelta
import logging
from cookidoo_api import CookidooAuthException, CookidooException
from cookidoo_api import (
CookidooAuthException,
CookidooException,
CookidooRequestException,
)
from cookidoo_api.types import CookidooCalendarDayRecipe
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
@@ -74,7 +78,13 @@ class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity):
week_day
)
except CookidooAuthException:
await self.coordinator.cookidoo.refresh_token()
try:
await self.coordinator.cookidoo.login()
except (CookidooAuthException, CookidooRequestException) as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="calendar_fetch_failed",
) from exc
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
week_day
)
@@ -54,7 +54,7 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cookidoo."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
COUNTRY_DATA_SCHEMA: dict
LANGUAGE_DATA_SCHEMA: dict
@@ -223,8 +223,9 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
cookidoo = await cookidoo_from_config_data(self.hass, data_input)
try:
auth_data = await cookidoo.login()
self.user_uuid = auth_data.sub
await cookidoo.login()
user_info = await cookidoo.get_user_info()
self.user_uuid = user_info.id
if language_input:
await cookidoo.get_additional_items()
except CookidooRequestException:
@@ -87,7 +87,7 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
)
except CookidooAuthException:
try:
await self.cookidoo.refresh_token()
await self.cookidoo.login()
except CookidooAuthException as exc:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
@@ -96,6 +96,11 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
},
) from exc
except CookidooRequestException as exc:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
) from exc
_LOGGER.debug(
"Authentication failed but re-authentication"
" was successful, trying again later"
+3 -2
View File
@@ -2,11 +2,12 @@
from typing import Any
from aiohttp import CookieJar
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .coordinator import CookidooConfigEntry
@@ -21,7 +22,7 @@ async def cookidoo_from_config_data(
)
return Cookidoo(
async_get_clientsession(hass),
async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)),
CookidooConfig(
email=data[CONF_EMAIL],
password=data[CONF_PASSWORD],
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["cookidoo_api"],
"quality_scale": "silver",
"requirements": ["cookidoo-api==0.14.0"]
"requirements": ["cookidoo-api==0.17.2"]
}
+2 -2
View File
@@ -15,8 +15,8 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.1",
"aiodhcpwatcher==1.2.6",
"aiodiscover==3.2.3",
"cached-ipaddress==1.0.1"
"cached-ipaddress==1.1.1"
]
}
@@ -5,6 +5,7 @@ from typing import Any
from eufylife_ble_client import MODEL_TO_NAME
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -75,6 +76,7 @@ class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN):
data={CONF_MODEL: model},
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -19,7 +19,7 @@ from homeassistant.config_entries import (
ConfigSubentryFlow,
SubentryFlowResult,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME, CONF_PROMPT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
@@ -38,7 +38,6 @@ from .const import (
CONF_HARASSMENT_BLOCK_THRESHOLD,
CONF_HATE_BLOCK_THRESHOLD,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_RECOMMENDED,
CONF_SEXUAL_BLOCK_THRESHOLD,
CONF_TEMPERATURE,
@@ -2,7 +2,7 @@
import logging
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT
from homeassistant.helpers import llm
LOGGER = logging.getLogger(__package__)
@@ -15,8 +15,6 @@ DEFAULT_STT_NAME = "Google AI STT"
DEFAULT_TTS_NAME = "Google AI TTS"
DEFAULT_AI_TASK_NAME = "Google AI Task"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PROMPT = "prompt"
DEFAULT_STT_PROMPT = "Transcribe the attached audio"
CONF_RECOMMENDED = "recommended"
@@ -4,11 +4,11 @@ from typing import Literal
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.const import CONF_LLM_HASS_API, CONF_PROMPT, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_PROMPT, DOMAIN
from .const import DOMAIN
from .entity import GoogleGenerativeAILLMBaseEntity
@@ -7,16 +7,11 @@ from google.genai.types import Part
from homeassistant.components import stt
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_PROMPT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
CONF_CHAT_MODEL,
CONF_PROMPT,
DEFAULT_STT_PROMPT,
LOGGER,
RECOMMENDED_STT_MODEL,
)
from .const import CONF_CHAT_MODEL, DEFAULT_STT_PROMPT, LOGGER, RECOMMENDED_STT_MODEL
from .entity import GoogleGenerativeAILLMBaseEntity
from .helpers import convert_to_wav
@@ -5,6 +5,7 @@ from typing import Any
from govee_ble import GoveeBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -76,6 +77,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN):
title=title, data={CONF_DEVICE_TYPE: device.device_type}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
+6 -2
View File
@@ -54,10 +54,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
try:
await homee.get_access_token()
except HomeeConnectionFailedException as exc:
raise ConfigEntryNotReady(f"Connection to Homee failed: {exc.reason}") from exc
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="connection_failed",
) from exc
except HomeeAuthFailedException as exc:
raise ConfigEntryAuthFailed(
f"Authentication to Homee failed: {exc.reason}"
translation_domain=DOMAIN,
translation_key="auth_failed",
) from exc
hass.loop.create_task(homee.run())
@@ -59,7 +59,7 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues:
@@ -487,9 +487,15 @@
}
},
"exceptions": {
"auth_failed": {
"message": "Authentication to homee failed."
},
"connection_closed": {
"message": "Could not connect to homee while setting attribute."
},
"connection_failed": {
"message": "Connection to homee failed."
},
"disarm_not_supported": {
"message": "Disarm is not supported by homee."
},
@@ -10,6 +10,7 @@ from homeassistant.components.event import (
EventEntity,
EventEntityDescription,
)
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,10 +24,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
ATTR_SEVERITY = "severity"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LATITUDE = "latitude"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LONGITUDE = "longitude"
ATTR_DATE_TIME = "date_time"
@@ -9,6 +9,7 @@ from idasen_ha import Desk
from idasen_ha.errors import AuthFailedError
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -85,6 +86,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery in async_discovered_service_info(self.hass):
if (
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/infrared",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["infrared-protocols==5.4.0"]
"requirements": ["infrared-protocols==5.6.0"]
}
@@ -5,6 +5,7 @@ from typing import Any
from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -74,6 +75,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN):
title=title, data={CONF_DEVICE_TYPE: device_type}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -128,7 +128,18 @@ class INKBIRDActiveBluetoothProcessorCoordinator(
@callback
def _async_schedule_poll(self, _: datetime) -> None:
"""Schedule a poll of the device."""
if self._last_service_info and self._async_needs_poll(
self._last_service_info, self._last_poll
):
# ``self._last_service_info`` only tracks dispatched events, so when
# the device keeps broadcasting the same payload (HA dedupes the
# repeats before dispatch) its timestamp stops advancing. Pull the
# latest service info from the bluetooth manager instead so the
# recency check in ``poll_needed`` sees every observed advertisement.
service_info = (
async_last_service_info(self.hass, self.address, connectable=False)
or self._last_service_info
)
if service_info and self.needs_poll(service_info):
# Seed ``_last_service_info`` so the debounced poll has a service
# info to hand to ``_async_poll_data``; the base ``_async_poll``
# asserts on it.
self._last_service_info = service_info
self._debounced_poll.async_schedule_call()
@@ -63,5 +63,5 @@
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["inkbird-ble==1.2.2"]
"requirements": ["inkbird-ble==1.2.3"]
}
@@ -6,7 +6,7 @@ from typing import Any, TypedDict
import voluptuous as vol
from homeassistant.components.script import CONF_MODE
from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
from homeassistant.const import CONF_ACTION, CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
@@ -29,8 +29,6 @@ CONF_INTENTS = "intents"
CONF_SPEECH = "speech"
CONF_REPROMPT = "reprompt"
# pylint: disable-next=home-assistant-duplicate-const
CONF_ACTION = "action"
CONF_CARD = "card"
CONF_TITLE = "title"
CONF_CONTENT = "content"
@@ -12,6 +12,7 @@ from microbot import (
)
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -83,6 +84,7 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN):
if discovery := self._discovered_adv:
self._discovered_advs[discovery.address] = discovery
else:
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass):
self._ble_device = discovery_info.device
@@ -7,6 +7,7 @@ from bluetooth_data_tools import human_readable_name
from ld2410_ble import BLEAK_EXCEPTIONS, LD2410BLE
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -77,6 +78,7 @@ class Ld2410BleConfigFlow(ConfigFlow, domain=DOMAIN):
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery in async_discovered_service_info(self.hass):
if (
@@ -19,6 +19,12 @@
"classical_rating": {
"default": "mdi:chart-line"
},
"puzzle_games": {
"default": "mdi:puzzle"
},
"puzzle_rating": {
"default": "mdi:chart-line"
},
"rapid_games": {
"default": "mdi:chess-pawn"
},
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["aiolichess==1.2.0"]
"requirements": ["aiolichess==1.3.0"]
}
@@ -79,6 +79,21 @@ SENSORS: tuple[LichessEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state.classical_games,
),
LichessEntityDescription(
key="puzzle_rating",
translation_key="puzzle_rating",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda state: state.puzzle_rating,
),
LichessEntityDescription(
key="puzzle_games",
translation_key="puzzle_games",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda state: state.puzzle_games,
),
)
@@ -42,6 +42,13 @@
"classical_rating": {
"name": "Classical rating"
},
"puzzle_games": {
"name": "Puzzle games",
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
},
"puzzle_rating": {
"name": "Puzzle rating"
},
"rapid_games": {
"name": "Rapid games",
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
@@ -12,7 +12,13 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
)
from homeassistant.const import ATTR_NAME, ATTR_SERIAL_NUMBER, CONF_NAME, PERCENTAGE
from homeassistant.const import (
ATTR_MANUFACTURER,
ATTR_NAME,
ATTR_SERIAL_NUMBER,
CONF_NAME,
PERCENTAGE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -28,8 +34,6 @@ ATTR_CYCLE_COUNT = "cycle_count"
ATTR_ENERGY_FULL = "energy_full"
ATTR_ENERGY_FULL_DESIGN = "energy_full_design"
ATTR_ENERGY_NOW = "energy_now"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL_NAME = "model_name"
ATTR_POWER_NOW = "power_now"
ATTR_STATUS = "status"
@@ -5,6 +5,7 @@ from typing import Any
from qingping_ble import QingpingBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothScanningMode,
BluetoothServiceInfoBleak,
@@ -96,6 +97,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
+10 -2
View File
@@ -113,11 +113,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
except RainbirdApiException as err:
raise ConfigEntryNotReady from err
# Rain Bird devices can only handle a single request at a time. This shared
# lock ensures that the background coordinators do not poll the device
# concurrently.
device_lock = asyncio.Lock()
data = RainbirdData(
controller,
model_info,
coordinator=RainbirdUpdateCoordinator(hass, entry, controller, model_info),
schedule_coordinator=RainbirdScheduleUpdateCoordinator(hass, entry, controller),
coordinator=RainbirdUpdateCoordinator(
hass, entry, controller, model_info, device_lock
),
schedule_coordinator=RainbirdScheduleUpdateCoordinator(
hass, entry, controller, device_lock
),
)
await data.coordinator.async_config_entry_first_refresh()
@@ -25,6 +25,7 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=1)
# The calendar data requires RPCs for each program/zone, and the data rarely
# changes, so we refresh it less often.
CALENDAR_UPDATE_INTERVAL = datetime.timedelta(minutes=15)
CALENDAR_TIMEOUT_SECONDS = 30
# The valves state are not immediately reflected after issuing a command. We add
# small delay to give additional time to reflect the new state.
@@ -64,6 +65,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
config_entry: RainbirdConfigEntry,
controller: AsyncRainbirdController,
model_info: ModelAndVersion,
device_lock: asyncio.Lock,
) -> None:
"""Initialize RainbirdUpdateCoordinator."""
super().__init__(
@@ -80,6 +82,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
self._unique_id = config_entry.unique_id
self._zones: set[int] | None = None
self._model_info = model_info
self._device_lock = device_lock
@property
def controller(self) -> AsyncRainbirdController:
@@ -112,8 +115,9 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
async def _async_update_data(self) -> RainbirdDeviceState:
"""Fetch data from Rain Bird device."""
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
return await self._fetch_data()
async with self._device_lock:
async with asyncio.timeout(TIMEOUT_SECONDS):
return await self._fetch_data()
except RainbirdDeviceBusyException as err:
raise UpdateFailed("Rain Bird device is busy") from err
except RainbirdApiException as err:
@@ -147,6 +151,7 @@ class RainbirdScheduleUpdateCoordinator(DataUpdateCoordinator[Schedule]):
hass: HomeAssistant,
config_entry: RainbirdConfigEntry,
controller: AsyncRainbirdController,
device_lock: asyncio.Lock,
) -> None:
"""Initialize ZoneStateUpdateCoordinator."""
super().__init__(
@@ -158,11 +163,13 @@ class RainbirdScheduleUpdateCoordinator(DataUpdateCoordinator[Schedule]):
update_interval=CALENDAR_UPDATE_INTERVAL,
)
self._controller = controller
self._device_lock = device_lock
async def _async_update_data(self) -> Schedule:
"""Fetch data from Rain Bird device."""
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
return await self._controller.get_schedule()
async with self._device_lock:
async with asyncio.timeout(CALENDAR_TIMEOUT_SECONDS):
return await self._controller.get_schedule()
except RainbirdApiException as err:
raise UpdateFailed(f"Error communicating with Device: {err}") from err
@@ -5,6 +5,7 @@ from typing import Any
from rapt_ble import RAPTPillBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -70,6 +71,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -5,6 +5,7 @@ from typing import Any
from ruuvitag_ble import RuuvitagBluetoothDeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -70,6 +71,7 @@ class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -5,6 +5,7 @@ from typing import Any
from sensirion_ble import SensirionBluetoothDeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -70,6 +71,7 @@ class SensirionConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -39,6 +39,7 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
BLOCK_EXPECTED_SLEEP_PERIOD,
BLOCK_WRONG_SLEEP_PERIOD,
CONF_BLE_SCANNER_MODE,
CONF_COAP_PORT,
CONF_SLEEP_PERIOD,
DOMAIN,
@@ -46,6 +47,7 @@ from .const import (
LOGGER,
MODELS_WITH_WRONG_SLEEP_PERIOD,
PUSH_UPDATE_ISSUE_ID,
BLEScannerMode,
)
from .coordinator import (
ShellyBlockCoordinator,
@@ -125,6 +127,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool:
"""Migrate old config entries."""
if entry.version > 1 or (entry.version == 1 and entry.minor_version > 3):
return False
if entry.minor_version < 3:
# One-time flip of explicit Active scanning to Auto so existing
# installs get the new battery-friendly default; Passive stays
# Passive because users picked it deliberately.
options = dict(entry.options)
if options.get(CONF_BLE_SCANNER_MODE) == BLEScannerMode.ACTIVE:
options[CONF_BLE_SCANNER_MODE] = BLEScannerMode.AUTO
hass.config_entries.async_update_entry(entry, options=options, minor_version=3)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bool:
"""Set up Shelly from a config entry."""
entry.runtime_data = ShellyEntryData([])
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
BLE_SCANNER_MODE_TO_BLUETOOTH_SCANNING_MODE = {
BLEScannerMode.PASSIVE: BluetoothScanningMode.PASSIVE,
BLEScannerMode.ACTIVE: BluetoothScanningMode.ACTIVE,
BLEScannerMode.AUTO: BluetoothScanningMode.AUTO,
}
@@ -31,13 +32,25 @@ async def async_connect_scanner(
"""Connect scanner."""
device = coordinator.device
entry = coordinator.config_entry
bluetooth_scanning_mode = BLE_SCANNER_MODE_TO_BLUETOOTH_SCANNING_MODE[scanner_mode]
# Options persist as plain strings, coerce so `is` checks work.
scanner_mode = BLEScannerMode(scanner_mode)
requested_mode = BLE_SCANNER_MODE_TO_BLUETOOTH_SCANNING_MODE[scanner_mode]
# AUTO runs the radio passive and lets habluetooth's auto-scheduler
# flip the BLE script to active on demand.
firmware_active = scanner_mode is BLEScannerMode.ACTIVE
current_mode = (
BluetoothScanningMode.ACTIVE
if firmware_active
else BluetoothScanningMode.PASSIVE
)
scanner = create_scanner(
coordinator.bluetooth_source,
entry.title,
requested_mode=bluetooth_scanning_mode,
current_mode=bluetooth_scanning_mode,
requested_mode=requested_mode,
current_mode=current_mode,
)
if scanner_mode is BLEScannerMode.AUTO:
scanner.set_active_window_provider(device)
unload_callbacks = [
async_register_scanner(
hass,
@@ -52,7 +65,7 @@ async def async_connect_scanner(
]
await async_start_scanner(
device=device,
active=scanner_mode == BLEScannerMode.ACTIVE,
active=firmware_active,
event_type=BLE_SCAN_RESULT_EVENT,
data_version=BLE_SCAN_RESULT_VERSION,
)
@@ -103,6 +103,7 @@ CONFIG_SCHEMA: Final = vol.Schema(
BLE_SCANNER_OPTIONS = [
BLEScannerMode.DISABLED,
BLEScannerMode.AUTO,
BLEScannerMode.ACTIVE,
BLEScannerMode.PASSIVE,
]
@@ -205,7 +206,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Shelly."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
host: str = ""
port: int = DEFAULT_HTTP_PORT
+1
View File
@@ -237,6 +237,7 @@ class BLEScannerMode(StrEnum):
DISABLED = "disabled"
ACTIVE = "active"
PASSIVE = "passive"
AUTO = "auto"
BLE_SCANNER_MIN_FIRMWARE = "1.5.1"
+3 -4
View File
@@ -53,10 +53,9 @@ def async_manage_ble_scanner_firmware_unsupported_issue(
if supports_scripts and device.model not in (MODEL_PLUG_S_G3, MODEL_OUT_PLUG_S_G3):
firmware = AwesomeVersion(device.shelly["ver"])
if (
firmware < BLE_SCANNER_MIN_FIRMWARE
and entry.options.get(CONF_BLE_SCANNER_MODE) == BLEScannerMode.ACTIVE
):
if firmware < BLE_SCANNER_MIN_FIRMWARE and entry.options.get(
CONF_BLE_SCANNER_MODE
) in (BLEScannerMode.ACTIVE, BLEScannerMode.AUTO):
ir.async_create_issue(
hass,
DOMAIN,
+5 -4
View File
@@ -685,7 +685,7 @@
},
"step": {
"confirm": {
"description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware} and acts as BLE scanner with active mode. This firmware version is not supported for BLE scanner active mode.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version.",
"description": "Your Shelly device {device_name} with IP address {ip_address} is running firmware {firmware} and acts as a BLE scanner in Active or Auto mode. This firmware version is not supported for these BLE scanner modes.\n\nSelect **Submit** button to start the OTA update to the latest stable firmware version.",
"title": "[%key:component::shelly::issues::ble_scanner_firmware_unsupported::title%]"
}
}
@@ -787,16 +787,17 @@
"data_description": {
"ble_scanner_mode": "The scanner mode to use for Bluetooth scanning."
},
"description": "Bluetooth scanning can be active or passive. With active, the Shelly requests data from nearby devices; with passive, the Shelly receives unsolicited data from nearby devices."
"description": "Auto is recommended for most setups; the Shelly listens passively and only briefly switches to active when needed, saving battery on your Bluetooth devices."
}
}
},
"selector": {
"ble_scanner_mode": {
"options": {
"active": "[%key:common::state::active%]",
"active": "Active (uses more device battery, fastest updates)",
"auto": "Auto (recommended, saves device battery)",
"disabled": "[%key:common::state::disabled%]",
"passive": "Passive"
"passive": "Passive (lowest device battery use, some details may be missing)"
}
},
"device": {
+7 -4
View File
@@ -40,14 +40,17 @@ async def validate_input(
data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
session = async_get_clientsession(hass, verify_ssl=user_input[CONF_VERIFY_SSL])
protocol = "https" if user_input[CONF_SSL] else "http"
host = data[CONF_HOST] if data is not None else user_input[CONF_HOST]
url = URL.build(scheme=protocol, host=host)
url = str(URL.build(scheme=protocol, host=host))
sma = SMAWebConnect(
session, str(url), user_input[CONF_PASSWORD], group=user_input[CONF_GROUP]
session=async_get_clientsession(hass, verify_ssl=user_input[CONF_VERIFY_SSL]),
url=url,
**{
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_GROUP: user_input[CONF_GROUP],
},
)
# new_session raises SmaAuthenticationException on failure
@@ -7,6 +7,7 @@ from typing import Any
from pysnooz.advertisement import SnoozAdvertisementData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothScanningMode,
BluetoothServiceInfo,
@@ -94,6 +95,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self._create_snooz_entry(discovered)
await bluetooth.async_request_active_scan(self.hass)
configured_addresses = self._async_current_ids(include_ignore=False)
for info in async_discovered_service_info(self.hass):
@@ -14,6 +14,7 @@ from switchbot import (
)
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -424,6 +425,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_password()
return await self._async_create_entry_from_discovery(user_input)
await bluetooth.async_request_active_scan(self.hass)
self._async_discover_devices()
if len(self._discovered_advs) == 1:
# If there is only one device we can ask for a password
@@ -5,6 +5,7 @@ from typing import Any
from thermopro_ble import ThermoProBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -70,6 +71,7 @@ class ThermoProConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -5,6 +5,7 @@ from typing import Any
from tilt_ble import TiltBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -70,6 +71,7 @@ class TiltConfigFlow(ConfigFlow, domain=DOMAIN):
title=self._discovered_devices[address], data={}
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -8,6 +8,7 @@ from togrill_bluetooth.client import Client
from togrill_bluetooth.packets import PacketA0Notify
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -113,6 +114,7 @@ class ToGrillBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_infos[address]
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, True):
address = discovery_info.address
+4
View File
@@ -48,3 +48,7 @@ VALLOX_CELL_STATE_TO_STR = {
2: "Bypass",
3: "Defrosting",
}
# The vallox_websocket_api client uses a hardcoded value of 65535 to
# represent an indefinite duration.
PROFILE_DURATION_INDEFINITE = 65535
+22 -10
View File
@@ -7,8 +7,9 @@ from vallox_websocket_api import Profile, ValloxApiException
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN, I18N_KEY_TO_VALLOX_PROFILE
from .const import DOMAIN, I18N_KEY_TO_VALLOX_PROFILE, PROFILE_DURATION_INDEFINITE
from .coordinator import ValloxConfigEntry, ValloxDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -39,7 +40,7 @@ SERVICE_SCHEMA_SET_PROFILE = vol.Schema(
{
vol.Required(ATTR_PROFILE): vol.In(I18N_KEY_TO_VALLOX_PROFILE),
vol.Optional(ATTR_DURATION): vol.All(
vol.Coerce(int), vol.Clamp(min=1, max=65535)
vol.Coerce(int), vol.Clamp(min=1, max=PROFILE_DURATION_INDEFINITE)
),
}
)
@@ -65,7 +66,14 @@ async def _async_set_profile_fan_speed(call: ServiceCall, profile: Profile) -> N
try:
await coordinator.client.set_fan_speed(profile, fan_speed)
except ValloxApiException as err:
_LOGGER.error("Error setting fan speed for %s profile: %s", profile.name, err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="failed_to_set_fan_speed_for_profile",
translation_placeholders={
"profile": profile.name.lower(),
"fan_speed": str(fan_speed),
},
) from err
else:
await coordinator.async_request_refresh()
@@ -96,14 +104,18 @@ async def _async_set_profile(call: ServiceCall) -> None:
await coordinator.client.set_profile(
I18N_KEY_TO_VALLOX_PROFILE[profile_key], duration
)
# pylint: disable-next=home-assistant-action-swallowed-exception
except ValloxApiException as err:
_LOGGER.error(
"Error setting profile %s for duration %s: %s",
profile_key,
duration,
err,
)
placeholders = {"profile": profile_key}
if duration is not None and duration != PROFILE_DURATION_INDEFINITE:
placeholders["duration"] = str(duration)
translation_key = "failed_to_set_profile_for_duration"
else:
translation_key = "failed_to_set_profile"
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=translation_key,
translation_placeholders=placeholders,
) from err
else:
await coordinator.async_request_refresh()
@@ -103,6 +103,17 @@
}
}
},
"exceptions": {
"failed_to_set_fan_speed_for_profile": {
"message": "Failed to set fan speed to {fan_speed}% for profile {profile}"
},
"failed_to_set_profile": {
"message": "Failed to set profile to {profile}"
},
"failed_to_set_profile_for_duration": {
"message": "Failed to set profile to {profile} for {duration} minutes"
}
},
"selector": {
"profile": {
"options": {
@@ -7,6 +7,7 @@ from typing import Any
from victron_ble_ha_parser import VictronBluetoothDeviceData
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -101,6 +102,7 @@ class VictronBLEConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders={"title": title},
)
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -0,0 +1,94 @@
"""The Vistapool integration."""
from dataclasses import dataclass, field
import logging
from aioaquarite import AquariteAuth, AquariteClient, AquariteError, AuthenticationError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import VistapoolDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
@dataclass
class VistapoolData:
"""Runtime data for a Vistapool account (holds one coordinator per pool)."""
auth: AquariteAuth
api: AquariteClient
coordinators: dict[str, VistapoolDataUpdateCoordinator] = field(
default_factory=dict
)
type VistapoolConfigEntry = ConfigEntry[VistapoolData]
async def async_setup_entry(hass: HomeAssistant, entry: VistapoolConfigEntry) -> bool:
"""Set up Vistapool from a config entry.
One config entry represents a Hayward account; the account can contain
multiple pools, each exposed as a separate device.
"""
user_config = entry.data
session = async_get_clientsession(hass)
auth = AquariteAuth(session, user_config[CONF_USERNAME], user_config[CONF_PASSWORD])
try:
await auth.authenticate()
except AuthenticationError as exc:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="invalid_credentials",
) from exc
except AquariteError as exc:
raise ConfigEntryNotReady from exc
api = AquariteClient(auth)
try:
pools = await api.get_pools()
except AquariteError as exc:
raise ConfigEntryNotReady from exc
if not pools:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="no_pools",
)
data = VistapoolData(auth=auth, api=api)
try:
for pool_id, pool_name in pools.items():
coordinator = VistapoolDataUpdateCoordinator(
hass, entry, auth, api, pool_id, pool_name
)
data.coordinators[pool_id] = coordinator
await coordinator.async_config_entry_first_refresh()
try:
await coordinator.subscribe()
except AquariteError as exc:
raise ConfigEntryNotReady from exc
entry.async_on_unload(coordinator.async_shutdown)
except Exception:
for coordinator in data.coordinators.values():
await coordinator.async_shutdown()
raise
entry.runtime_data = data
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: VistapoolConfigEntry) -> bool:
"""Unload Vistapool config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,76 @@
"""Config Flow for the Vistapool integration."""
import logging
from typing import Any
from aioaquarite import AquariteAuth, AquariteClient, AquariteError, AuthenticationError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
AUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
class VistapoolConfigFlow(ConfigFlow, domain=DOMAIN):
"""Vistapool config flow (one entry per Hayward account)."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
session = async_get_clientsession(self.hass)
try:
auth = AquariteAuth(session, username, password)
await auth.authenticate()
except AuthenticationError:
errors["base"] = "invalid_auth"
except AquariteError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected error during authentication")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(auth.user_id)
self._abort_if_unique_id_configured()
api = AquariteClient(auth)
try:
pools = await api.get_pools()
except AquariteError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected error fetching pools")
errors["base"] = "unknown"
else:
if not pools:
errors["base"] = "no_pools"
else:
return self.async_create_entry(
title=username,
data={
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
)
return self.async_show_form(
step_id="user", data_schema=AUTH_SCHEMA, errors=errors
)
@@ -0,0 +1,13 @@
"""Shared constants for the Vistapool integration."""
DOMAIN = "vistapool"
BRAND = "Sugar Valley"
MODEL = "Vistapool"
PATH_PREFIX = "main."
PATH_HASCD = f"{PATH_PREFIX}hasCD"
PATH_HASCL = f"{PATH_PREFIX}hasCL"
PATH_HASPH = f"{PATH_PREFIX}hasPH"
PATH_HASRX = f"{PATH_PREFIX}hasRX"
PATH_HASUV = f"{PATH_PREFIX}hasUV"
PATH_HASHIDRO = f"{PATH_PREFIX}hasHidro"
@@ -0,0 +1,83 @@
"""Data coordinator for the Vistapool integration."""
import logging
from typing import TYPE_CHECKING, Any
from aioaquarite import (
AquariteAuth,
AquariteClient,
AquariteError,
ResilientPoolSubscription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
if TYPE_CHECKING:
from . import VistapoolConfigEntry
_LOGGER = logging.getLogger(__name__)
class VistapoolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Vistapool coordinator for a single pool's Firestore subscription."""
config_entry: VistapoolConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: VistapoolConfigEntry,
auth: AquariteAuth,
api: AquariteClient,
pool_id: str,
pool_name: str,
) -> None:
"""Initialize the coordinator."""
self.auth = auth
self.api = api
self.pool_id: str = pool_id
self.pool_name: str = pool_name
self.subscription: ResilientPoolSubscription | None = None
super().__init__(
hass,
logger=_LOGGER,
name=f"Vistapool {pool_name}",
update_interval=None,
config_entry=entry,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch latest pool data (fallback for manual refresh)."""
try:
return await self.api.fetch_pool_data(self.pool_id)
except AquariteError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
) from err
async def subscribe(self) -> None:
"""Subscribe to Firestore real-time updates via the library."""
def _on_data(data: dict[str, Any]) -> None:
"""Callback from the Firestore thread; push data to the HA loop."""
self.hass.loop.call_soon_threadsafe(self.async_set_updated_data, data)
self.subscription = await self.api.subscribe_pool_resilient(
self.pool_id, _on_data
)
async def async_shutdown(self) -> None:
"""Cleanly close the resilient subscription."""
if self.subscription is not None:
await self.subscription.aclose()
self.subscription = None
await super().async_shutdown()
def get_value(self, path: str, default: Any = None) -> Any:
"""Get nested data using dot-notation path."""
return AquariteClient.get_value(self.data, path, default)
@@ -0,0 +1,39 @@
"""Shared base entity helpers for Vistapool."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import BRAND, DOMAIN, MODEL
from .coordinator import VistapoolDataUpdateCoordinator
class VistapoolEntity(CoordinatorEntity[VistapoolDataUpdateCoordinator]):
"""Base entity class for Vistapool platforms (one device per pool)."""
_attr_has_entity_name = True
def __init__(self, coordinator: VistapoolDataUpdateCoordinator) -> None:
"""Initialize the base entity."""
super().__init__(coordinator)
sw_version = coordinator.get_value("main.version")
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.pool_id)},
name=coordinator.pool_name,
manufacturer=BRAND,
model=MODEL,
sw_version=str(sw_version) if sw_version is not None else None,
)
@property
def pool_id(self) -> str:
"""Return the pool ID for the entity."""
return self.coordinator.pool_id
@property
def pool_name(self) -> str:
"""Return the friendly pool name for the entity."""
return self.coordinator.pool_name
def build_unique_id(self, suffix: str) -> str:
"""Return a consistent unique ID for the entity."""
return f"{self.coordinator.pool_id}-{suffix}"
@@ -0,0 +1,27 @@
{
"entity": {
"sensor": {
"chlorine": {
"default": "mdi:gauge"
},
"conductivity": {
"default": "mdi:gauge"
},
"electrolysis": {
"default": "mdi:flash"
},
"filtration_intel_time": {
"default": "mdi:timer-outline"
},
"hydrolysis": {
"default": "mdi:flash"
},
"redox_potential": {
"default": "mdi:gauge"
},
"uv": {
"default": "mdi:weather-sunny-alert"
}
}
}
}
@@ -0,0 +1,12 @@
{
"domain": "vistapool",
"name": "Vistapool",
"codeowners": ["@fdebrus"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vistapool",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["aioaquarite"],
"quality_scale": "bronze",
"requirements": ["aioaquarite==0.4.0"]
}
@@ -0,0 +1,67 @@
rules:
# Bronze
action-setup:
status: exempt
comment: No service actions in initial sensor-only platform
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: No service actions in initial sensor-only platform
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: No user actions (sensor-only platform)
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No options flow
docs-troubleshooting: todo
entity-category: done
entity-disabled-by-default: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery: todo
discovery-update-info: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-use-cases: todo
dynamic-devices: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: No known repair scenarios
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
@@ -0,0 +1,187 @@
"""Vistapool Sensor entities."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfElectricPotential,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import VistapoolConfigEntry
from .const import (
PATH_HASCD,
PATH_HASCL,
PATH_HASHIDRO,
PATH_HASPH,
PATH_HASRX,
PATH_HASUV,
)
from .coordinator import VistapoolDataUpdateCoordinator
from .entity import VistapoolEntity
PARALLEL_UPDATES = 1
def _convert_hundredths(value: Any) -> float:
return float(value) / 100
def _convert_tenths(value: Any) -> float:
return float(value) / 10
@dataclass(frozen=True, kw_only=True)
class VistapoolSensorEntityDescription(SensorEntityDescription):
"""Describes a Vistapool sensor entity."""
value_path: str
value_fn: Callable[[Any], float | int] = float
exists_path: str | None = None
SENSOR_DESCRIPTIONS: tuple[VistapoolSensorEntityDescription, ...] = (
VistapoolSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_path="main.temperature",
),
VistapoolSensorEntityDescription(
key="conductivity",
translation_key="conductivity",
state_class=SensorStateClass.MEASUREMENT,
value_path="modules.cd.current",
value_fn=_convert_hundredths,
exists_path=PATH_HASCD,
),
VistapoolSensorEntityDescription(
key="chlorine",
translation_key="chlorine",
state_class=SensorStateClass.MEASUREMENT,
value_path="modules.cl.current",
value_fn=_convert_hundredths,
exists_path=PATH_HASCL,
),
VistapoolSensorEntityDescription(
key="ph",
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
value_path="modules.ph.current",
value_fn=_convert_hundredths,
exists_path=PATH_HASPH,
),
VistapoolSensorEntityDescription(
key="redox_potential",
translation_key="redox_potential",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
state_class=SensorStateClass.MEASUREMENT,
value_path="modules.rx.current",
value_fn=int,
exists_path=PATH_HASRX,
),
VistapoolSensorEntityDescription(
key="uv",
translation_key="uv",
state_class=SensorStateClass.MEASUREMENT,
value_path="modules.uv.current",
value_fn=_convert_hundredths,
exists_path=PATH_HASUV,
),
VistapoolSensorEntityDescription(
key="filtration_intel_time",
translation_key="filtration_intel_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.MEASUREMENT,
value_path="filtration.intel.time",
value_fn=int,
),
VistapoolSensorEntityDescription(
key="rssi",
translation_key="rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_path="main.RSSI",
value_fn=int,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: VistapoolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Vistapool sensors for every pool on the account."""
entities: list[VistapoolSensorEntity] = []
for coordinator in entry.runtime_data.coordinators.values():
for description in SENSOR_DESCRIPTIONS:
if description.exists_path is not None and not coordinator.get_value(
description.exists_path
):
continue
entities.append(VistapoolSensorEntity(coordinator, description))
# Electrolysis/hydrolysis: dynamic key based on hardware type
if coordinator.get_value(PATH_HASHIDRO):
is_electrolysis = coordinator.get_value("hidro.is_electrolysis")
entities.append(
VistapoolSensorEntity(
coordinator,
VistapoolSensorEntityDescription(
key="electrolysis" if is_electrolysis else "hydrolysis",
translation_key=(
"electrolysis" if is_electrolysis else "hydrolysis"
),
native_unit_of_measurement="g/h",
state_class=SensorStateClass.MEASUREMENT,
value_path="hidro.current",
value_fn=_convert_tenths,
),
)
)
async_add_entities(entities)
class VistapoolSensorEntity(VistapoolEntity, SensorEntity):
"""Generic Vistapool sensor driven by an entity description."""
entity_description: VistapoolSensorEntityDescription
def __init__(
self,
coordinator: VistapoolDataUpdateCoordinator,
description: VistapoolSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = self.build_unique_id(description.key)
@property
def native_value(self) -> float | int | None:
"""Return the sensor value, transformed by the description's value_fn."""
value = self.coordinator.get_value(self.entity_description.value_path)
if value is None:
return None
return self.entity_description.value_fn(value)
@@ -0,0 +1,66 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_pools": "No pools were found on this account.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"password": "Password",
"username": "Vistapool Username"
},
"data_description": {
"password": "The password for your Vistapool account",
"username": "The email used for your Vistapool account"
},
"description": "Enter your Vistapool credentials. All pools on the account will be added automatically.",
"title": "Authentication"
}
}
},
"entity": {
"sensor": {
"chlorine": {
"name": "Chlorine"
},
"conductivity": {
"name": "Conductivity"
},
"electrolysis": {
"name": "Electrolysis"
},
"filtration_intel_time": {
"name": "Filtration intel time"
},
"hydrolysis": {
"name": "Hydrolysis"
},
"redox_potential": {
"name": "Redox potential"
},
"rssi": {
"name": "Wi-Fi signal strength"
},
"uv": {
"name": "UV"
}
}
},
"exceptions": {
"invalid_credentials": {
"message": "Invalid Vistapool credentials."
},
"no_pools": {
"message": "No pools were found on this account."
},
"update_failed": {
"message": "Error fetching data from Vistapool."
}
}
}
+4 -1
View File
@@ -42,12 +42,14 @@ class WLEDNumberEntityDescription(NumberEntityDescription):
"""Class describing WLED number entities."""
value_fn: Callable[[Segment], int | None]
segment_translation_key: str
NUMBERS = [
WLEDNumberEntityDescription(
key=ATTR_SPEED,
translation_key="speed",
segment_translation_key="segment_speed",
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
@@ -57,6 +59,7 @@ NUMBERS = [
WLEDNumberEntityDescription(
key=ATTR_INTENSITY,
translation_key="intensity",
segment_translation_key="segment_intensity",
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
@@ -84,7 +87,7 @@ class WLEDNumber(WLEDEntity, NumberEntity):
# Segment 0 uses a simpler name, which is more natural for when using
# a single segment / using WLED with one big LED strip.
if segment != 0:
self._attr_translation_key = f"segment_{description.translation_key}"
self._attr_translation_key = description.segment_translation_key
self._attr_translation_placeholders = {"segment": str(segment)}
self._attr_unique_id = (
@@ -14,7 +14,7 @@ from xiaomi_ble import (
)
from xiaomi_ble.parser import EncryptionScheme
from homeassistant.components import onboarding
from homeassistant.components import bluetooth, onboarding
from homeassistant.components.bluetooth import (
BluetoothScanningMode,
BluetoothServiceInfo,
@@ -304,6 +304,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN):
return self._async_get_or_create_entry()
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
@@ -2,10 +2,11 @@
from ha_xthings_cloud import XthingsCloudApiClient
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_TOKEN, PLATFORMS
from .const import PLATFORMS
from .coordinator import XthingsCloudConfigEntry, XthingsCloudCoordinator
@@ -10,17 +10,11 @@ from ha_xthings_cloud import (
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
from .const import (
CONF_EMAIL,
CONF_PASSWORD,
CONF_REFRESH_TOKEN,
CONF_TOKEN,
DOMAIN,
LOGGER,
)
from .const import CONF_REFRESH_TOKEN, DOMAIN, LOGGER
ERROR_CODE_MAP: dict[int, str] = {
20001: "token_invalid",
@@ -7,15 +7,7 @@ from homeassistant.const import Platform
DOMAIN = "xthings_cloud"
LOGGER = logging.getLogger(__package__)
# pylint: disable-next=home-assistant-duplicate-const
CONF_EMAIL = "email"
# pylint: disable-next=home-assistant-duplicate-const
CONF_PASSWORD = "password"
# pylint: disable-next=home-assistant-duplicate-const
CONF_TOKEN = "token"
CONF_REFRESH_TOKEN = "refresh_token"
# pylint: disable-next=home-assistant-duplicate-const
CONF_CLIENT_ID = "client_id"
CONF_INSTANCE_ID = "instance_id"
# Polling interval (seconds)
@@ -11,12 +11,13 @@ from ha_xthings_cloud import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_REFRESH_TOKEN, CONF_TOKEN, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
from .const import CONF_REFRESH_TOKEN, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
type XthingsCloudConfigEntry = ConfigEntry[XthingsCloudCoordinator]
+1
View File
@@ -808,6 +808,7 @@ FLOWS = {
"victron_gx",
"victron_remote_monitoring",
"vilfo",
"vistapool",
"vivotek",
"vizio",
"vlc_telnet",
@@ -7752,6 +7752,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"vistapool": {
"name": "Vistapool",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push"
},
"vivotek": {
"name": "VIVOTEK",
"integration_type": "device",
+6 -6
View File
@@ -1,6 +1,6 @@
# Automatically generated by gen_requirements_all.py, do not edit
aiodhcpwatcher==1.2.1
aiodhcpwatcher==1.2.6
aiodiscover==3.2.3
aiodns==4.0.4
aiogithubapi==26.0.0
@@ -20,22 +20,22 @@ audioop-lts==0.2.2
av==16.0.1
awesomeversion==25.8.0
bcrypt==5.0.0
bleak-retry-connector==4.6.0
bleak==2.1.1
bleak-retry-connector==4.6.1
bleak==3.0.2
bluetooth-adapters==2.1.0
bluetooth-auto-recovery==1.5.3
bluetooth-data-tools==1.29.18
cached-ipaddress==1.0.1
cached-ipaddress==1.1.1
certifi>=2021.5.30
ciso8601==2.3.3
cronsim==2.7
cryptography==48.0.0
dbus-fast==5.0.3
dbus-fast==5.0.9
file-read-backwards==2.0.0
fnv-hash-fast==2.0.3
go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==6.7.2
habluetooth==6.7.3
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==2.0.0
Generated
+10
View File
@@ -5850,6 +5850,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.vistapool.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.vivotek.*]
check_untyped_defs = true
disallow_incomplete_defs = true
+1 -1
View File
@@ -30,7 +30,7 @@ home-assistant-bluetooth==2.0.0
home-assistant-intents==2026.5.5
httpx==0.28.1
ifaddr==0.2.0
infrared-protocols==5.4.0
infrared-protocols==5.6.0
Jinja2==3.1.6
lru-dict==1.4.1
mutagen==1.47.0
+13 -10
View File
@@ -202,6 +202,9 @@ aioapcaccess==1.0.0
# homeassistant.components.aquacell
aioaquacell==1.0.0
# homeassistant.components.vistapool
aioaquarite==0.4.0
# homeassistant.components.aseko_pool_live
aioaseko==1.0.0
@@ -230,7 +233,7 @@ aiocentriconnect==0.2.3
aiocomelit==2.0.3
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
aiodhcpwatcher==1.2.6
# homeassistant.components.dhcp
aiodiscover==3.2.3
@@ -309,7 +312,7 @@ aiokef==0.2.16
aiokem==1.0.1
# homeassistant.components.lichess
aiolichess==1.2.0
aiolichess==1.3.0
# homeassistant.components.lifx
aiolifx-effects==0.3.2
@@ -651,10 +654,10 @@ bizkaibus==0.1.1
bleak-esphome==3.9.1
# homeassistant.components.bluetooth
bleak-retry-connector==4.6.0
bleak-retry-connector==4.6.1
# homeassistant.components.bluetooth
bleak==2.1.1
bleak==3.0.2
# homeassistant.components.blebox
blebox-uniapi==2.5.3
@@ -727,7 +730,7 @@ btsmarthub-devicelist==0.2.3
buienradar==1.0.6
# homeassistant.components.dhcp
cached-ipaddress==1.0.1
cached-ipaddress==1.1.1
# homeassistant.components.caldav
caldav==2.1.0
@@ -769,7 +772,7 @@ connect-box==0.3.1
construct==2.10.68
# homeassistant.components.cookidoo
cookidoo-api==0.14.0
cookidoo-api==0.17.2
# homeassistant.components.backup
# homeassistant.components.utility_meter
@@ -794,7 +797,7 @@ datadog==0.52.0
datapoint==0.12.1
# homeassistant.components.bluetooth
dbus-fast==5.0.3
dbus-fast==5.0.9
# homeassistant.components.debugpy
debugpy==1.8.17
@@ -1210,7 +1213,7 @@ ha-xthings-cloud==1.0.5
habiticalib==0.4.7
# homeassistant.components.bluetooth
habluetooth==6.7.2
habluetooth==6.7.3
# homeassistant.components.hanna
hanna-cloud==0.0.7
@@ -1356,10 +1359,10 @@ influxdb-client==1.50.0
influxdb==5.3.1
# homeassistant.components.infrared
infrared-protocols==5.4.0
infrared-protocols==5.6.0
# homeassistant.components.inkbird
inkbird-ble==1.2.2
inkbird-ble==1.2.3
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.6.2
+2 -1
View File
@@ -7,7 +7,8 @@ from unittest.mock import AsyncMock, Mock, patch
from altruistclient import AltruistDeviceModel, AltruistError
import pytest
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
from homeassistant.components.altruist.const import DOMAIN
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry, load_fixture
@@ -5,8 +5,9 @@ from unittest.mock import AsyncMock
import pytest
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
from homeassistant.components.altruist.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
+111 -1
View File
@@ -4,12 +4,26 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from pyatv import conf
from pyatv.const import PairingRequirement, Protocol
from pyatv.const import (
DeviceModel,
FeatureName,
FeatureState,
KeyboardFocusState,
PairingRequirement,
Protocol,
)
from pyatv.interface import FeatureInfo, Features, Playing, PushUpdater
from pyatv.support import http
import pytest
from homeassistant.components.apple_tv.const import DOMAIN
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.core import HomeAssistant
from .common import MockPairingHandler, airplay_service, create_conf, mrp_service
from tests.common import MockConfigEntry
@pytest.fixture(autouse=True, name="mock_scan")
def mock_scan_fixture() -> Generator[AsyncMock]:
@@ -191,6 +205,102 @@ def airplay_device_with_password(mock_scan: AsyncMock) -> AsyncMock:
return mock_scan
class _MockFeatures(Features):
"""Real Features implementation with configurable per-feature state."""
def __init__(self, default: FeatureState = FeatureState.Available) -> None:
"""Initialize with a default state applied to every feature."""
self._default = default
self._overrides: dict[FeatureName, FeatureState] = {}
def set_state(self, feature: FeatureName, state: FeatureState) -> None:
"""Override the reported state for a single feature."""
self._overrides[feature] = state
def get_feature(self, feature_name: FeatureName) -> FeatureInfo:
"""Return the (possibly overridden) state for a feature."""
return FeatureInfo(state=self._overrides.get(feature_name, self._default))
class _MockPushUpdater(PushUpdater):
"""Real PushUpdater with the I/O replaced by an in-memory delivery."""
def __init__(self, playing: Playing) -> None:
"""Store the play status that will be delivered on start()."""
super().__init__()
self._playing = playing
self._active = False
@property
def active(self) -> bool:
"""Return whether the updater is active."""
return self._active
def start(self, initial_delay: int = 0) -> None:
"""Synchronously deliver the canned play status to the listener."""
self._active = True
self.listener.playstatus_update(self, self._playing)
def stop(self) -> None:
"""Stop forwarding updates."""
self._active = False
@pytest.fixture
def mock_atv() -> AsyncMock:
"""Create a mock Apple TV interface."""
atv = AsyncMock()
atv.close = MagicMock()
atv.features = _MockFeatures()
atv.keyboard = AsyncMock()
atv.push_updater = _MockPushUpdater(Playing())
atv.stream = AsyncMock()
atv.keyboard.text_focus_state = KeyboardFocusState.Focused
atv.device_info.model = DeviceModel.Gen4K
atv.device_info.raw_model = "AppleTV6,2"
atv.device_info.version = "15.0"
atv.device_info.mac = "AA:BB:CC:DD:EE:FF"
return atv
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Create an Apple TV mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Living Room",
unique_id="mrpid",
data={
CONF_ADDRESS: "127.0.0.1",
CONF_NAME: "Living Room",
"credentials": {str(Protocol.MRP.value): "mrp_creds"},
"identifiers": ["mrpid"],
},
)
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_async_zeroconf: MagicMock,
mock_atv: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> MockConfigEntry:
"""Set up Apple TV integration with mocked pyatv."""
mock_config_entry.add_to_hass(hass)
scan_result = create_conf("127.0.0.1", "Living Room", mrp_service())
with (
patch("homeassistant.components.apple_tv.scan", return_value=[scan_result]),
patch("homeassistant.components.apple_tv.connect", return_value=mock_atv),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
@pytest.fixture
def dmap_with_requirement(
mock_scan: AsyncMock, pairing_requirement: PairingRequirement
@@ -0,0 +1,88 @@
"""Tests for the Apple TV media player."""
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
DOMAIN as MP_DOMAIN,
SERVICE_PLAY_MEDIA,
MediaType,
)
from homeassistant.components.media_source import PlayMedia
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
ENTITY_ID = "media_player.living_room_living_room"
pytestmark = pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
("play_item", "expected_stream_arg"),
[
(
PlayMedia(
url="/api/media_source_proxy/song.mp3",
mime_type="audio/mp3",
path=Path("/media/song.mp3"),
),
"/media/song.mp3",
),
(
PlayMedia(
url="https://example.com/song.mp3",
mime_type="audio/mp3",
),
"https://example.com/song.mp3",
),
],
)
async def test_play_media_from_media_source(
hass: HomeAssistant,
mock_atv: AsyncMock,
play_item: PlayMedia,
expected_stream_arg: str,
) -> None:
"""Stream resolved media via its local path when present, otherwise via the URL."""
with patch(
"homeassistant.components.apple_tv.media_player.media_source.async_resolve_media",
return_value=play_item,
):
await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
ATTR_MEDIA_CONTENT_ID: "media-source://local/song.mp3",
},
blocking=True,
)
mock_atv.stream.stream_file.assert_awaited_once_with(expected_stream_arg)
@pytest.mark.parametrize("media_type", [MediaType.APP, MediaType.URL])
async def test_play_media_launches_app(
hass: HomeAssistant,
mock_atv: AsyncMock,
media_type: MediaType,
) -> None:
"""App and URL media types launch the corresponding app on the device."""
await hass.services.async_call(
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: "com.netflix.Netflix",
},
blocking=True,
)
mock_atv.apps.launch_app.assert_awaited_once_with("com.netflix.Netflix")
mock_atv.stream.stream_file.assert_not_called()
+4 -52
View File
@@ -1,67 +1,19 @@
"""Tests for Apple TV keyboard services."""
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from unittest.mock import AsyncMock, PropertyMock, patch
from pyatv.const import DeviceModel, KeyboardFocusState, Protocol
from pyatv.const import KeyboardFocusState
from pyatv.exceptions import NotSupportedError, ProtocolError
import pytest
from homeassistant.components.apple_tv.const import ATTR_TEXT, DOMAIN
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_ADDRESS, CONF_NAME
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from .common import create_conf, mrp_service
from tests.common import MockConfigEntry
@pytest.fixture
def mock_atv() -> AsyncMock:
"""Create a mock Apple TV interface with keyboard support."""
atv = AsyncMock()
atv.close = MagicMock()
atv.features = MagicMock()
atv.keyboard = AsyncMock()
atv.push_updater = MagicMock()
atv.keyboard.text_focus_state = KeyboardFocusState.Focused
atv.device_info.model = DeviceModel.Gen4K
atv.device_info.raw_model = "AppleTV6,2"
atv.device_info.version = "15.0"
atv.device_info.mac = "AA:BB:CC:DD:EE:FF"
return atv
@pytest.fixture
async def mock_config_entry(
hass: HomeAssistant,
mock_async_zeroconf: MagicMock,
mock_atv: AsyncMock,
) -> MockConfigEntry:
"""Set up Apple TV integration with mocked pyatv."""
entry = MockConfigEntry(
domain=DOMAIN,
title="Living Room",
unique_id="mrpid",
data={
CONF_ADDRESS: "127.0.0.1",
CONF_NAME: "Living Room",
"credentials": {str(Protocol.MRP.value): "mrp_creds"},
"identifiers": ["mrpid"],
},
)
entry.add_to_hass(hass)
scan_result = create_conf("127.0.0.1", "Living Room", mrp_service())
with (
patch("homeassistant.components.apple_tv.scan", return_value=[scan_result]),
patch("homeassistant.components.apple_tv.connect", return_value=mock_atv),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
pytestmark = pytest.mark.usefixtures("init_integration")
async def test_set_keyboard_text(
+1 -1
View File
@@ -4,9 +4,9 @@ from homeassistant.components.aws_s3.const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
)
from homeassistant.const import CONF_PREFIX
# What gets persisted in the config entry (empty prefix is not stored)
CONFIG_ENTRY_DATA = {
+2 -6
View File
@@ -10,12 +10,8 @@ from botocore.exceptions import (
import pytest
from homeassistant import config_entries
from homeassistant.components.aws_s3.const import (
CONF_BUCKET,
CONF_ENDPOINT_URL,
CONF_PREFIX,
DOMAIN,
)
from homeassistant.components.aws_s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN
from homeassistant.const import CONF_PREFIX
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
+11 -7
View File
@@ -73,7 +73,7 @@ async def test_flow_manual_configuration(hass: HomeAssistant) -> None:
CONF_PASSWORD: "pass",
CONF_PORT: 80,
CONF_MODEL: "M1065-LW",
CONF_NAME: "M1065-LW 0",
CONF_NAME: f"M1065-LW - {MAC}",
}
@@ -189,10 +189,10 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(
CONF_PASSWORD: "pass",
CONF_PORT: 80,
CONF_MODEL: "M1065-LW",
CONF_NAME: "M1065-LW 2",
CONF_NAME: f"M1065-LW - {MAC}",
}
assert result["data"][CONF_NAME] == "M1065-LW 2"
assert result["data"][CONF_NAME] == f"M1065-LW - {MAC}"
async def test_reauth_flow_update_configuration(
@@ -266,7 +266,7 @@ async def test_reconfiguration_flow_update_configuration(
@pytest.mark.parametrize(
("source", "discovery_info"),
("source", "discovery_info", "expected_title"),
[
(
SOURCE_DHCP,
@@ -275,6 +275,7 @@ async def test_reconfiguration_flow_update_configuration(
ip=DEFAULT_HOST,
macaddress=DHCP_FORMATTED_MAC,
),
f"axis-{MAC}",
),
(
SOURCE_SSDP,
@@ -314,6 +315,7 @@ async def test_reconfiguration_flow_update_configuration(
"presentationURL": f"http://{DEFAULT_HOST}:80/",
},
),
f"AXIS M1065-LW - {MAC}",
),
(
SOURCE_ZEROCONF,
@@ -329,6 +331,7 @@ async def test_reconfiguration_flow_update_configuration(
"macaddress": MAC,
},
),
f"AXIS M1065-LW - {MAC}",
),
],
)
@@ -337,6 +340,7 @@ async def test_discovery_flow(
hass: HomeAssistant,
source: str,
discovery_info: BaseServiceInfo,
expected_title: str,
) -> None:
"""Test the different discovery flows for new devices work."""
result = await hass.config_entries.flow.async_init(
@@ -362,7 +366,7 @@ async def test_discovery_flow(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"M1065-LW - {MAC}"
assert result["title"] == expected_title
assert result["data"] == {
CONF_PROTOCOL: "http",
CONF_HOST: "1.2.3.4",
@@ -370,10 +374,10 @@ async def test_discovery_flow(
CONF_PASSWORD: "pass",
CONF_PORT: 80,
CONF_MODEL: "M1065-LW",
CONF_NAME: "M1065-LW 0",
CONF_NAME: expected_title,
}
assert result["data"][CONF_NAME] == "M1065-LW 0"
assert result["data"][CONF_NAME] == expected_title
@pytest.mark.parametrize(
+2 -1
View File
@@ -11,7 +11,8 @@ from homeassistant.components.cloudflare_r2.backup import (
MULTIPART_MIN_PART_SIZE_BYTES,
suggested_filenames,
)
from homeassistant.components.cloudflare_r2.const import CONF_PREFIX, DOMAIN
from homeassistant.components.cloudflare_r2.const import DOMAIN
from homeassistant.const import CONF_PREFIX
from .const import USER_INPUT

Some files were not shown because too many files have changed in this diff Show More