mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 02:13:44 +02:00
Compare commits
259 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c0fcbb824 | |||
| d8716b6b72 | |||
| e88092dc52 | |||
| 6e4b5f31d4 | |||
| df6763328f | |||
| 51e409c1c7 | |||
| 8c9bf7e41c | |||
| ce94073321 | |||
| fa61e02207 | |||
| d1dab83f10 | |||
| 2b7d593ebe | |||
| e407b4730d | |||
| 0d19e85a0d | |||
| dac6271e01 | |||
| 8cae8edc55 | |||
| a3b0909e3f | |||
| ee30520b57 | |||
| 536e686892 | |||
| ef767c2b9f | |||
| c1ecc13cb3 | |||
| c5e3ba536c | |||
| 0e324c074a | |||
| a3ba7803db | |||
| 49bf5db5ff | |||
| 50981c26ad | |||
| 2adbf7c933 | |||
| 838ef0bb9f | |||
| 43c2658962 | |||
| bbefa971d8 | |||
| cb97f2f13c | |||
| a657b9bb84 | |||
| 2d2f55a4df | |||
| df16e6d022 | |||
| 56212c6fa5 | |||
| bc964ce7f0 | |||
| ed4f55406c | |||
| 03d5b18974 | |||
| 53c486ccd1 | |||
| 9a2a177b28 | |||
| 18e12740d9 | |||
| 5a24b670a2 | |||
| 94c5c8f42e | |||
| e84d5fba11 | |||
| b808c0c5eb | |||
| d4adb1f298 | |||
| fe0a822721 | |||
| 9f427893b1 | |||
| dea31e5744 | |||
| 51d6948848 | |||
| 7ce74cb5ec | |||
| 29ba140816 | |||
| 0ca4f3e1ba | |||
| 0430e6794e | |||
| 29fa7f827a | |||
| 57d1001603 | |||
| 96de4b3828 | |||
| c6cb2884f4 | |||
| 27e81fe0ed | |||
| 2c1db10986 | |||
| a7ba4bd086 | |||
| 25449b424f | |||
| f6f89bd807 | |||
| 370d7d6bdf | |||
| 4dbf3359c1 | |||
| 25eb7173bf | |||
| 648c3d500b | |||
| 33016c2977 | |||
| 5679b061d2 | |||
| 2eb2bdd615 | |||
| 184cbfea23 | |||
| f88bc008e5 | |||
| a927312fb5 | |||
| 5f13db2356 | |||
| 64e84e2aa0 | |||
| 901457e7aa | |||
| 89a9c2ec24 | |||
| 9e04457472 | |||
| 6ecdbb677f | |||
| 79de1d9ed4 | |||
| 7fefa5c235 | |||
| 94db78a0be | |||
| 83a1b06b56 | |||
| 1e42a38473 | |||
| c54ed53a81 | |||
| 611a952232 | |||
| 05e76105ad | |||
| ed56e5d631 | |||
| 9253fa4471 | |||
| 5f36062ef3 | |||
| e562b6f42b | |||
| b76a94bd42 | |||
| 4e11ff05de | |||
| 080e3d7a42 | |||
| 69e3348cd7 | |||
| 6caa4baa00 | |||
| 4729b19dc6 | |||
| 8abbc4abbc | |||
| 3a667bce8c | |||
| 4c86102daf | |||
| 15bf652f37 | |||
| eafed2b86c | |||
| 79901cede9 | |||
| 27dc82d7d0 | |||
| ae37c8cc7a | |||
| 5eadfcc524 | |||
| 5fd1e23255 | |||
| 72bcc6702f | |||
| 8889464e04 | |||
| af58b0c3b7 | |||
| e9e20229a3 | |||
| 80ff6dc618 | |||
| fa30100160 | |||
| e6c20333b3 | |||
| 3858400a6f | |||
| 95eefbac20 | |||
| e1e731eb48 | |||
| f7ce4ff25c | |||
| 617e87e02c | |||
| dafd54ba2b | |||
| e8c3539709 | |||
| e5263dc0c8 | |||
| 3584c710b9 | |||
| 0b56ef5699 | |||
| 90bd9bb626 | |||
| 7863927c3a | |||
| 9fcf757021 | |||
| fc0547ccdf | |||
| 22f8f117fb | |||
| 2052579efc | |||
| b8f2583bc3 | |||
| 6323a078e1 | |||
| ca0be3ec8a | |||
| 91157c21ef | |||
| cc4fae10f5 | |||
| d180ff417d | |||
| 8870b657d1 | |||
| 81735b7b47 | |||
| 7fd261347b | |||
| df796d432e | |||
| f6e36615d6 | |||
| 0278735dbf | |||
| 9c8d8fef16 | |||
| 6897b24c10 | |||
| a2a3f59e65 | |||
| 2626a74840 | |||
| 689260f581 | |||
| f1a2c8be4b | |||
| 0579d565dd | |||
| f141f5f908 | |||
| 0c25252d9f | |||
| 400b377aa8 | |||
| a5f3c434e0 | |||
| 365f8046ac | |||
| 4ac35d40cd | |||
| 7691991a93 | |||
| d0c45b1857 | |||
| 02750452df | |||
| 41a81cbf15 | |||
| ff621d5bf3 | |||
| 6d561a9796 | |||
| 4784199038 | |||
| df35c8e707 | |||
| 57eeaf1f75 | |||
| 3cadc1796f | |||
| ae06f734ce | |||
| 08a53362a7 | |||
| 274c928ec0 | |||
| d75dda0c05 | |||
| 0c40fcdaeb | |||
| 0a1ba8a4a3 | |||
| 018acc0a3c | |||
| 3a293c6bc4 | |||
| 9155d56190 | |||
| 461dc13da9 | |||
| b48e2127b8 | |||
| 11ab992dbb | |||
| 4be2cdf90a | |||
| cdd5cb2876 | |||
| cdc67aa891 | |||
| 6a22a2b867 | |||
| 0883b23d0c | |||
| 595459bfda | |||
| 7ab8ff56b3 | |||
| eda36512ec | |||
| 04aee812f8 | |||
| 6718cce203 | |||
| 49f0bb6990 | |||
| 38afcbb21f | |||
| 87ab2beddf | |||
| a05a34239d | |||
| f11aba9648 | |||
| c2ef119e50 | |||
| 8b6c99776e | |||
| 463bffaeb6 | |||
| 0cfd8032c0 | |||
| 144d5ff0cc | |||
| ab5c65b08c | |||
| 6b33bf3961 | |||
| 89eb395e2d | |||
| d671d48869 | |||
| ed582fae91 | |||
| 4d5c3ee0aa | |||
| 02046fcdb4 | |||
| fbe27749a0 | |||
| eddab96a69 | |||
| ed3376352d | |||
| dfbb763031 | |||
| 5cf13d9273 | |||
| e18ffc53f2 | |||
| 0eea3176d6 | |||
| 4f20977a8e | |||
| 5bd63bb56b | |||
| f7103da818 | |||
| bf4922a7ef | |||
| 6f7eac5c6d | |||
| d6e73a89f3 | |||
| 269aefd405 | |||
| a6865f1639 | |||
| f55aa0b86e | |||
| 02b34f05aa | |||
| 37f42707e5 | |||
| 17f3ba1434 | |||
| 31dcc25ba5 | |||
| 4da93f6a5e | |||
| 5ed7d32749 | |||
| ab5b9dbdc9 | |||
| 3b28bf07d1 | |||
| b626c9b450 | |||
| 5430eca93e | |||
| b41c477f44 | |||
| 5900413c08 | |||
| c2ceab741f | |||
| 45ff4940eb | |||
| 9c8a15cb64 | |||
| b09e54c961 | |||
| f44b7e202a | |||
| 0f535e979f | |||
| 4c2c01b4f6 | |||
| b1d48fe9a2 | |||
| b1dfc3cd23 | |||
| 696efe349e | |||
| 6a32722acc | |||
| 8eaec56c6b | |||
| 60d3c9342d | |||
| 2bd5039f28 | |||
| 8b1b14a704 | |||
| 5e674ce1d0 | |||
| 3656bcf752 | |||
| 39093fc2bc | |||
| efa5838be4 | |||
| 1c6ad2fa66 | |||
| af144e1b77 | |||
| b451bfed81 | |||
| 3e32c50936 | |||
| 208b15637a | |||
| c958cce769 | |||
| 602ec54579 | |||
| fa2bfc5d9d | |||
| 94f906b34c |
@@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
|
||||
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2024.11"
|
||||
HA_SHORT_VERSION: "2024.12"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -622,13 +622,13 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
@@ -823,7 +823,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
name: Split tests for full run Python ${{ matrix.python-version }}
|
||||
name: Split tests for full run
|
||||
steps:
|
||||
- name: Install additional OS dependencies
|
||||
run: |
|
||||
@@ -949,6 +949,7 @@ jobs:
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
--numprocesses auto \
|
||||
--snapshot-details \
|
||||
--dist=loadfile \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
@@ -1071,6 +1072,7 @@ jobs:
|
||||
-qq \
|
||||
--timeout=20 \
|
||||
--numprocesses 1 \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=10 \
|
||||
@@ -1199,6 +1201,7 @@ jobs:
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--numprocesses 1 \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
@@ -1345,6 +1348,7 @@ jobs:
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--numprocesses auto \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.1
|
||||
rev: v0.7.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -324,6 +324,7 @@ homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
homeassistant.components.mqtt.*
|
||||
homeassistant.components.music_assistant.*
|
||||
homeassistant.components.my.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.myuplink.*
|
||||
|
||||
+4
-2
@@ -496,8 +496,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
/tests/components/freedompro/ @stefano055415
|
||||
/homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||
/tests/components/fritzbox/ @mib1185 @flabbamann
|
||||
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
|
||||
@@ -954,6 +954,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
/homeassistant/components/mullvad/ @meichthys
|
||||
/tests/components/mullvad/ @meichthys
|
||||
/homeassistant/components/music_assistant/ @music-assistant
|
||||
/tests/components/music_assistant/ @music-assistant
|
||||
/homeassistant/components/mutesync/ @currentoor
|
||||
/tests/components/mutesync/ @currentoor
|
||||
/homeassistant/components/my/ @home-assistant/core
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from .backup_restore import restore_backup
|
||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
@@ -182,6 +183,9 @@ def main() -> int:
|
||||
return scripts.run(args.script)
|
||||
|
||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||
if restore_backup(config_dir):
|
||||
return RESTART_EXIT_CODE
|
||||
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Home Assistant module to handle restoring backups."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import securetar
|
||||
|
||||
from .const import __version__ as HA_VERSION
|
||||
|
||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||
KEEP_PATHS = ("backups",)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RestoreBackupFileContent:
|
||||
"""Definition for restore backup file content."""
|
||||
|
||||
backup_file_path: Path
|
||||
|
||||
|
||||
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||
"""Return the contents of the restore backup file."""
|
||||
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
||||
try:
|
||||
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
|
||||
return RestoreBackupFileContent(
|
||||
backup_file_path=Path(instruction_content["path"])
|
||||
)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _clear_configuration_directory(config_dir: Path) -> None:
|
||||
"""Delete all files and directories in the config directory except for the backups directory."""
|
||||
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
||||
config_contents = sorted(
|
||||
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
||||
)
|
||||
|
||||
for entry in config_contents:
|
||||
entrypath = config_dir.joinpath(entry)
|
||||
|
||||
if entrypath.is_file():
|
||||
entrypath.unlink()
|
||||
elif entrypath.is_dir():
|
||||
shutil.rmtree(entrypath)
|
||||
|
||||
|
||||
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
||||
"""Extract the backup file to the config directory."""
|
||||
with (
|
||||
TemporaryDirectory() as tempdir,
|
||||
securetar.SecureTarFile(
|
||||
backup_file_path,
|
||||
gzip=False,
|
||||
mode="r",
|
||||
) as ostf,
|
||||
):
|
||||
ostf.extractall(
|
||||
path=Path(tempdir, "extracted"),
|
||||
members=securetar.secure_path(ostf),
|
||||
filter="fully_trusted",
|
||||
)
|
||||
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
||||
|
||||
if (
|
||||
backup_meta_version := AwesomeVersion(
|
||||
backup_meta["homeassistant"]["version"]
|
||||
)
|
||||
) > HA_VERSION:
|
||||
raise ValueError(
|
||||
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
||||
)
|
||||
|
||||
with securetar.SecureTarFile(
|
||||
Path(
|
||||
tempdir,
|
||||
"extracted",
|
||||
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
||||
),
|
||||
gzip=backup_meta["compressed"],
|
||||
mode="r",
|
||||
) as istf:
|
||||
for member in istf.getmembers():
|
||||
if member.name == "data":
|
||||
continue
|
||||
member.name = member.name.replace("data/", "")
|
||||
_clear_configuration_directory(config_dir)
|
||||
istf.extractall(
|
||||
path=config_dir,
|
||||
members=[
|
||||
member
|
||||
for member in securetar.secure_path(istf)
|
||||
if member.name != "data"
|
||||
],
|
||||
filter="fully_trusted",
|
||||
)
|
||||
|
||||
|
||||
def restore_backup(config_dir_path: str) -> bool:
|
||||
"""Restore the backup file if any.
|
||||
|
||||
Returns True if a restore backup file was found and restored, False otherwise.
|
||||
"""
|
||||
config_dir = Path(config_dir_path)
|
||||
if not (restore_content := restore_backup_file_content(config_dir)):
|
||||
return False
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||
backup_file_path = restore_content.backup_file_path
|
||||
_LOGGER.info("Restoring %s", backup_file_path)
|
||||
try:
|
||||
_extract_backup(config_dir, backup_file_path)
|
||||
except FileNotFoundError as err:
|
||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||
_LOGGER.info("Restore complete, restarting")
|
||||
return True
|
||||
@@ -1,6 +1,5 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
@@ -13,20 +12,10 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetData:
|
||||
"""Aemet runtime data."""
|
||||
|
||||
name: str
|
||||
coordinator: WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
@@ -46,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
||||
except AemetError as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, entry, aemet)
|
||||
await weather_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Final, cast
|
||||
@@ -19,6 +20,7 @@ from aemet_opendata.helpers import dict_nested_value
|
||||
from aemet_opendata.interface import AEMET
|
||||
|
||||
from homeassistant.components.weather import Forecast
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -29,6 +31,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
API_TIMEOUT: Final[int] = 120
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetData:
|
||||
"""Aemet runtime data."""
|
||||
|
||||
name: str
|
||||
coordinator: WeatherUpdateCoordinator
|
||||
|
||||
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
@@ -36,6 +48,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AemetConfigEntry,
|
||||
aemet: AEMET,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
@@ -44,6 +57,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=WEATHER_UPDATE_INTERVAL,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .coordinator import AemetConfigEntry
|
||||
|
||||
TO_REDACT_CONFIG = [
|
||||
CONF_API_KEY,
|
||||
|
||||
@@ -55,7 +55,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_CONDITION,
|
||||
@@ -87,7 +86,7 @@ from .const import (
|
||||
ATTR_API_WIND_SPEED,
|
||||
CONDITIONS_MAP,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import CONDITIONS_MAP
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["agent"],
|
||||
"requirements": ["agent-py==0.0.23"]
|
||||
"requirements": ["agent-py==0.0.24"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Config flow for AirNow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -12,7 +14,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
) -> AirNowOptionsFlowHandler:
|
||||
"""Return the options flow."""
|
||||
return AirNowOptionsFlowHandler(config_entry)
|
||||
return AirNowOptionsFlowHandler()
|
||||
|
||||
|
||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class AirNowOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle an options flow for AirNow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
options_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_RADIUS): vol.All(
|
||||
int,
|
||||
vol.Range(min=5),
|
||||
),
|
||||
}
|
||||
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -46,9 +45,11 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return HomeassistantAnalyticsOptionsFlowHandler(config_entry)
|
||||
return HomeassistantAnalyticsOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -132,7 +133,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Homeassistant Analytics options."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -211,6 +212,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
),
|
||||
},
|
||||
),
|
||||
self.options,
|
||||
self.config_entry.options,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
@@ -186,16 +186,14 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle an option flow for Android Debug Bridge."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__(config_entry)
|
||||
|
||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
||||
self._state_det_rules: dict[str, Any] = self.options.setdefault(
|
||||
CONF_STATE_DETECTION_RULES, {}
|
||||
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||
self._state_det_rules: dict[str, Any] = dict(
|
||||
config_entry.options.get(CONF_STATE_DETECTION_RULES, {})
|
||||
)
|
||||
self._conf_app_id: str | None = None
|
||||
self._conf_rule_id: str | None = None
|
||||
@@ -237,7 +235,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
|
||||
]
|
||||
rules = [RULES_NEW_ID, *self._state_det_rules]
|
||||
options = self.options
|
||||
options = self.config_entry.options
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
@@ -221,13 +221,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
||||
"""Android TV Remote options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__(config_entry)
|
||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
||||
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||
self._conf_app_id: str | None = None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -121,7 +121,6 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.last_rendered_recommended = config_entry.options.get(
|
||||
CONF_RECOMMENDED, False
|
||||
)
|
||||
|
||||
@@ -22,8 +22,8 @@ class EnhancedAudioChunk:
|
||||
timestamp_ms: int
|
||||
"""Timestamp relative to start of audio stream (milliseconds)"""
|
||||
|
||||
is_speech: bool | None
|
||||
"""True if audio chunk likely contains speech, False if not, None if unknown"""
|
||||
speech_probability: float | None
|
||||
"""Probability that audio chunk contains speech (0-1), None if unknown"""
|
||||
|
||||
|
||||
class AudioEnhancer(ABC):
|
||||
@@ -70,27 +70,27 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
|
||||
)
|
||||
|
||||
self.vad: MicroVad | None = None
|
||||
self.threshold = 0.5
|
||||
|
||||
if self.is_vad_enabled:
|
||||
self.vad = MicroVad()
|
||||
_LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
|
||||
_LOGGER.debug("Initialized microVAD")
|
||||
|
||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
is_speech: bool | None = None
|
||||
speech_probability: float | None = None
|
||||
|
||||
assert len(audio) == BYTES_PER_CHUNK
|
||||
|
||||
if self.vad is not None:
|
||||
# Run VAD
|
||||
speech_prob = self.vad.Process10ms(audio)
|
||||
is_speech = speech_prob > self.threshold
|
||||
speech_probability = self.vad.Process10ms(audio)
|
||||
|
||||
if self.audio_processor is not None:
|
||||
# Run noise suppression and auto gain
|
||||
audio = self.audio_processor.Process10ms(audio).audio
|
||||
|
||||
return EnhancedAudioChunk(
|
||||
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
||||
audio=audio,
|
||||
timestamp_ms=timestamp_ms,
|
||||
speech_probability=speech_probability,
|
||||
)
|
||||
|
||||
@@ -780,7 +780,9 @@ class PipelineRun:
|
||||
# speaking the voice command.
|
||||
audio_chunks_for_stt.extend(
|
||||
EnhancedAudioChunk(
|
||||
audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
|
||||
audio=chunk_ts[0],
|
||||
timestamp_ms=chunk_ts[1],
|
||||
speech_probability=None,
|
||||
)
|
||||
for chunk_ts in result.queued_audio
|
||||
)
|
||||
@@ -827,7 +829,7 @@ class PipelineRun:
|
||||
|
||||
if wake_word_vad is not None:
|
||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||
if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
|
||||
if not wake_word_vad.process(chunk_seconds, chunk.speech_probability):
|
||||
raise WakeWordTimeoutError(
|
||||
code="wake-word-timeout", message="Wake word was not detected"
|
||||
)
|
||||
@@ -955,7 +957,7 @@ class PipelineRun:
|
||||
|
||||
if stt_vad is not None:
|
||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||
if not stt_vad.process(chunk_seconds, chunk.is_speech):
|
||||
if not stt_vad.process(chunk_seconds, chunk.speech_probability):
|
||||
# Silence detected at the end of voice command
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
@@ -1221,7 +1223,7 @@ class PipelineRun:
|
||||
yield EnhancedAudioChunk(
|
||||
audio=sub_chunk,
|
||||
timestamp_ms=timestamp_ms,
|
||||
is_speech=None, # no VAD
|
||||
speech_probability=None, # no VAD
|
||||
)
|
||||
timestamp_ms += MS_PER_CHUNK
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class AudioBuffer:
|
||||
class VoiceCommandSegmenter:
|
||||
"""Segments an audio stream into voice commands."""
|
||||
|
||||
speech_seconds: float = 0.3
|
||||
speech_seconds: float = 0.1
|
||||
"""Seconds of speech before voice command has started."""
|
||||
|
||||
command_seconds: float = 1.0
|
||||
@@ -96,6 +96,12 @@ class VoiceCommandSegmenter:
|
||||
timed_out: bool = False
|
||||
"""True a timeout occurred during voice command."""
|
||||
|
||||
before_command_speech_threshold: float = 0.2
|
||||
"""Probability threshold for speech before voice command."""
|
||||
|
||||
in_command_speech_threshold: float = 0.5
|
||||
"""Probability threshold for speech during voice command."""
|
||||
|
||||
_speech_seconds_left: float = 0.0
|
||||
"""Seconds left before considering voice command as started."""
|
||||
|
||||
@@ -124,7 +130,7 @@ class VoiceCommandSegmenter:
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self.in_command = False
|
||||
|
||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
||||
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||
"""Process samples using external VAD.
|
||||
|
||||
Returns False when command is done.
|
||||
@@ -142,7 +148,12 @@ class VoiceCommandSegmenter:
|
||||
self.timed_out = True
|
||||
return False
|
||||
|
||||
if speech_probability is None:
|
||||
speech_probability = 0.0
|
||||
|
||||
if not self.in_command:
|
||||
# Before command
|
||||
is_speech = speech_probability > self.before_command_speech_threshold
|
||||
if is_speech:
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self._speech_seconds_left -= chunk_seconds
|
||||
@@ -160,24 +171,29 @@ class VoiceCommandSegmenter:
|
||||
if self._reset_seconds_left <= 0:
|
||||
self._speech_seconds_left = self.speech_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
elif not is_speech:
|
||||
# Silence in command
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self._silence_seconds_left -= chunk_seconds
|
||||
self._command_seconds_left -= chunk_seconds
|
||||
if (self._silence_seconds_left <= 0) and (self._command_seconds_left <= 0):
|
||||
# Command finished successfully
|
||||
self.reset()
|
||||
_LOGGER.debug("Voice command finished")
|
||||
return False
|
||||
else:
|
||||
# Speech in command.
|
||||
# Reset silence counter if enough speech.
|
||||
self._reset_seconds_left -= chunk_seconds
|
||||
self._command_seconds_left -= chunk_seconds
|
||||
if self._reset_seconds_left <= 0:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
# In command
|
||||
is_speech = speech_probability > self.in_command_speech_threshold
|
||||
if not is_speech:
|
||||
# Silence in command
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self._silence_seconds_left -= chunk_seconds
|
||||
self._command_seconds_left -= chunk_seconds
|
||||
if (self._silence_seconds_left <= 0) and (
|
||||
self._command_seconds_left <= 0
|
||||
):
|
||||
# Command finished successfully
|
||||
self.reset()
|
||||
_LOGGER.debug("Voice command finished")
|
||||
return False
|
||||
else:
|
||||
# Speech in command.
|
||||
# Reset silence counter if enough speech.
|
||||
self._reset_seconds_left -= chunk_seconds
|
||||
self._command_seconds_left -= chunk_seconds
|
||||
if self._reset_seconds_left <= 0:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
|
||||
return True
|
||||
|
||||
@@ -226,6 +242,9 @@ class VoiceActivityTimeout:
|
||||
reset_seconds: float = 0.5
|
||||
"""Seconds of speech before resetting timeout."""
|
||||
|
||||
speech_threshold: float = 0.5
|
||||
"""Threshold for speech."""
|
||||
|
||||
_silence_seconds_left: float = 0.0
|
||||
"""Seconds left before considering voice command as stopped."""
|
||||
|
||||
@@ -241,12 +260,15 @@ class VoiceActivityTimeout:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
|
||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
||||
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||
"""Process samples using external VAD.
|
||||
|
||||
Returns False when timeout is reached.
|
||||
"""
|
||||
if is_speech:
|
||||
if speech_probability is None:
|
||||
speech_probability = 0.0
|
||||
|
||||
if speech_probability > self.speech_threshold:
|
||||
# Speech
|
||||
self._reset_seconds_left -= chunk_seconds
|
||||
if self._reset_seconds_left <= 0:
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -59,9 +59,11 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> AxisOptionsFlowHandler:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> AxisOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return AxisOptionsFlowHandler(config_entry)
|
||||
return AxisOptionsFlowHandler()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Axis config flow."""
|
||||
@@ -264,7 +266,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class AxisOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Axis device options."""
|
||||
|
||||
config_entry: AxisConfigEntry
|
||||
@@ -282,8 +284,7 @@ class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Axis device stream options."""
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
return self.async_create_entry(data=self.config_entry.options | user_input)
|
||||
|
||||
schema = {}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ LOGGER = getLogger(__package__)
|
||||
EXCLUDE_FROM_BACKUP = [
|
||||
"__pycache__/*",
|
||||
".DS_Store",
|
||||
".HA_RESTORE",
|
||||
"*.db-shm",
|
||||
"*.log.*",
|
||||
"*.log",
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import Any, Protocol, cast
|
||||
|
||||
from securetar import SecureTarFile, atomic_contents_add
|
||||
|
||||
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -123,6 +124,10 @@ class BaseBackupManager(abc.ABC):
|
||||
LOGGER.debug("Loaded %s platforms", len(self.platforms))
|
||||
self.loaded_platforms = True
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||
"""Restpre a backup."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
||||
"""Generate a backup."""
|
||||
@@ -291,6 +296,25 @@ class BackupManager(BaseBackupManager):
|
||||
|
||||
return tar_file_path.stat().st_size
|
||||
|
||||
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||
"""Restore a backup.
|
||||
|
||||
This will write the restore information to .HA_RESTORE which
|
||||
will be handled during startup by the restore_backup module.
|
||||
"""
|
||||
if (backup := await self.async_get_backup(slug=slug)) is None:
|
||||
raise HomeAssistantError(f"Backup {slug} not found")
|
||||
|
||||
def _write_restore_file() -> None:
|
||||
"""Write the restore file."""
|
||||
Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text(
|
||||
json.dumps({"path": backup.path.as_posix()}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
await self.hass.async_add_executor_job(_write_restore_file)
|
||||
await self.hass.services.async_call("homeassistant", "restart", {})
|
||||
|
||||
|
||||
def _generate_slug(date: str, name: str) -> str:
|
||||
"""Generate a backup slug."""
|
||||
|
||||
@@ -22,6 +22,7 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
|
||||
websocket_api.async_register_command(hass, handle_info)
|
||||
websocket_api.async_register_command(hass, handle_create)
|
||||
websocket_api.async_register_command(hass, handle_remove)
|
||||
websocket_api.async_register_command(hass, handle_restore)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@@ -85,6 +86,24 @@ async def handle_remove(
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/restore",
|
||||
vol.Required("slug"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def handle_restore(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Restore a backup."""
|
||||
await hass.data[DATA_MANAGER].async_restore_backup(msg["slug"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/generate"})
|
||||
@websocket_api.async_response
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -153,10 +153,10 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> BMWOptionsFlow:
|
||||
"""Return a MyBMW option flow."""
|
||||
return BMWOptionsFlow(config_entry)
|
||||
return BMWOptionsFlow()
|
||||
|
||||
|
||||
class BMWOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
class BMWOptionsFlow(OptionsFlow):
|
||||
"""Handle a option flow for MyBMW."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"list_access": {
|
||||
"default": "mdi:account-lock",
|
||||
"state": {
|
||||
"shared": "mdi:account-group"
|
||||
"shared": "mdi:account-group",
|
||||
"invitation": "mdi:account-multiple-plus"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
|
||||
translation_key=BringSensor.LIST_ACCESS,
|
||||
value_fn=lambda lst, _: lst["status"].lower(),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=["registered", "shared"],
|
||||
options=["registered", "shared", "invitation"],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
"name": "List access",
|
||||
"state": {
|
||||
"registered": "Private",
|
||||
"shared": "Shared"
|
||||
"shared": "Shared",
|
||||
"invitation": "Invitation pending"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==0.6.4"]
|
||||
"requirements": ["python-bsblan==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ async def async_setup_platform(
|
||||
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
coordinator = CalDavUpdateCoordinator(
|
||||
hass,
|
||||
None,
|
||||
calendar=calendar,
|
||||
days=days,
|
||||
include_all_day=True,
|
||||
@@ -126,6 +127,7 @@ async def async_setup_platform(
|
||||
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
coordinator = CalDavUpdateCoordinator(
|
||||
hass,
|
||||
None,
|
||||
calendar=calendar,
|
||||
days=days,
|
||||
include_all_day=False,
|
||||
@@ -152,6 +154,7 @@ async def async_setup_entry(
|
||||
async_generate_entity_id(ENTITY_ID_FORMAT, calendar.name, hass=hass),
|
||||
CalDavUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
calendar=calendar,
|
||||
days=CONFIG_ENTRY_DEFAULT_DAYS,
|
||||
include_all_day=True,
|
||||
@@ -204,7 +207,8 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
||||
if self._supports_offset:
|
||||
self._attr_extra_state_attributes = {
|
||||
"offset_reached": is_offset_reached(
|
||||
self._event.start_datetime_local, self.coordinator.offset
|
||||
self._event.start_datetime_local,
|
||||
self.coordinator.offset, # type: ignore[arg-type]
|
||||
)
|
||||
if self._event
|
||||
else False
|
||||
|
||||
@@ -6,6 +6,9 @@ from datetime import date, datetime, time, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import caldav
|
||||
|
||||
from homeassistant.components.calendar import CalendarEvent, extract_offset
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -14,6 +17,9 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .api import get_attr_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CalDavConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
@@ -23,11 +29,20 @@ OFFSET = "!!"
|
||||
class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(self, hass, calendar, days, include_all_day, search):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: CalDavConfigEntry | None,
|
||||
calendar: caldav.Calendar,
|
||||
days: int,
|
||||
include_all_day: bool,
|
||||
search: str | None,
|
||||
) -> None:
|
||||
"""Set up how we are going to search the WebDav calendar."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"CalDAV {calendar.name}",
|
||||
update_interval=MIN_TIME_BETWEEN_UPDATES,
|
||||
)
|
||||
@@ -35,7 +50,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
self.days = days
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.offset = None
|
||||
self.offset: timedelta | None = None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
@@ -109,7 +124,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
_start_of_tomorrow = start_of_tomorrow
|
||||
if _start_of_today <= start_dt < _start_of_tomorrow:
|
||||
new_event = event.copy()
|
||||
new_vevent = new_event.instance.vevent
|
||||
new_vevent = new_event.instance.vevent # type: ignore[attr-defined]
|
||||
if hasattr(new_vevent, "dtend"):
|
||||
dur = new_vevent.dtend.value - new_vevent.dtstart.value
|
||||
new_vevent.dtend.value = start_dt + dur
|
||||
|
||||
@@ -421,8 +421,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if hass.config.webrtc.ice_servers:
|
||||
return hass.config.webrtc.ice_servers
|
||||
return [
|
||||
RTCIceServer(urls="stun:stun.home-assistant.io:80"),
|
||||
RTCIceServer(urls="stun:stun.home-assistant.io:3478"),
|
||||
RTCIceServer(
|
||||
urls=[
|
||||
"stun:stun.home-assistant.io:80",
|
||||
"stun:stun.home-assistant.io:3478",
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
async_register_ice_servers(hass, get_ice_servers)
|
||||
|
||||
@@ -52,7 +52,7 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return CanaryOptionsFlowHandler(config_entry)
|
||||
return CanaryOptionsFlowHandler()
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by configuration file."""
|
||||
@@ -104,10 +104,6 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class CanaryOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Canary client options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -41,7 +41,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> CastOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return CastOptionsFlowHandler(config_entry)
|
||||
return CastOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -109,9 +109,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class CastOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Google Cast options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Google Cast options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.updated_config: dict[str, Any] = {}
|
||||
|
||||
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hass_nabucasa"],
|
||||
"requirements": ["hass-nabucasa==0.83.0"],
|
||||
"requirements": ["hass-nabucasa==0.84.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -158,16 +158,12 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for Coinbase."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@Petro31"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"requirements": ["numpy==1.26.4"]
|
||||
"requirements": ["numpy==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -154,16 +154,12 @@ class Control4ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for Control4."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.4"]
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"]
|
||||
}
|
||||
|
||||
@@ -213,18 +213,19 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Crownstone options."""
|
||||
super().__init__(OPTIONS_FLOW, self.async_create_new_entry)
|
||||
self.entry = config_entry
|
||||
self.updated_options = config_entry.options.copy()
|
||||
self.options = config_entry.options.copy()
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage Crownstone options."""
|
||||
self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][self.entry.entry_id].cloud
|
||||
self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][
|
||||
self.config_entry.entry_id
|
||||
].cloud
|
||||
|
||||
spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data}
|
||||
usb_path = self.entry.options.get(CONF_USB_PATH)
|
||||
usb_sphere = self.entry.options.get(CONF_USB_SPHERE)
|
||||
usb_path = self.config_entry.options.get(CONF_USB_PATH)
|
||||
usb_sphere = self.config_entry.options.get(CONF_USB_SPHERE)
|
||||
|
||||
options_schema = vol.Schema(
|
||||
{vol.Optional(CONF_USE_USB_OPTION, default=usb_path is not None): bool}
|
||||
@@ -243,14 +244,14 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
|
||||
if user_input[CONF_USE_USB_OPTION] and usb_path is None:
|
||||
return await self.async_step_usb_config()
|
||||
if not user_input[CONF_USE_USB_OPTION] and usb_path is not None:
|
||||
self.updated_options[CONF_USB_PATH] = None
|
||||
self.updated_options[CONF_USB_SPHERE] = None
|
||||
self.options[CONF_USB_PATH] = None
|
||||
self.options[CONF_USB_SPHERE] = None
|
||||
elif (
|
||||
CONF_USB_SPHERE_OPTION in user_input
|
||||
and spheres[user_input[CONF_USB_SPHERE_OPTION]] != usb_sphere
|
||||
):
|
||||
sphere_id = spheres[user_input[CONF_USB_SPHERE_OPTION]]
|
||||
self.updated_options[CONF_USB_SPHERE] = sphere_id
|
||||
self.options[CONF_USB_SPHERE] = sphere_id
|
||||
|
||||
return self.async_create_new_entry()
|
||||
|
||||
@@ -260,7 +261,7 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
|
||||
"""Create a new entry."""
|
||||
# these attributes will only change when a usb was configured
|
||||
if self.usb_path is not None and self.usb_sphere_id is not None:
|
||||
self.updated_options[CONF_USB_PATH] = self.usb_path
|
||||
self.updated_options[CONF_USB_SPHERE] = self.usb_sphere_id
|
||||
self.options[CONF_USB_PATH] = self.usb_path
|
||||
self.options[CONF_USB_SPHERE] = self.usb_sphere_id
|
||||
|
||||
return super().async_create_entry(title="", data=self.updated_options)
|
||||
return super().async_create_entry(title="", data=self.options)
|
||||
|
||||
@@ -74,9 +74,11 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> DeconzOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return DeconzOptionsFlowHandler(config_entry)
|
||||
return DeconzOptionsFlowHandler()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the deCONZ config flow."""
|
||||
@@ -299,11 +301,6 @@ class DeconzOptionsFlowHandler(OptionsFlow):
|
||||
|
||||
gateway: DeconzHub
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize deCONZ options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -315,8 +312,7 @@ class DeconzOptionsFlowHandler(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the deconz devices options."""
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
return self.async_create_entry(data=self.config_entry.options | user_input)
|
||||
|
||||
schema_options = {}
|
||||
for option, default in (
|
||||
|
||||
@@ -47,7 +47,6 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -52,10 +52,6 @@ CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Options for the component."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Init object."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -119,7 +115,7 @@ class DenonAvrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -69,16 +69,12 @@ class DexcomConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> DexcomOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return DexcomOptionsFlowHandler(config_entry)
|
||||
return DexcomOptionsFlowHandler()
|
||||
|
||||
|
||||
class DexcomOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for Dexcom."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -74,7 +74,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Define the config flow to handle options."""
|
||||
return DlnaDmrOptionsFlowHandler(config_entry)
|
||||
return DlnaDmrOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(self, user_input: FlowInput = None) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user.
|
||||
@@ -327,10 +327,6 @@ class DlnaDmrOptionsFlowHandler(OptionsFlow):
|
||||
Configures the single instance and updates the existing config entry.
|
||||
"""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
@@ -101,7 +101,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> DnsIPOptionsFlowHandler:
|
||||
"""Return Option handler."""
|
||||
return DnsIPOptionsFlowHandler(config_entry)
|
||||
return DnsIPOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -165,7 +165,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class DnsIPOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option config flow for dnsip integration."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -213,16 +213,12 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for doorbird."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -171,9 +171,11 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> DSMROptionFlowHandler:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> DSMROptionFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return DSMROptionFlowHandler(config_entry)
|
||||
return DSMROptionFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -311,10 +313,6 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class DSMROptionFlowHandler(OptionsFlow):
|
||||
"""Handle options."""
|
||||
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.entry = entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -328,7 +326,7 @@ class DSMROptionFlowHandler(OptionsFlow):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TIME_BETWEEN_UPDATE,
|
||||
default=self.entry.options.get(
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
|
||||
@@ -31,25 +31,26 @@ async def async_setup_entry(
|
||||
"""Set up the ecobee thermostat switch entity."""
|
||||
data: EcobeeData = hass.data[DOMAIN]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
EcobeeVentilator20MinSwitch(
|
||||
data,
|
||||
index,
|
||||
(await dt_util.async_get_time_zone(thermostat["location"]["timeZone"]))
|
||||
or dt_util.get_default_time_zone(),
|
||||
)
|
||||
entities: list[SwitchEntity] = [
|
||||
EcobeeVentilator20MinSwitch(
|
||||
data,
|
||||
index,
|
||||
(await dt_util.async_get_time_zone(thermostat["location"]["timeZone"]))
|
||||
or dt_util.get_default_time_zone(),
|
||||
)
|
||||
for index, thermostat in enumerate(data.ecobee.thermostats)
|
||||
if thermostat["settings"]["ventilatorType"] != "none"
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
(
|
||||
EcobeeSwitchAuxHeatOnly(data, index)
|
||||
for index, thermostat in enumerate(data.ecobee.thermostats)
|
||||
if thermostat["settings"]["ventilatorType"] != "none"
|
||||
],
|
||||
update_before_add=True,
|
||||
if thermostat["settings"]["hasHeatPump"]
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
EcobeeSwitchAuxHeatOnly(data, index)
|
||||
for index, thermostat in enumerate(data.ecobee.thermostats)
|
||||
if thermostat["settings"]["hasHeatPump"]
|
||||
)
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -103,13 +102,12 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return ElevenLabsOptionsFlow(config_entry)
|
||||
|
||||
|
||||
class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
class ElevenLabsOptionsFlow(OptionsFlow):
|
||||
"""ElevenLabs options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__(config_entry)
|
||||
self.api_key: str = self.config_entry.data[CONF_API_KEY]
|
||||
self.api_key: str = config_entry.data[CONF_API_KEY]
|
||||
# id -> name
|
||||
self.voices: dict[str, str] = {}
|
||||
self.models: dict[str, str] = {}
|
||||
@@ -170,7 +168,7 @@ class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
vol.Required(CONF_CONFIGURE_VOICE, default=False): bool,
|
||||
}
|
||||
),
|
||||
self.options,
|
||||
self.config_entry.options,
|
||||
)
|
||||
|
||||
async def async_step_voice_settings(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Configflow for the emoncms integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyemoncms import EmoncmsClient
|
||||
@@ -9,7 +11,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -68,7 +70,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowWithConfigEntry:
|
||||
) -> EmoncmsOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return EmoncmsOptionsFlow(config_entry)
|
||||
|
||||
@@ -77,6 +79,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Initiate a flow via the UI."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
@@ -89,7 +92,8 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass, user_input[CONF_URL], user_input[CONF_API_KEY]
|
||||
)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = result[CONF_MESSAGE]
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
else:
|
||||
self.include_only_feeds = user_input.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
self.url = user_input[CONF_URL]
|
||||
@@ -113,6 +117,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_choose_feeds(
|
||||
@@ -167,32 +172,36 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return result
|
||||
|
||||
|
||||
class EmoncmsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
class EmoncmsOptionsFlow(OptionsFlow):
|
||||
"""Emoncms Options flow handler."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize emoncms options flow."""
|
||||
self._url = config_entry.data[CONF_URL]
|
||||
self._api_key = config_entry.data[CONF_API_KEY]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
data = self.options if self.options else self._config_entry.data
|
||||
url = data[CONF_URL]
|
||||
api_key = data[CONF_API_KEY]
|
||||
include_only_feeds = data.get(CONF_ONLY_INCLUDE_FEEDID, [])
|
||||
description_placeholders = {}
|
||||
include_only_feeds = self.config_entry.options.get(
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
self.config_entry.data.get(CONF_ONLY_INCLUDE_FEEDID, []),
|
||||
)
|
||||
options: list = include_only_feeds
|
||||
result = await get_feed_list(self.hass, url, api_key)
|
||||
result = await get_feed_list(self.hass, self._url, self._api_key)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = result[CONF_MESSAGE]
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
else:
|
||||
options = get_options(result[CONF_MESSAGE])
|
||||
dropdown = {"options": options, "mode": "dropdown", "multiple": True}
|
||||
if user_input:
|
||||
include_only_feeds = user_input[CONF_ONLY_INCLUDE_FEEDID]
|
||||
return self.async_create_entry(
|
||||
title=sensor_name(url),
|
||||
data={
|
||||
CONF_URL: url,
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_ONLY_INCLUDE_FEEDID: include_only_feeds,
|
||||
},
|
||||
)
|
||||
@@ -207,4 +216,5 @@ class EmoncmsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
@@ -138,10 +138,11 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the emoncms sensors."""
|
||||
config = entry.options if entry.options else entry.data
|
||||
name = sensor_name(config[CONF_URL])
|
||||
exclude_feeds = config.get(CONF_EXCLUDE_FEEDID)
|
||||
include_only_feeds = config.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
name = sensor_name(entry.data[CONF_URL])
|
||||
exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID)
|
||||
include_only_feeds = entry.options.get(
|
||||
CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
)
|
||||
|
||||
if exclude_feeds is None and include_only_feeds is None:
|
||||
return
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"api_error": "An error occured in the pyemoncms API : {details}"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -19,6 +22,9 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"api_error": "[%key:component::emoncms::config::error::api_error%]"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.2"]
|
||||
"requirements": ["sense-energy==0.13.3"]
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ class EnergyManager:
|
||||
"device_consumption",
|
||||
):
|
||||
if key in update:
|
||||
data[key] = update[key] # type: ignore[literal-required]
|
||||
data[key] = update[key]
|
||||
|
||||
self.data = data
|
||||
self._store.async_delay_save(lambda: data, 60)
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -66,9 +66,11 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> EnvoyOptionsFlowHandler:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> EnvoyOptionsFlowHandler:
|
||||
"""Options flow handler for Enphase_Envoy."""
|
||||
return EnvoyOptionsFlowHandler(config_entry)
|
||||
return EnvoyOptionsFlowHandler()
|
||||
|
||||
@callback
|
||||
def _async_generate_schema(self) -> vol.Schema:
|
||||
@@ -288,7 +290,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class EnvoyOptionsFlowHandler(OptionsFlow):
|
||||
"""Envoy config flow options handler."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -257,6 +257,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, discovery_info: MqttServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle MQTT discovery."""
|
||||
if not discovery_info.payload:
|
||||
return self.async_abort(reason="mqtt_missing_payload")
|
||||
|
||||
device_info = json_loads_object(discovery_info.payload)
|
||||
if "mac" not in device_info:
|
||||
return self.async_abort(reason="mqtt_missing_mac")
|
||||
@@ -482,16 +485,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for esphome."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -31,6 +31,7 @@ class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevi
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name="ESPHome Dashboard",
|
||||
update_interval=timedelta(minutes=5),
|
||||
always_update=False,
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"service_received": "Action received",
|
||||
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
|
||||
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties."
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties.",
|
||||
"mqtt_missing_payload": "Missing MQTT Payload."
|
||||
},
|
||||
"error": {
|
||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
||||
|
||||
@@ -240,6 +240,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name=f"{DOMAIN}_coordinator",
|
||||
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
|
||||
update_method=broker.async_update,
|
||||
|
||||
@@ -150,7 +150,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> EzvizOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return EzvizOptionsFlowHandler(config_entry)
|
||||
return EzvizOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -391,10 +391,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class EzvizOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle EZVIZ client options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -46,9 +45,11 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return FeedReaderOptionsFlowHandler(config_entry)
|
||||
return FeedReaderOptionsFlowHandler()
|
||||
|
||||
def show_user_form(
|
||||
self,
|
||||
@@ -147,7 +148,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="reconfigure_successful")
|
||||
|
||||
|
||||
class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class FeedReaderOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle an options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -162,7 +163,9 @@ class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_MAX_ENTRIES,
|
||||
default=self.options.get(CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES),
|
||||
default=self.config_entry.options.get(
|
||||
CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES
|
||||
),
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Config flow for file integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from typing import Any
|
||||
@@ -11,7 +13,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
@@ -74,9 +75,11 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FileOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return FileOptionsFlowHandler(config_entry)
|
||||
return FileOptionsFlowHandler()
|
||||
|
||||
async def validate_file_path(self, file_path: str) -> bool:
|
||||
"""Ensure the file path is valid."""
|
||||
@@ -151,7 +154,7 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(title=title, data=data, options=options)
|
||||
|
||||
|
||||
class FileOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class FileOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle File options."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -71,9 +71,11 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FluxLedOptionsFlow:
|
||||
"""Get the options flow for the Flux LED component."""
|
||||
return FluxLedOptionsFlow(config_entry)
|
||||
return FluxLedOptionsFlow()
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: dhcp.DhcpServiceInfo
|
||||
@@ -320,10 +322,6 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class FluxLedOptionsFlow(OptionsFlow):
|
||||
"""Handle flux_led options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the flux_led options flow."""
|
||||
self._config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -332,7 +330,7 @@ class FluxLedOptionsFlow(OptionsFlow):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = self._config_entry.options
|
||||
options = self.config_entry.options
|
||||
options_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
|
||||
@@ -41,7 +41,7 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> ForecastSolarOptionFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return ForecastSolarOptionFlowHandler(config_entry)
|
||||
return ForecastSolarOptionFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -91,10 +91,6 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class ForecastSolarOptionFlowHandler(OptionsFlow):
|
||||
"""Handle options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -52,10 +52,6 @@ TEST_CONNECTION_ERROR_DICT = {
|
||||
class ForkedDaapdOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a forked-daapd options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -122,7 +118,7 @@ class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> ForkedDaapdOptionsFlowHandler:
|
||||
"""Return options flow handler."""
|
||||
return ForkedDaapdOptionsFlowHandler(config_entry)
|
||||
return ForkedDaapdOptionsFlowHandler()
|
||||
|
||||
async def validate_input(self, user_input):
|
||||
"""Validate the user input."""
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -60,9 +59,11 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FritzBoxToolsOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return FritzBoxToolsOptionsFlowHandler(config_entry)
|
||||
return FritzBoxToolsOptionsFlowHandler()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize FRITZ!Box Tools flow."""
|
||||
@@ -393,7 +394,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle an options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -404,19 +405,18 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options = self.config_entry.options
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CONSIDER_HOME,
|
||||
default=self.options.get(
|
||||
default=options.get(
|
||||
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
|
||||
vol.Optional(
|
||||
CONF_OLD_DISCOVERY,
|
||||
default=self.options.get(
|
||||
CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY
|
||||
),
|
||||
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "fritz",
|
||||
"name": "AVM FRITZ!Box Tools",
|
||||
"codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"],
|
||||
"codeowners": ["@AaronDavidSchneider", "@chemelli74", "@mib1185"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fritz",
|
||||
|
||||
@@ -141,7 +141,7 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> FritzBoxCallMonitorOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return FritzBoxCallMonitorOptionsFlowHandler(config_entry)
|
||||
return FritzBoxCallMonitorOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -278,10 +278,6 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a fritzbox_callmonitor options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
@classmethod
|
||||
def _are_prefixes_valid(cls, prefixes: str | None) -> bool:
|
||||
"""Check if prefixes are valid."""
|
||||
|
||||
@@ -324,7 +324,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> GenericOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return GenericOptionsFlowHandler(config_entry)
|
||||
return GenericOptionsFlowHandler()
|
||||
|
||||
def check_for_existing(self, options: dict[str, Any]) -> bool:
|
||||
"""Check whether an existing entry is using the same URLs."""
|
||||
@@ -409,9 +409,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class GenericOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Generic IP Camera options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Generic IP Camera options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.preview_cam: dict[str, Any] = {}
|
||||
self.user_input: dict[str, Any] = {}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["ha-av==10.1.1", "Pillow==10.4.0"]
|
||||
"requirements": ["av==13.1.0", "Pillow==10.4.0"]
|
||||
}
|
||||
|
||||
@@ -211,16 +211,12 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for GitHub."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
|
||||
@@ -238,16 +238,12 @@ class OAuth2FlowHandler(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Create an options flow."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Google Calendar options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -84,16 +84,12 @@ class OAuth2FlowHandler(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Create the options flow."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Google Assistant SDK options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -135,10 +135,10 @@ class GoogleCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> GoogleCloudOptionsFlowHandler:
|
||||
"""Create the options flow."""
|
||||
return GoogleCloudOptionsFlowHandler(config_entry)
|
||||
return GoogleCloudOptionsFlowHandler()
|
||||
|
||||
|
||||
class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class GoogleCloudOptionsFlowHandler(OptionsFlow):
|
||||
"""Google Cloud options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -169,7 +169,7 @@ class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
)
|
||||
),
|
||||
**tts_options_schema(
|
||||
self.options, voices, from_config_flow=True
|
||||
self.config_entry.options, voices, from_config_flow=True
|
||||
).schema,
|
||||
vol.Optional(
|
||||
CONF_STT_MODEL,
|
||||
@@ -182,6 +182,6 @@ class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
),
|
||||
}
|
||||
),
|
||||
self.options,
|
||||
self.config_entry.options,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_tts_voices(
|
||||
|
||||
|
||||
def tts_options_schema(
|
||||
config_options: dict[str, Any],
|
||||
config_options: Mapping[str, Any],
|
||||
voices: dict[str, list[str]],
|
||||
from_config_flow: bool = False,
|
||||
) -> vol.Schema:
|
||||
|
||||
@@ -163,7 +163,6 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.last_rendered_recommended = config_entry.options.get(
|
||||
CONF_RECOMMENDED, False
|
||||
)
|
||||
|
||||
@@ -148,10 +148,6 @@ def default_options(hass: HomeAssistant) -> dict[str, str]:
|
||||
class GoogleOptionsFlow(OptionsFlow):
|
||||
"""Handle an options flow for Google Travel Time."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize google options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
@@ -213,7 +209,7 @@ class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> GoogleOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return GoogleOptionsFlow(config_entry)
|
||||
return GoogleOptionsFlow()
|
||||
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
@@ -30,6 +30,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CALENDAR,
|
||||
Platform.SENSOR,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Binary sensor platform for Habitica integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ASSETS_URL
|
||||
from .entity import HabiticaBase
|
||||
from .types import HabiticaConfigEntry
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HabiticaBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Habitica Binary Sensor Description."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], bool | None]
|
||||
entity_picture: Callable[[dict[str, Any]], str | None]
|
||||
|
||||
|
||||
class HabiticaBinarySensor(StrEnum):
|
||||
"""Habitica Entities."""
|
||||
|
||||
PENDING_QUEST = "pending_quest"
|
||||
|
||||
|
||||
def get_scroll_image_for_pending_quest_invitation(user: dict[str, Any]) -> str | None:
|
||||
"""Entity picture for pending quest invitation."""
|
||||
if user["party"]["quest"].get("key") and user["party"]["quest"]["RSVPNeeded"]:
|
||||
return f"inventory_quest_scroll_{user["party"]["quest"]["key"]}.png"
|
||||
return None
|
||||
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS: tuple[HabiticaBinarySensorEntityDescription, ...] = (
|
||||
HabiticaBinarySensorEntityDescription(
|
||||
key=HabiticaBinarySensor.PENDING_QUEST,
|
||||
translation_key=HabiticaBinarySensor.PENDING_QUEST,
|
||||
value_fn=lambda user: user["party"]["quest"]["RSVPNeeded"],
|
||||
entity_picture=get_scroll_image_for_pending_quest_invitation,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HabiticaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the habitica binary sensors."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HabiticaBinarySensorEntity(coordinator, description)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class HabiticaBinarySensorEntity(HabiticaBase, BinarySensorEntity):
|
||||
"""Representation of a Habitica binary sensor."""
|
||||
|
||||
entity_description: HabiticaBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""If the binary sensor is on."""
|
||||
return self.entity_description.value_fn(self.coordinator.data.user)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the entity picture to use in the frontend, if any."""
|
||||
if entity_picture := self.entity_description.entity_picture(
|
||||
self.coordinator.data.user
|
||||
):
|
||||
return f"{ASSETS_URL}{entity_picture}"
|
||||
return None
|
||||
@@ -135,6 +135,14 @@
|
||||
"on": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"pending_quest": {
|
||||
"default": "mdi:script-outline",
|
||||
"state": {
|
||||
"on": "mdi:script-text-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
from aiohttp import ClientResponseError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_NAME, CONF_NAME
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
@@ -54,6 +55,21 @@ SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
if not (entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Habitica integration."""
|
||||
|
||||
@@ -86,14 +102,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def cast_skill(call: ServiceCall) -> ServiceResponse:
|
||||
"""Skill action."""
|
||||
entry: HabiticaConfigEntry | None
|
||||
if not (
|
||||
entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY])
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_found",
|
||||
)
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
coordinator = entry.runtime_data
|
||||
skill = {
|
||||
"pickpocket": {"spellId": "pickPocket", "cost": "10 MP"},
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"pending_quest": {
|
||||
"name": "Pending quest invitation"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"run_cron": {
|
||||
"name": "Start my day"
|
||||
@@ -219,7 +224,10 @@
|
||||
"message": "Unable to cast skill, your character does not have the skill or spell {skill}."
|
||||
},
|
||||
"entry_not_found": {
|
||||
"message": "The selected character is currently not configured or loaded in Home Assistant."
|
||||
"message": "The selected character is not configured in Home Assistant."
|
||||
},
|
||||
"entry_not_loaded": {
|
||||
"message": "The selected character is currently not loaded or disabled in Home Assistant."
|
||||
},
|
||||
"task_not_found": {
|
||||
"message": "Unable to cast skill, could not find the task {task}"
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID
|
||||
from .data import HarmonyConfigEntry
|
||||
from .util import (
|
||||
find_best_name_for_remote,
|
||||
find_unique_id_for_remote,
|
||||
@@ -156,7 +155,7 @@ class HarmonyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _async_create_entry_from_valid_input(
|
||||
self, validated: dict[str, Any], user_input: dict[str, Any]
|
||||
@@ -186,10 +185,6 @@ def _options_from_user_input(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for Harmony."""
|
||||
|
||||
def __init__(self, config_entry: HarmonyConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -137,17 +137,3 @@ class SupervisorEntityModel(StrEnum):
|
||||
CORE = "Home Assistant Core"
|
||||
SUPERVIOSR = "Home Assistant Supervisor"
|
||||
HOST = "Home Assistant Host"
|
||||
|
||||
|
||||
class SupervisorIssueContext(StrEnum):
|
||||
"""Context for supervisor issues."""
|
||||
|
||||
ADDON = "addon"
|
||||
CORE = "core"
|
||||
DNS_SERVER = "dns_server"
|
||||
MOUNT = "mount"
|
||||
OS = "os"
|
||||
PLUGIN = "plugin"
|
||||
SUPERVISOR = "supervisor"
|
||||
STORE = "store"
|
||||
SYSTEM = "system"
|
||||
|
||||
@@ -91,15 +91,6 @@ async def async_create_backup(
|
||||
return await hassio.send_command(command, payload=payload, timeout=None)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@_api_bool
|
||||
async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict:
|
||||
"""Apply a suggestion from supervisor's resolution center."""
|
||||
hassio: HassIO = hass.data[DOMAIN]
|
||||
command = f"/resolution/suggestion/{suggestion_uuid}"
|
||||
return await hassio.send_command(command, timeout=None)
|
||||
|
||||
|
||||
@api_data
|
||||
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
|
||||
"""Return settings specific to Home Assistant Green."""
|
||||
@@ -245,26 +236,6 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command("/ingress/panels", method="get")
|
||||
|
||||
@api_data
|
||||
def get_resolution_info(self) -> Coroutine:
|
||||
"""Return data for Supervisor resolution center.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command("/resolution/info", method="get")
|
||||
|
||||
@api_data
|
||||
def get_suggestions_for_issue(
|
||||
self, issue_id: str
|
||||
) -> Coroutine[Any, Any, dict[str, Any]]:
|
||||
"""Return suggestions for issue from Supervisor resolution center.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(
|
||||
f"/resolution/issue/{issue_id}/suggestions", method="get"
|
||||
)
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(
|
||||
self, http_config: dict[str, Any], refresh_token: RefreshToken
|
||||
@@ -304,14 +275,6 @@ class HassIO:
|
||||
"/supervisor/options", payload={"diagnostics": diagnostics}
|
||||
)
|
||||
|
||||
@_api_bool
|
||||
def apply_suggestion(self, suggestion_uuid: str) -> Coroutine:
|
||||
"""Apply a suggestion from supervisor's resolution center.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(f"/resolution/suggestion/{suggestion_uuid}")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str,
|
||||
|
||||
@@ -7,6 +7,10 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
from uuid import UUID
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import ContextType, Issue as SupervisorIssue
|
||||
|
||||
from homeassistant.core import HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -20,12 +24,8 @@ from homeassistant.helpers.issue_registry import (
|
||||
from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_ISSUES,
|
||||
ATTR_SUGGESTIONS,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_UNHEALTHY,
|
||||
ATTR_UNHEALTHY_REASONS,
|
||||
ATTR_UNSUPPORTED,
|
||||
ATTR_UNSUPPORTED_REASONS,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
@@ -45,10 +45,9 @@ from .const import (
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .coordinator import get_addons_info
|
||||
from .handler import HassIO, HassioAPIError
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
@@ -120,9 +119,9 @@ class SuggestionDataType(TypedDict):
|
||||
class Suggestion:
|
||||
"""Suggestion from Supervisor which resolves an issue."""
|
||||
|
||||
uuid: str
|
||||
uuid: UUID
|
||||
type: str
|
||||
context: SupervisorIssueContext
|
||||
context: ContextType
|
||||
reference: str | None = None
|
||||
|
||||
@property
|
||||
@@ -134,9 +133,9 @@ class Suggestion:
|
||||
def from_dict(cls, data: SuggestionDataType) -> Suggestion:
|
||||
"""Convert from dictionary representation."""
|
||||
return cls(
|
||||
uuid=data["uuid"],
|
||||
uuid=UUID(data["uuid"]),
|
||||
type=data["type"],
|
||||
context=SupervisorIssueContext(data["context"]),
|
||||
context=ContextType(data["context"]),
|
||||
reference=data["reference"],
|
||||
)
|
||||
|
||||
@@ -155,9 +154,9 @@ class IssueDataType(TypedDict):
|
||||
class Issue:
|
||||
"""Issue from Supervisor."""
|
||||
|
||||
uuid: str
|
||||
uuid: UUID
|
||||
type: str
|
||||
context: SupervisorIssueContext
|
||||
context: ContextType
|
||||
reference: str | None = None
|
||||
suggestions: list[Suggestion] = field(default_factory=list, compare=False)
|
||||
|
||||
@@ -171,9 +170,9 @@ class Issue:
|
||||
"""Convert from dictionary representation."""
|
||||
suggestions: list[SuggestionDataType] = data.get("suggestions", [])
|
||||
return cls(
|
||||
uuid=data["uuid"],
|
||||
uuid=UUID(data["uuid"]),
|
||||
type=data["type"],
|
||||
context=SupervisorIssueContext(data["context"]),
|
||||
context=ContextType(data["context"]),
|
||||
reference=data["reference"],
|
||||
suggestions=[
|
||||
Suggestion.from_dict(suggestion) for suggestion in suggestions
|
||||
@@ -190,7 +189,8 @@ class SupervisorIssues:
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[str, Issue] = {}
|
||||
self._issues: dict[UUID, Issue] = {}
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
@@ -283,7 +283,7 @@ class SupervisorIssues:
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
issue.uuid,
|
||||
issue.uuid.hex,
|
||||
is_fixable=bool(issue.suggestions),
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue.key,
|
||||
@@ -292,19 +292,37 @@ class SupervisorIssues:
|
||||
|
||||
self._issues[issue.uuid] = issue
|
||||
|
||||
async def add_issue_from_data(self, data: IssueDataType) -> None:
|
||||
async def add_issue_from_data(self, data: SupervisorIssue) -> None:
|
||||
"""Add issue from data to list after getting latest suggestions."""
|
||||
try:
|
||||
data["suggestions"] = (
|
||||
await self._client.get_suggestions_for_issue(data["uuid"])
|
||||
)[ATTR_SUGGESTIONS]
|
||||
except HassioAPIError:
|
||||
suggestions = (
|
||||
await self._supervisor_client.resolution.suggestions_for_issue(
|
||||
data.uuid
|
||||
)
|
||||
)
|
||||
except SupervisorError:
|
||||
_LOGGER.error(
|
||||
"Could not get suggestions for supervisor issue %s, skipping it",
|
||||
data["uuid"],
|
||||
data.uuid.hex,
|
||||
)
|
||||
return
|
||||
self.add_issue(Issue.from_dict(data))
|
||||
self.add_issue(
|
||||
Issue(
|
||||
uuid=data.uuid,
|
||||
type=str(data.type),
|
||||
context=data.context,
|
||||
reference=data.reference,
|
||||
suggestions=[
|
||||
Suggestion(
|
||||
uuid=suggestion.uuid,
|
||||
type=str(suggestion.type),
|
||||
context=suggestion.context,
|
||||
reference=suggestion.reference,
|
||||
)
|
||||
for suggestion in suggestions
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def remove_issue(self, issue: Issue) -> None:
|
||||
"""Remove an issue from the list. Delete a repair if necessary."""
|
||||
@@ -312,13 +330,13 @@ class SupervisorIssues:
|
||||
return
|
||||
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid)
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
|
||||
|
||||
del self._issues[issue.uuid]
|
||||
|
||||
def get_issue(self, issue_id: str) -> Issue | None:
|
||||
"""Get issue from key."""
|
||||
return self._issues.get(issue_id)
|
||||
return self._issues.get(UUID(issue_id))
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
@@ -331,8 +349,8 @@ class SupervisorIssues:
|
||||
async def _update(self, _: datetime | None = None) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
try:
|
||||
data = await self._client.get_resolution_info()
|
||||
except HassioAPIError as err:
|
||||
data = await self._supervisor_client.resolution.info()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.error("Failed to update supervisor issues: %r", err)
|
||||
async_call_later(
|
||||
self._hass,
|
||||
@@ -340,18 +358,16 @@ class SupervisorIssues:
|
||||
HassJob(self._update, cancel_on_shutdown=True),
|
||||
)
|
||||
return
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
self.unhealthy_reasons = set(data.unhealthy)
|
||||
self.unsupported_reasons = set(data.unsupported)
|
||||
|
||||
# Remove any cached issues that weren't returned
|
||||
for issue_id in set(self._issues.keys()) - {
|
||||
issue["uuid"] for issue in data[ATTR_ISSUES]
|
||||
}:
|
||||
for issue_id in set(self._issues) - {issue.uuid for issue in data.issues}:
|
||||
self.remove_issue(self._issues[issue_id])
|
||||
|
||||
# Add/update any issues that came back
|
||||
await asyncio.gather(
|
||||
*[self.add_issue_from_data(issue) for issue in data[ATTR_ISSUES]]
|
||||
*[self.add_issue_from_data(issue) for issue in data.issues]
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -6,6 +6,8 @@ from collections.abc import Callable, Coroutine
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import ContextType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
@@ -20,9 +22,8 @@ from .const import (
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_COMPONENTS,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .handler import async_apply_suggestion
|
||||
from .handler import get_supervisor_client
|
||||
from .issues import Issue, Suggestion
|
||||
|
||||
HELP_URLS = {
|
||||
@@ -51,9 +52,10 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
_data: dict[str, Any] | None = None
|
||||
_issue: Issue | None = None
|
||||
|
||||
def __init__(self, issue_id: str) -> None:
|
||||
def __init__(self, hass: HomeAssistant, issue_id: str) -> None:
|
||||
"""Initialize repair flow."""
|
||||
self._issue_id = issue_id
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
@@ -124,9 +126,12 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
|
||||
return self._async_form_for_suggestion(suggestion)
|
||||
|
||||
if await async_apply_suggestion(self.hass, suggestion.uuid):
|
||||
return self.async_create_entry(data={})
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
try:
|
||||
await self._supervisor_client.resolution.apply_suggestion(suggestion.uuid)
|
||||
except SupervisorError:
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
@staticmethod
|
||||
def _async_step(
|
||||
@@ -163,9 +168,9 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
if issue.key == self.issue.key or issue.type != self.issue.type:
|
||||
continue
|
||||
|
||||
if issue.context == SupervisorIssueContext.CORE:
|
||||
if issue.context == ContextType.CORE:
|
||||
components.insert(0, "Home Assistant")
|
||||
elif issue.context == SupervisorIssueContext.ADDON:
|
||||
elif issue.context == ContextType.ADDON:
|
||||
components.append(
|
||||
next(
|
||||
(
|
||||
@@ -210,11 +215,11 @@ async def async_create_fix_flow(
|
||||
supervisor_issues = get_issues_info(hass)
|
||||
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
||||
return DockerConfigIssueRepairFlow(issue_id)
|
||||
return DockerConfigIssueRepairFlow(hass, issue_id)
|
||||
if issue and issue.key in {
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
}:
|
||||
return AddonIssueRepairFlow(issue_id)
|
||||
return AddonIssueRepairFlow(hass, issue_id)
|
||||
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
||||
return SupervisorIssueRepairFlow(hass, issue_id)
|
||||
|
||||
@@ -113,7 +113,7 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> HERETravelTimeOptionsFlow:
|
||||
"""Get the options flow."""
|
||||
return HERETravelTimeOptionsFlow(config_entry)
|
||||
return HERETravelTimeOptionsFlow()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -297,9 +297,8 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class HERETravelTimeOptionsFlow(OptionsFlow):
|
||||
"""Handle HERE Travel Time options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize HERE Travel Time options flow."""
|
||||
self.config_entry = config_entry
|
||||
self._config: dict[str, Any] = {}
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -182,7 +182,6 @@ class HiveOptionsFlowHandler(OptionsFlow):
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Hive options flow."""
|
||||
self.hive = None
|
||||
self.config_entry = config_entry
|
||||
self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120)
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -362,15 +362,14 @@ class HomeKitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for homekit."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.hk_options: dict[str, Any] = {}
|
||||
self.included_cameras: list[str] = []
|
||||
|
||||
|
||||
@@ -420,6 +420,7 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.WIND_SPEED
|
||||
_attr_native_unit_of_measurement = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the windspeed sensor."""
|
||||
@@ -451,6 +452,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.PRECIPITATION
|
||||
_attr_native_unit_of_measurement = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
|
||||
@@ -129,16 +129,12 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> HoneywellOptionsFlowHandler:
|
||||
"""Options callback for Honeywell."""
|
||||
return HoneywellOptionsFlowHandler(config_entry)
|
||||
return HoneywellOptionsFlowHandler()
|
||||
|
||||
|
||||
class HoneywellOptionsFlowHandler(OptionsFlow):
|
||||
"""Config flow options for Honeywell."""
|
||||
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize Honeywell options flow."""
|
||||
self.config_entry = entry
|
||||
|
||||
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
|
||||
@@ -69,7 +69,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get options flow."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _async_show_user_form(
|
||||
self,
|
||||
@@ -345,10 +345,6 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Huawei LTE options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["huawei_lte_api.Session"],
|
||||
"requirements": [
|
||||
"huawei-lte-api==1.9.3",
|
||||
"huawei-lte-api==1.10.0",
|
||||
"stringcase==1.2.0",
|
||||
"url-normalize==1.4.3"
|
||||
],
|
||||
|
||||
@@ -57,8 +57,8 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
|
||||
return HueV1OptionsFlowHandler(config_entry)
|
||||
return HueV2OptionsFlowHandler(config_entry)
|
||||
return HueV1OptionsFlowHandler()
|
||||
return HueV2OptionsFlowHandler()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Hue flow."""
|
||||
@@ -280,10 +280,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class HueV1OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Hue options for V1 implementation."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Hue options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -315,10 +311,6 @@ class HueV1OptionsFlowHandler(OptionsFlow):
|
||||
class HueV2OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Hue options for V2 implementation."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Hue options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -137,15 +137,15 @@
|
||||
"services": {
|
||||
"hue_activate_scene": {
|
||||
"name": "Activate scene",
|
||||
"description": "Activates a hue scene stored in the hue hub.",
|
||||
"description": "Activates a Hue scene stored in the Hue hub.",
|
||||
"fields": {
|
||||
"group_name": {
|
||||
"name": "Group",
|
||||
"description": "Name of hue group/room from the hue app."
|
||||
"description": "Name of Hue group/room from the Hue app."
|
||||
},
|
||||
"scene_name": {
|
||||
"name": "Scene",
|
||||
"description": "Name of hue scene from the hue app."
|
||||
"description": "Name of Hue scene from the Hue app."
|
||||
},
|
||||
"dynamic": {
|
||||
"name": "Dynamic",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huum",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["huum==0.7.10"]
|
||||
"requirements": ["huum==0.7.11"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user