mirror of
https://github.com/home-assistant/core.git
synced 2026-05-25 10:15:22 +02:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c73ad0310 | |||
| 4a04a271ec | |||
| 52c27bdea5 | |||
| 6fdc52c002 | |||
| e560bbc103 | |||
| b8c573685f | |||
| 3764b70b90 | |||
| 5d2de6f82b | |||
| 64d17f44fa | |||
| 6f67d44cfe | |||
| def3befb0e | |||
| 05716ae196 | |||
| c0a864297f | |||
| 04bb84cd03 | |||
| cb55accc3b | |||
| d21c227804 | |||
| 1ebccd9fa2 | |||
| cfbd0f3217 | |||
| 4afb7c0997 | |||
| 105caccc51 | |||
| 6419551117 | |||
| 585bd6616a | |||
| b8dd97cf21 | |||
| 68fc4aed78 | |||
| 7dbb259625 | |||
| 057eac7fb6 | |||
| 31c9cdf742 | |||
| 3147104132 | |||
| d6d0f37b52 | |||
| 75e48745a8 | |||
| 533417778c | |||
| e49fd4ebbd | |||
| 8412b029b1 | |||
| c65de7521f | |||
| 752c17917e | |||
| f643c7ddc6 | |||
| 6f5d4cf991 | |||
| b52466fed1 | |||
| 189534e32b | |||
| 684ae23b18 | |||
| f4d2f65602 | |||
| 65879ff37b | |||
| d902104bee | |||
| 7bad27c412 | |||
| 74a7102cf6 | |||
| e88fb03388 |
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -237,6 +237,7 @@ class BLEScannerMode(StrEnum):
|
||||
DISABLED = "disabled"
|
||||
ACTIVE = "active"
|
||||
PASSIVE = "passive"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
BLE_SCANNER_MIN_FIRMWARE = "1.5.1"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
|
||||
Generated
+1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
Generated
+13
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user