forked from home-assistant/core
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98cff0bd74 | |||
| b3a32b5f59 | |||
| d7d2b7ad76 |
@@ -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@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
|
||||
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
@@ -622,13 +622,13 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} 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
|
||||
name: Split tests for full run Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Install additional OS dependencies
|
||||
run: |
|
||||
@@ -949,7 +949,6 @@ jobs:
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
--numprocesses auto \
|
||||
--snapshot-details \
|
||||
--dist=loadfile \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
@@ -1072,7 +1071,6 @@ jobs:
|
||||
-qq \
|
||||
--timeout=20 \
|
||||
--numprocesses 1 \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=10 \
|
||||
@@ -1201,7 +1199,6 @@ jobs:
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--numprocesses 1 \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
@@ -1348,7 +1345,6 @@ jobs:
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--numprocesses auto \
|
||||
--snapshot-details \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
|
||||
+2
-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/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||
/tests/components/fritzbox/ @mib1185 @flabbamann
|
||||
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
|
||||
|
||||
+1
-2
@@ -7,8 +7,7 @@ FROM ${BUILD_FROM}
|
||||
# Synchronize with homeassistant/core.py:async_stop
|
||||
ENV \
|
||||
S6_SERVICES_GRACETIME=240000 \
|
||||
UV_SYSTEM_PYTHON=true \
|
||||
UV_NO_CACHE=true
|
||||
UV_SYSTEM_PYTHON=true
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
||||
"""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"))
|
||||
instruction_content = instruction_path.read_text(encoding="utf-8")
|
||||
return RestoreBackupFileContent(
|
||||
backup_file_path=Path(instruction_content["path"])
|
||||
backup_file_path=Path(instruction_content.split(";")[0])
|
||||
)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
@@ -12,10 +13,20 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||
from .coordinator import 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."""
|
||||
@@ -35,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
||||
except AemetError as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, entry, aemet)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
|
||||
await weather_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Final, cast
|
||||
@@ -20,7 +19,6 @@ 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
|
||||
|
||||
@@ -31,16 +29,6 @@ _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."""
|
||||
@@ -48,7 +36,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AemetConfigEntry,
|
||||
aemet: AEMET,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
@@ -57,7 +44,6 @@ 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 .coordinator import AemetConfigEntry
|
||||
from . import AemetConfigEntry
|
||||
|
||||
TO_REDACT_CONFIG = [
|
||||
CONF_API_KEY,
|
||||
|
||||
@@ -55,6 +55,7 @@ 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,
|
||||
@@ -86,7 +87,7 @@ from .const import (
|
||||
ATTR_API_WIND_SPEED,
|
||||
CONDITIONS_MAP,
|
||||
)
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
|
||||
@@ -248,7 +249,6 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
name="Rain",
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
key=ATTR_API_RAIN_PROB,
|
||||
@@ -263,7 +263,6 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
name="Snow",
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
key=ATTR_API_SNOW_PROB,
|
||||
|
||||
@@ -27,8 +27,9 @@ 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 AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .coordinator import 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.24"]
|
||||
"requirements": ["agent-py==0.0.23"]
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.9.2"]
|
||||
"requirements": ["airthings-ble==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -1083,13 +1083,7 @@ async def async_api_arm(
|
||||
arm_state = directive.payload["armState"]
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
# Per Alexa Documentation: users are not allowed to switch from armed_away
|
||||
# directly to another armed state without first disarming the system.
|
||||
# https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-securitypanelcontroller.html#arming
|
||||
if (
|
||||
entity.state == alarm_control_panel.AlarmControlPanelState.ARMED_AWAY
|
||||
and arm_state != "ARMED_AWAY"
|
||||
):
|
||||
if entity.state != alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||
msg = "You must disarm the system before you can set the requested arm state."
|
||||
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
@@ -370,3 +370,71 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"""Return the devices payload."""
|
||||
integrations_without_model_id: set[str] = set()
|
||||
devices: list[dict[str, Any]] = []
|
||||
dev_reg = dr.async_get(hass)
|
||||
ignored_integrations = {
|
||||
"bluetooth",
|
||||
"esphome",
|
||||
"hassio",
|
||||
"mqtt",
|
||||
}
|
||||
# Devices that need via device info set
|
||||
new_indexes: dict[str, int] = {}
|
||||
via_devices: dict[str, str] = {}
|
||||
|
||||
for device in dev_reg.devices.values():
|
||||
# Ignore services
|
||||
if device.entry_type:
|
||||
continue
|
||||
|
||||
if not device.primary_config_entry:
|
||||
continue
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(device.primary_config_entry)
|
||||
|
||||
if config_entry is None:
|
||||
continue
|
||||
|
||||
if config_entry.domain in ignored_integrations:
|
||||
continue
|
||||
|
||||
if not device.model_id:
|
||||
integrations_without_model_id.add(config_entry.domain)
|
||||
continue
|
||||
|
||||
if not device.manufacturer:
|
||||
continue
|
||||
|
||||
new_indexes[device.id] = len(devices)
|
||||
devices.append(
|
||||
{
|
||||
"integration": config_entry.domain,
|
||||
"manufacturer": device.manufacturer,
|
||||
"model_id": device.model_id,
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"hw_version": device.hw_version,
|
||||
"has_suggested_area": device.suggested_area is not None,
|
||||
"has_configuration_url": device.configuration_url is not None,
|
||||
"via_device": None,
|
||||
}
|
||||
)
|
||||
if device.via_device_id:
|
||||
via_devices[device.id] = device.via_device_id
|
||||
|
||||
for from_device, via_device in via_devices.items():
|
||||
if via_device not in new_indexes:
|
||||
continue
|
||||
devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device]
|
||||
|
||||
return {
|
||||
"version": "home-assistant:1",
|
||||
"no_model_id": sorted(integrations_without_model_id),
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -45,11 +46,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return HomeassistantAnalyticsOptionsFlowHandler()
|
||||
return HomeassistantAnalyticsOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -133,7 +132,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
|
||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle Homeassistant Analytics options."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -212,6 +211,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
|
||||
),
|
||||
},
|
||||
),
|
||||
self.config_entry.options,
|
||||
self.options,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
@@ -186,14 +186,16 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle an option flow for Android Debug Bridge."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
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, {})
|
||||
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._conf_app_id: str | None = None
|
||||
self._conf_rule_id: str | None = None
|
||||
@@ -235,7 +237,7 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
|
||||
]
|
||||
rules = [RULES_NEW_ID, *self._state_det_rules]
|
||||
options = self.config_entry.options
|
||||
options = self.options
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
@@ -221,12 +221,13 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Android TV Remote options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||
super().__init__(config_entry)
|
||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
||||
self._conf_app_id: str | None = None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -121,6 +121,7 @@ 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)"""
|
||||
|
||||
speech_probability: float | None
|
||||
"""Probability that audio chunk contains speech (0-1), None if unknown"""
|
||||
is_speech: bool | None
|
||||
"""True if audio chunk likely contains speech, False if not, 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")
|
||||
_LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
|
||||
|
||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
speech_probability: float | None = None
|
||||
is_speech: bool | None = None
|
||||
|
||||
assert len(audio) == BYTES_PER_CHUNK
|
||||
|
||||
if self.vad is not None:
|
||||
# Run VAD
|
||||
speech_probability = self.vad.Process10ms(audio)
|
||||
speech_prob = self.vad.Process10ms(audio)
|
||||
is_speech = speech_prob > self.threshold
|
||||
|
||||
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,
|
||||
speech_probability=speech_probability,
|
||||
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
||||
)
|
||||
|
||||
@@ -780,9 +780,7 @@ class PipelineRun:
|
||||
# speaking the voice command.
|
||||
audio_chunks_for_stt.extend(
|
||||
EnhancedAudioChunk(
|
||||
audio=chunk_ts[0],
|
||||
timestamp_ms=chunk_ts[1],
|
||||
speech_probability=None,
|
||||
audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
|
||||
)
|
||||
for chunk_ts in result.queued_audio
|
||||
)
|
||||
@@ -829,7 +827,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.speech_probability):
|
||||
if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
|
||||
raise WakeWordTimeoutError(
|
||||
code="wake-word-timeout", message="Wake word was not detected"
|
||||
)
|
||||
@@ -957,7 +955,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.speech_probability):
|
||||
if not stt_vad.process(chunk_seconds, chunk.is_speech):
|
||||
# Silence detected at the end of voice command
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
@@ -1223,7 +1221,7 @@ class PipelineRun:
|
||||
yield EnhancedAudioChunk(
|
||||
audio=sub_chunk,
|
||||
timestamp_ms=timestamp_ms,
|
||||
speech_probability=None, # no VAD
|
||||
is_speech=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.1
|
||||
speech_seconds: float = 0.3
|
||||
"""Seconds of speech before voice command has started."""
|
||||
|
||||
command_seconds: float = 1.0
|
||||
@@ -96,12 +96,6 @@ 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."""
|
||||
|
||||
@@ -130,7 +124,7 @@ class VoiceCommandSegmenter:
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
self.in_command = False
|
||||
|
||||
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
||||
"""Process samples using external VAD.
|
||||
|
||||
Returns False when command is done.
|
||||
@@ -148,12 +142,7 @@ 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
|
||||
@@ -171,29 +160,24 @@ 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:
|
||||
# In command
|
||||
is_speech = speech_probability > self.in_command_speech_threshold
|
||||
if not is_speech:
|
||||
# Silence in command
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -242,9 +226,6 @@ 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."""
|
||||
|
||||
@@ -260,15 +241,12 @@ class VoiceActivityTimeout:
|
||||
self._silence_seconds_left = self.silence_seconds
|
||||
self._reset_seconds_left = self.reset_seconds
|
||||
|
||||
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
||||
"""Process samples using external VAD.
|
||||
|
||||
Returns False when timeout is reached.
|
||||
"""
|
||||
if speech_probability is None:
|
||||
speech_probability = 0.0
|
||||
|
||||
if speech_probability > self.speech_threshold:
|
||||
if is_speech:
|
||||
# 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,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -59,11 +59,9 @@ 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()
|
||||
return AxisOptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Axis config flow."""
|
||||
@@ -266,7 +264,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
class AxisOptionsFlowHandler(OptionsFlow):
|
||||
class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle Axis device options."""
|
||||
|
||||
config_entry: AxisConfigEntry
|
||||
@@ -284,7 +282,8 @@ class AxisOptionsFlowHandler(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the Axis device stream options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=self.config_entry.options | user_input)
|
||||
self.options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
|
||||
schema = {}
|
||||
|
||||
|
||||
@@ -124,9 +124,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id=STEP_CONN_STRING,
|
||||
data_schema=CONN_STRING_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
|
||||
},
|
||||
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
@@ -146,9 +144,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id=STEP_SAS,
|
||||
data_schema=SAS_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
|
||||
},
|
||||
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ class BackupManager(BaseBackupManager):
|
||||
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()}),
|
||||
f"{backup.path.as_posix()};",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@@ -21,57 +21,41 @@ class BangOlufsenSource:
|
||||
name="Audio Streamer",
|
||||
id="uriStreamer",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
BLUETOOTH: Final[Source] = Source(
|
||||
name="Bluetooth",
|
||||
id="bluetooth",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
CHROMECAST: Final[Source] = Source(
|
||||
name="Chromecast built-in",
|
||||
id="chromeCast",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
LINE_IN: Final[Source] = Source(
|
||||
name="Line-In",
|
||||
id="lineIn",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
SPDIF: Final[Source] = Source(
|
||||
name="Optical",
|
||||
id="spdif",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
NET_RADIO: Final[Source] = Source(
|
||||
name="B&O Radio",
|
||||
id="netRadio",
|
||||
is_seekable=False,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
DEEZER: Final[Source] = Source(
|
||||
name="Deezer",
|
||||
id="deezer",
|
||||
is_seekable=True,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
TIDAL: Final[Source] = Source(
|
||||
name="Tidal",
|
||||
id="tidal",
|
||||
is_seekable=True,
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -186,6 +170,20 @@ VALID_MEDIA_TYPES: Final[tuple] = (
|
||||
MediaType.CHANNEL,
|
||||
)
|
||||
|
||||
# Sources on the device that should not be selectable by the user
|
||||
HIDDEN_SOURCE_IDS: Final[tuple] = (
|
||||
"airPlay",
|
||||
"bluetooth",
|
||||
"chromeCast",
|
||||
"generator",
|
||||
"local",
|
||||
"dlna",
|
||||
"qplay",
|
||||
"wpl",
|
||||
"pl",
|
||||
"beolink",
|
||||
"usbIn",
|
||||
)
|
||||
|
||||
# Fallback sources to use in case of API failure.
|
||||
FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
@@ -193,7 +191,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
Source(
|
||||
id="uriStreamer",
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
is_playable=False,
|
||||
name="Audio Streamer",
|
||||
type=SourceTypeEnum(value="uriStreamer"),
|
||||
is_seekable=False,
|
||||
@@ -201,7 +199,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
Source(
|
||||
id="bluetooth",
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
is_playable=False,
|
||||
name="Bluetooth",
|
||||
type=SourceTypeEnum(value="bluetooth"),
|
||||
is_seekable=False,
|
||||
@@ -209,7 +207,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
Source(
|
||||
id="spotify",
|
||||
is_enabled=True,
|
||||
is_playable=True,
|
||||
is_playable=False,
|
||||
name="Spotify Connect",
|
||||
type=SourceTypeEnum(value="spotify"),
|
||||
is_seekable=True,
|
||||
|
||||
@@ -70,6 +70,7 @@ from .const import (
|
||||
CONNECTION_STATUS,
|
||||
DOMAIN,
|
||||
FALLBACK_SOURCES,
|
||||
HIDDEN_SOURCE_IDS,
|
||||
VALID_MEDIA_TYPES,
|
||||
BangOlufsenMediaType,
|
||||
BangOlufsenSource,
|
||||
@@ -168,7 +169,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error,
|
||||
WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata_and_beolink,
|
||||
WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress,
|
||||
WebsocketNotification.PLAYBACK_SOURCE: self._async_update_sources,
|
||||
WebsocketNotification.PLAYBACK_STATE: self._async_update_playback_state,
|
||||
WebsocketNotification.REMOTE_MENU_CHANGED: self._async_update_sources,
|
||||
WebsocketNotification.SOURCE_CHANGE: self._async_update_source_change,
|
||||
@@ -243,7 +243,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
if queue_settings.shuffle is not None:
|
||||
self._attr_shuffle = queue_settings.shuffle
|
||||
|
||||
async def _async_update_sources(self, _: Source | None = None) -> None:
|
||||
async def _async_update_sources(self) -> None:
|
||||
"""Get sources for the specific product."""
|
||||
|
||||
# Audio sources
|
||||
@@ -270,7 +270,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
self._audio_sources = {
|
||||
source.id: source.name
|
||||
for source in cast(list[Source], sources.items)
|
||||
if source.is_enabled and source.id and source.name and source.is_playable
|
||||
if source.is_enabled
|
||||
and source.id
|
||||
and source.name
|
||||
and source.id not in HIDDEN_SOURCE_IDS
|
||||
}
|
||||
|
||||
# Some sources are not Beolink expandable, meaning that they can't be joined by
|
||||
|
||||
@@ -63,9 +63,6 @@ class BangOlufsenWebsocket(BangOlufsenBase):
|
||||
self._client.get_playback_progress_notifications(
|
||||
self.on_playback_progress_notification
|
||||
)
|
||||
self._client.get_playback_source_notifications(
|
||||
self.on_playback_source_notification
|
||||
)
|
||||
self._client.get_playback_state_notifications(
|
||||
self.on_playback_state_notification
|
||||
)
|
||||
@@ -160,14 +157,6 @@ class BangOlufsenWebsocket(BangOlufsenBase):
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_playback_source_notification(self, notification: Source) -> None:
|
||||
"""Send playback_source dispatch."""
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_SOURCE}",
|
||||
notification,
|
||||
)
|
||||
|
||||
def on_source_change_notification(self, notification: Source) -> None:
|
||||
"""Send source_change dispatch."""
|
||||
async_dispatcher_send(
|
||||
|
||||
@@ -364,13 +364,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if self.is_grouped and not self.is_master:
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
match self._status.state:
|
||||
case "pause":
|
||||
return MediaPlayerState.PAUSED
|
||||
case "stream" | "play":
|
||||
return MediaPlayerState.PLAYING
|
||||
case _:
|
||||
return MediaPlayerState.IDLE
|
||||
status = self._status.state
|
||||
if status in ("pause", "stop"):
|
||||
return MediaPlayerState.PAUSED
|
||||
if status in ("stream", "play"):
|
||||
return MediaPlayerState.PLAYING
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@property
|
||||
def media_title(self) -> str | None:
|
||||
|
||||
@@ -7,11 +7,7 @@ from typing import Any
|
||||
|
||||
from bimmer_connected.api.authentication import MyBMWAuthentication
|
||||
from bimmer_connected.api.regions import get_region_from_name
|
||||
from bimmer_connected.models import (
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
|
||||
from httpx import RequestError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -21,7 +17,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -58,8 +54,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
try:
|
||||
await auth.login()
|
||||
except MyBMWCaptchaMissingError as ex:
|
||||
raise MissingCaptcha from ex
|
||||
except MyBMWAuthError as ex:
|
||||
raise InvalidAuth from ex
|
||||
except (MyBMWAPIError, RequestError) as ex:
|
||||
@@ -104,8 +98,6 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
except MissingCaptcha:
|
||||
errors["base"] = "missing_captcha"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
@@ -153,10 +145,10 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> BMWOptionsFlow:
|
||||
"""Return a MyBMW option flow."""
|
||||
return BMWOptionsFlow()
|
||||
return BMWOptionsFlow(config_entry)
|
||||
|
||||
|
||||
class BMWOptionsFlow(OptionsFlow):
|
||||
class BMWOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
"""Handle a option flow for MyBMW."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -200,7 +192,3 @@ class CannotConnect(HomeAssistantError):
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class MissingCaptcha(HomeAssistantError):
|
||||
"""Error to indicate the captcha token is missing."""
|
||||
|
||||
@@ -7,12 +7,7 @@ import logging
|
||||
|
||||
from bimmer_connected.account import MyBMWAccount
|
||||
from bimmer_connected.api.regions import get_region_from_name
|
||||
from bimmer_connected.models import (
|
||||
GPSPosition,
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from bimmer_connected.models import GPSPosition, MyBMWAPIError, MyBMWAuthError
|
||||
from httpx import RequestError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -66,12 +61,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
try:
|
||||
await self.account.get_vehicles()
|
||||
except MyBMWCaptchaMissingError as err:
|
||||
# If a captcha is required (user/password login flow), always trigger the reauth flow
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_captcha",
|
||||
) from err
|
||||
except MyBMWAuthError as err:
|
||||
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
|
||||
if self.last_update_success:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bimmer-connected[china]==0.16.4"]
|
||||
"requirements": ["bimmer-connected[china]==0.16.3"]
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"missing_captcha": "Captcha validation missing"
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
@@ -201,9 +200,6 @@
|
||||
"exceptions": {
|
||||
"invalid_poi": {
|
||||
"message": "Invalid data for point of interest: {poi_exception}"
|
||||
},
|
||||
"missing_captcha": {
|
||||
"message": "Login requires captcha validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"list_access": {
|
||||
"default": "mdi:account-lock",
|
||||
"state": {
|
||||
"shared": "mdi:account-group",
|
||||
"invitation": "mdi:account-multiple-plus"
|
||||
"shared": "mdi:account-group"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["bring-api==0.9.1"]
|
||||
"requirements": ["bring-api==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -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", "invitation"],
|
||||
options=["registered", "shared"],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -66,8 +66,7 @@
|
||||
"name": "List access",
|
||||
"state": {
|
||||
"registered": "Private",
|
||||
"shared": "Shared",
|
||||
"invitation": "Invitation pending"
|
||||
"shared": "Shared"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==1.2.1"]
|
||||
"requirements": ["python-bsblan==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ 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,
|
||||
@@ -127,7 +126,6 @@ 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,
|
||||
@@ -154,7 +152,6 @@ 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,
|
||||
@@ -207,8 +204,7 @@ 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, # type: ignore[arg-type]
|
||||
self._event.start_datetime_local, self.coordinator.offset
|
||||
)
|
||||
if self._event
|
||||
else False
|
||||
|
||||
@@ -6,9 +6,6 @@ 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
|
||||
@@ -17,9 +14,6 @@ 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)
|
||||
@@ -29,20 +23,11 @@ OFFSET = "!!"
|
||||
class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: CalDavConfigEntry | None,
|
||||
calendar: caldav.Calendar,
|
||||
days: int,
|
||||
include_all_day: bool,
|
||||
search: str | None,
|
||||
) -> None:
|
||||
def __init__(self, hass, calendar, days, include_all_day, search):
|
||||
"""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,
|
||||
)
|
||||
@@ -50,7 +35,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
self.days = days
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.offset: timedelta | None = None
|
||||
self.offset = None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
@@ -124,7 +109,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 # type: ignore[attr-defined]
|
||||
new_vevent = new_event.instance.vevent
|
||||
if hasattr(new_vevent, "dtend"):
|
||||
dur = new_vevent.dtend.value - new_vevent.dtstart.value
|
||||
new_vevent.dtend.value = start_dt + dur
|
||||
|
||||
@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
|
||||
import attr
|
||||
from propcache import cached_property, under_cached_property
|
||||
import voluptuous as vol
|
||||
from webrtc_models import RTCIceCandidate, RTCIceServer
|
||||
from webrtc_models import RTCIceServer
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
@@ -421,12 +421,8 @@ 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",
|
||||
"stun:stun.home-assistant.io:3478",
|
||||
]
|
||||
),
|
||||
RTCIceServer(urls="stun:stun.home-assistant.io:80"),
|
||||
RTCIceServer(urls="stun:stun.home-assistant.io:3478"),
|
||||
]
|
||||
|
||||
async_register_ice_servers(hass, get_ice_servers)
|
||||
@@ -476,8 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
_attr_state: None = None # State is determined by is_on
|
||||
_attr_supported_features: CameraEntityFeature = CameraEntityFeature(0)
|
||||
|
||||
__supports_stream: CameraEntityFeature | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a camera."""
|
||||
self._cache: dict[str, Any] = {}
|
||||
@@ -490,13 +484,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self._create_stream_lock: asyncio.Lock | None = None
|
||||
self._webrtc_provider: CameraWebRTCProvider | None = None
|
||||
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
|
||||
self._supports_native_sync_webrtc = (
|
||||
self._webrtc_sync_offer = (
|
||||
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
||||
)
|
||||
self._supports_native_async_webrtc = (
|
||||
type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def entity_picture(self) -> str:
|
||||
@@ -633,7 +623,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
if self._supports_native_sync_webrtc:
|
||||
if self._webrtc_sync_offer:
|
||||
try:
|
||||
answer = await self.async_handle_web_rtc_offer(offer_sdp)
|
||||
except ValueError as ex:
|
||||
@@ -789,9 +779,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
self.__supports_stream = (
|
||||
self.supported_features_compat & CameraEntityFeature.STREAM
|
||||
)
|
||||
await self.async_refresh_providers(write_state=False)
|
||||
|
||||
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
|
||||
@@ -801,25 +788,18 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
providers or inputs to the state attributes change.
|
||||
"""
|
||||
old_provider = self._webrtc_provider
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
)
|
||||
|
||||
old_legacy_provider = self._legacy_webrtc_provider
|
||||
new_provider = None
|
||||
new_legacy_provider = None
|
||||
|
||||
# Skip all providers if the camera has a native WebRTC implementation
|
||||
if not (
|
||||
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
|
||||
):
|
||||
# Camera doesn't have a native WebRTC implementation
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
if new_provider is None:
|
||||
# Only add the legacy provider if the new provider is not available
|
||||
new_legacy_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_legacy_provider
|
||||
)
|
||||
|
||||
if new_provider is None:
|
||||
# Only add the legacy provider if the new provider is not available
|
||||
new_legacy_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_legacy_provider
|
||||
)
|
||||
|
||||
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
|
||||
self._webrtc_provider = new_provider
|
||||
self._legacy_webrtc_provider = new_legacy_provider
|
||||
@@ -847,26 +827,20 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
|
||||
config = self._async_get_webrtc_client_configuration()
|
||||
|
||||
if not self._supports_native_sync_webrtc:
|
||||
# Until 2024.11, the frontend was not resolving any ice servers
|
||||
# The async approach was added 2024.11 and new integrations need to use it
|
||||
ice_servers = [
|
||||
server
|
||||
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
|
||||
for server in servers()
|
||||
]
|
||||
config.configuration.ice_servers.extend(ice_servers)
|
||||
ice_servers = [
|
||||
server
|
||||
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
|
||||
for server in servers()
|
||||
]
|
||||
config.configuration.ice_servers.extend(ice_servers)
|
||||
|
||||
config.get_candidates_upfront = (
|
||||
self._supports_native_sync_webrtc
|
||||
or self._legacy_webrtc_provider is not None
|
||||
self._webrtc_sync_offer or self._legacy_webrtc_provider is not None
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
|
||||
"""Handle a WebRTC candidate."""
|
||||
if self._webrtc_provider:
|
||||
await self._webrtc_provider.async_on_webrtc_candidate(session_id, candidate)
|
||||
@@ -890,7 +864,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
|
||||
if (
|
||||
type(self).async_handle_web_rtc_offer
|
||||
!= Camera.async_handle_web_rtc_offer
|
||||
or type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
):
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
else:
|
||||
@@ -901,21 +880,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
return CameraCapabilities(frontend_stream_types)
|
||||
|
||||
@callback
|
||||
def async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine.
|
||||
|
||||
Schedules async_refresh_providers if support of streams have changed.
|
||||
"""
|
||||
super().async_write_ha_state()
|
||||
if self.__supports_stream != (
|
||||
supports_stream := self.supported_features_compat
|
||||
& CameraEntityFeature.STREAM
|
||||
):
|
||||
self.__supports_stream = supports_stream
|
||||
self._invalidate_camera_capabilities_cache()
|
||||
self.hass.async_create_task(self.async_refresh_providers())
|
||||
|
||||
|
||||
class CameraView(HomeAssistantView):
|
||||
"""Base CameraView."""
|
||||
|
||||
@@ -11,7 +11,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
import voluptuous as vol
|
||||
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceServer
|
||||
from webrtc_models import RTCConfiguration, RTCIceServer
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -78,14 +78,7 @@ class WebRTCAnswer(WebRTCMessage):
|
||||
class WebRTCCandidate(WebRTCMessage):
|
||||
"""WebRTC candidate."""
|
||||
|
||||
candidate: RTCIceCandidate
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the message."""
|
||||
return {
|
||||
"type": self._get_type(),
|
||||
"candidate": self.candidate.candidate,
|
||||
}
|
||||
candidate: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -145,9 +138,7 @@ class CameraWebRTCProvider(ABC):
|
||||
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
||||
|
||||
@abstractmethod
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
|
||||
"""Handle the WebRTC candidate."""
|
||||
|
||||
@callback
|
||||
@@ -328,9 +319,7 @@ async def ws_candidate(
|
||||
)
|
||||
return
|
||||
|
||||
await camera.async_on_webrtc_candidate(
|
||||
msg["session_id"], RTCIceCandidate(msg["candidate"])
|
||||
)
|
||||
await camera.async_on_webrtc_candidate(msg["session_id"], msg["candidate"])
|
||||
connection.send_message(websocket_api.result_message(msg["id"]))
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> CastOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return CastOptionsFlowHandler()
|
||||
return CastOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -109,8 +109,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class CastOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Google Cast options."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> 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.84.0"],
|
||||
"requirements": ["hass-nabucasa==0.83.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": ["@Petro31"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"requirements": ["numpy==2.1.2"]
|
||||
"requirements": ["numpy==1.26.4"]
|
||||
}
|
||||
|
||||
@@ -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.6"]
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.10.30"]
|
||||
}
|
||||
|
||||
@@ -213,19 +213,18 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow):
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Crownstone options."""
|
||||
super().__init__(OPTIONS_FLOW, self.async_create_new_entry)
|
||||
self.options = config_entry.options.copy()
|
||||
self.entry = config_entry
|
||||
self.updated_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.config_entry.entry_id
|
||||
].cloud
|
||||
self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][self.entry.entry_id].cloud
|
||||
|
||||
spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data}
|
||||
usb_path = self.config_entry.options.get(CONF_USB_PATH)
|
||||
usb_sphere = self.config_entry.options.get(CONF_USB_SPHERE)
|
||||
usb_path = self.entry.options.get(CONF_USB_PATH)
|
||||
usb_sphere = self.entry.options.get(CONF_USB_SPHERE)
|
||||
|
||||
options_schema = vol.Schema(
|
||||
{vol.Optional(CONF_USE_USB_OPTION, default=usb_path is not None): bool}
|
||||
@@ -244,14 +243,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.options[CONF_USB_PATH] = None
|
||||
self.options[CONF_USB_SPHERE] = None
|
||||
self.updated_options[CONF_USB_PATH] = None
|
||||
self.updated_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.options[CONF_USB_SPHERE] = sphere_id
|
||||
self.updated_options[CONF_USB_SPHERE] = sphere_id
|
||||
|
||||
return self.async_create_new_entry()
|
||||
|
||||
@@ -261,7 +260,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.options[CONF_USB_PATH] = self.usb_path
|
||||
self.options[CONF_USB_SPHERE] = self.usb_sphere_id
|
||||
self.updated_options[CONF_USB_PATH] = self.usb_path
|
||||
self.updated_options[CONF_USB_SPHERE] = self.usb_sphere_id
|
||||
|
||||
return super().async_create_entry(title="", data=self.options)
|
||||
return super().async_create_entry(title="", data=self.updated_options)
|
||||
|
||||
@@ -74,11 +74,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> DeconzOptionsFlowHandler:
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return DeconzOptionsFlowHandler()
|
||||
return DeconzOptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the deCONZ config flow."""
|
||||
@@ -301,6 +299,11 @@ 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:
|
||||
@@ -312,7 +315,8 @@ class DeconzOptionsFlowHandler(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the deconz devices options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=self.config_entry.options | user_input)
|
||||
self.options.update(user_input)
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
|
||||
schema_options = {}
|
||||
for option, default in (
|
||||
|
||||
@@ -47,6 +47,7 @@ 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(
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
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()
|
||||
return DnsIPOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -165,7 +165,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class DnsIPOptionsFlowHandler(OptionsFlow):
|
||||
class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle a option config flow for dnsip integration."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==3.0.8"],
|
||||
"requirements": ["DoorBirdPy==3.0.7"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_axis-video._tcp.local.",
|
||||
|
||||
@@ -171,11 +171,9 @@ 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()
|
||||
return DSMROptionFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -313,6 +311,10 @@ 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:
|
||||
@@ -326,7 +328,7 @@ class DSMROptionFlowHandler(OptionsFlow):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TIME_BETWEEN_UPDATE,
|
||||
default=self.config_entry.options.get(
|
||||
default=self.entry.options.get(
|
||||
CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
|
||||
@@ -31,26 +31,25 @@ async def async_setup_entry(
|
||||
"""Set up the ecobee thermostat switch entity."""
|
||||
data: EcobeeData = hass.data[DOMAIN]
|
||||
|
||||
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)
|
||||
async_add_entities(
|
||||
[
|
||||
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"]["hasHeatPump"]
|
||||
)
|
||||
if thermostat["settings"]["ventilatorType"] != "none"
|
||||
],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
async_add_entities(
|
||||
EcobeeSwitchAuxHeatOnly(data, index)
|
||||
for index, thermostat in enumerate(data.ecobee.thermostats)
|
||||
if thermostat["settings"]["hasHeatPump"]
|
||||
)
|
||||
|
||||
|
||||
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -102,12 +103,13 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return ElevenLabsOptionsFlow(config_entry)
|
||||
|
||||
|
||||
class ElevenLabsOptionsFlow(OptionsFlow):
|
||||
class ElevenLabsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
"""ElevenLabs options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.api_key: str = config_entry.data[CONF_API_KEY]
|
||||
super().__init__(config_entry)
|
||||
self.api_key: str = self.config_entry.data[CONF_API_KEY]
|
||||
# id -> name
|
||||
self.voices: dict[str, str] = {}
|
||||
self.models: dict[str, str] = {}
|
||||
@@ -168,7 +170,7 @@ class ElevenLabsOptionsFlow(OptionsFlow):
|
||||
vol.Required(CONF_CONFIGURE_VOICE, default=False): bool,
|
||||
}
|
||||
),
|
||||
self.config_entry.options,
|
||||
self.options,
|
||||
)
|
||||
|
||||
async def async_step_voice_settings(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Configflow for the emoncms integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyemoncms import EmoncmsClient
|
||||
@@ -11,7 +9,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -70,7 +68,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> EmoncmsOptionsFlow:
|
||||
) -> OptionsFlowWithConfigEntry:
|
||||
"""Get the options flow for this handler."""
|
||||
return EmoncmsOptionsFlow(config_entry)
|
||||
|
||||
@@ -79,7 +77,6 @@ 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(
|
||||
@@ -92,8 +89,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass, user_input[CONF_URL], user_input[CONF_API_KEY]
|
||||
)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
errors["base"] = result[CONF_MESSAGE]
|
||||
else:
|
||||
self.include_only_feeds = user_input.get(CONF_ONLY_INCLUDE_FEEDID)
|
||||
self.url = user_input[CONF_URL]
|
||||
@@ -117,7 +113,6 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_choose_feeds(
|
||||
@@ -172,36 +167,32 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return result
|
||||
|
||||
|
||||
class EmoncmsOptionsFlow(OptionsFlow):
|
||||
class EmoncmsOptionsFlow(OptionsFlowWithConfigEntry):
|
||||
"""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] = {}
|
||||
description_placeholders = {}
|
||||
include_only_feeds = self.config_entry.options.get(
|
||||
CONF_ONLY_INCLUDE_FEEDID,
|
||||
self.config_entry.data.get(CONF_ONLY_INCLUDE_FEEDID, []),
|
||||
)
|
||||
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, [])
|
||||
options: list = include_only_feeds
|
||||
result = await get_feed_list(self.hass, self._url, self._api_key)
|
||||
result = await get_feed_list(self.hass, url, api_key)
|
||||
if not result[CONF_SUCCESS]:
|
||||
errors["base"] = "api_error"
|
||||
description_placeholders = {"details": result[CONF_MESSAGE]}
|
||||
errors["base"] = 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,
|
||||
},
|
||||
)
|
||||
@@ -216,5 +207,4 @@ class EmoncmsOptionsFlow(OptionsFlow):
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
@@ -138,11 +138,10 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the emoncms sensors."""
|
||||
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)
|
||||
)
|
||||
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)
|
||||
|
||||
if exclude_feeds is None and include_only_feeds is None:
|
||||
return
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"api_error": "An error occured in the pyemoncms API : {details}"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -22,9 +19,6 @@
|
||||
}
|
||||
},
|
||||
"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.3"]
|
||||
"requirements": ["sense-energy==0.13.2"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -66,11 +66,9 @@ 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()
|
||||
return EnvoyOptionsFlowHandler(config_entry)
|
||||
|
||||
@callback
|
||||
def _async_generate_schema(self) -> vol.Schema:
|
||||
@@ -290,7 +288,7 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class EnvoyOptionsFlowHandler(OptionsFlow):
|
||||
class EnvoyOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Envoy config flow options handler."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -257,9 +257,6 @@ 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")
|
||||
|
||||
@@ -570,11 +570,7 @@ def _async_setup_device_registry(
|
||||
configuration_url = None
|
||||
if device_info.webserver_port > 0:
|
||||
configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}"
|
||||
elif (
|
||||
(dashboard := async_get_dashboard(hass))
|
||||
and dashboard.data
|
||||
and dashboard.data.get(device_info.name)
|
||||
):
|
||||
elif dashboard := async_get_dashboard(hass):
|
||||
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
|
||||
|
||||
manufacturer = "espressif"
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"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_payload": "Missing MQTT Payload."
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties."
|
||||
},
|
||||
"error": {
|
||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -45,11 +46,9 @@ 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()
|
||||
return FeedReaderOptionsFlowHandler(config_entry)
|
||||
|
||||
def show_user_form(
|
||||
self,
|
||||
@@ -148,7 +147,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="reconfigure_successful")
|
||||
|
||||
|
||||
class FeedReaderOptionsFlowHandler(OptionsFlow):
|
||||
class FeedReaderOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle an options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -163,9 +162,7 @@ class FeedReaderOptionsFlowHandler(OptionsFlow):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_MAX_ENTRIES,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES
|
||||
),
|
||||
default=self.options.get(CONF_MAX_ENTRIES, DEFAULT_MAX_ENTRIES),
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyfibaro"],
|
||||
"requirements": ["pyfibaro==0.8.0"]
|
||||
"requirements": ["pyfibaro==0.7.8"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Config flow for file integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from typing import Any
|
||||
@@ -13,6 +11,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
@@ -75,11 +74,9 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FileOptionsFlowHandler:
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return FileOptionsFlowHandler()
|
||||
return FileOptionsFlowHandler(config_entry)
|
||||
|
||||
async def validate_file_path(self, file_path: str) -> bool:
|
||||
"""Ensure the file path is valid."""
|
||||
@@ -154,7 +151,7 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(title=title, data=data, options=options)
|
||||
|
||||
|
||||
class FileOptionsFlowHandler(OptionsFlow):
|
||||
class FileOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle File options."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -71,11 +71,9 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FluxLedOptionsFlow:
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for the Flux LED component."""
|
||||
return FluxLedOptionsFlow()
|
||||
return FluxLedOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: dhcp.DhcpServiceInfo
|
||||
@@ -322,6 +320,10 @@ 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:
|
||||
@@ -330,7 +332,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(
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -59,11 +60,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> FritzBoxToolsOptionsFlowHandler:
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return FritzBoxToolsOptionsFlowHandler()
|
||||
return FritzBoxToolsOptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize FRITZ!Box Tools flow."""
|
||||
@@ -394,7 +393,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
||||
class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle an options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -405,18 +404,19 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
||||
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=options.get(
|
||||
default=self.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=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
|
||||
default=self.options.get(
|
||||
CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "fritz",
|
||||
"name": "AVM FRITZ!Box Tools",
|
||||
"codeowners": ["@AaronDavidSchneider", "@chemelli74", "@mib1185"],
|
||||
"codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fritz",
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20241106.0"]
|
||||
"requirements": ["home-assistant-frontend==20241031.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["ayla-iot-unofficial==1.4.3"]
|
||||
"requirements": ["ayla-iot-unofficial==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> GenericOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return GenericOptionsFlowHandler()
|
||||
return GenericOptionsFlowHandler(config_entry)
|
||||
|
||||
def check_for_existing(self, options: dict[str, Any]) -> bool:
|
||||
"""Check whether an existing entry is using the same URLs."""
|
||||
@@ -409,8 +409,9 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class GenericOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Generic IP Camera options."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> 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": ["av==13.1.0", "Pillow==10.4.0"]
|
||||
"requirements": ["ha-av==10.1.1", "Pillow==10.4.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import shutil
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
|
||||
from go2rtc_client.exceptions import Go2RtcClientError
|
||||
from go2rtc_client.ws import (
|
||||
Go2RtcWsClient,
|
||||
ReceiveMessages,
|
||||
@@ -15,7 +15,6 @@ from go2rtc_client.ws import (
|
||||
WsError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
from webrtc_models import RTCIceCandidate
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
Camera,
|
||||
@@ -38,7 +37,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.package import is_docker_env
|
||||
|
||||
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL
|
||||
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN
|
||||
from .server import Server
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -114,18 +113,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
server = Server(
|
||||
hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
|
||||
)
|
||||
try:
|
||||
await server.start()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.warning("Could not start go2rtc server", exc_info=True)
|
||||
return False
|
||||
await server.start()
|
||||
|
||||
async def on_stop(event: Event) -> None:
|
||||
await server.stop()
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
|
||||
|
||||
url = HA_MANAGED_URL
|
||||
url = "http://localhost:1984/"
|
||||
|
||||
hass.data[_DATA_GO2RTC] = url
|
||||
discovery_flow.async_create_flow(
|
||||
@@ -147,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Validate the server URL
|
||||
try:
|
||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
||||
await client.validate_server_version()
|
||||
await client.streams.list()
|
||||
except Go2RtcClientError as err:
|
||||
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -155,10 +150,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
) from err
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
except Go2RtcVersionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"The go2rtc server version is not supported, {err}"
|
||||
) from err
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
@@ -211,21 +202,16 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
self._session, self._url, source=camera.entity_id
|
||||
)
|
||||
|
||||
if not (stream_source := await camera.stream_source()):
|
||||
send_message(
|
||||
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
|
||||
)
|
||||
return
|
||||
|
||||
streams = await self._rest_client.streams.list()
|
||||
|
||||
if (stream := streams.get(camera.entity_id)) is None or not any(
|
||||
stream_source == producer.url for producer in stream.producers
|
||||
):
|
||||
await self._rest_client.streams.add(
|
||||
camera.entity_id,
|
||||
[stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"],
|
||||
)
|
||||
if camera.entity_id not in streams:
|
||||
if not (stream_source := await camera.stream_source()):
|
||||
send_message(
|
||||
WebRTCError(
|
||||
"go2rtc_webrtc_offer_failed", "Camera has no stream source"
|
||||
)
|
||||
)
|
||||
return
|
||||
await self._rest_client.streams.add(camera.entity_id, stream_source)
|
||||
|
||||
@callback
|
||||
def on_messages(message: ReceiveMessages) -> None:
|
||||
@@ -233,7 +219,7 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
value: WebRTCMessage
|
||||
match message:
|
||||
case WebRTCCandidate():
|
||||
value = HAWebRTCCandidate(RTCIceCandidate(message.candidate))
|
||||
value = HAWebRTCCandidate(message.candidate)
|
||||
case WebRTCAnswer():
|
||||
value = HAWebRTCAnswer(message.sdp)
|
||||
case WsError():
|
||||
@@ -245,13 +231,11 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||
config = camera.async_get_webrtc_client_configuration()
|
||||
await ws_client.send(WebRTCOffer(offer_sdp, config.configuration.ice_servers))
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
|
||||
"""Handle the WebRTC candidate."""
|
||||
|
||||
if ws_client := self._sessions.get(session_id):
|
||||
await ws_client.send(WebRTCCandidate(candidate.candidate))
|
||||
await ws_client.send(WebRTCCandidate(candidate))
|
||||
else:
|
||||
_LOGGER.debug("Unknown session %s. Ignoring candidate", session_id)
|
||||
|
||||
|
||||
@@ -4,5 +4,3 @@ DOMAIN = "go2rtc"
|
||||
|
||||
CONF_DEBUG_UI = "debug_ui"
|
||||
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
||||
HA_MANAGED_API_PORT = 11984
|
||||
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["go2rtc-client==0.1.0"],
|
||||
"requirements": ["go2rtc-client==0.0.1b3"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -1,75 +1,40 @@
|
||||
"""Go2rtc server."""
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TERMINATE_TIMEOUT = 5
|
||||
_SETUP_TIMEOUT = 30
|
||||
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
|
||||
_LOCALHOST_IP = "127.0.0.1"
|
||||
_LOG_BUFFER_SIZE = 512
|
||||
_RESPAWN_COOLDOWN = 1
|
||||
|
||||
# Default configuration for HA
|
||||
# - Api is listening only on localhost
|
||||
# - Disable rtsp listener
|
||||
# - Clear default ice servers
|
||||
_GO2RTC_CONFIG_FORMAT = r"""
|
||||
api:
|
||||
listen: "{api_ip}:{api_port}"
|
||||
listen: "{api_ip}:1984"
|
||||
|
||||
rtsp:
|
||||
# ffmpeg needs rtsp for opus audio transcoding
|
||||
listen: "127.0.0.1:18554"
|
||||
listen: "127.0.0.1:8554"
|
||||
|
||||
webrtc:
|
||||
listen: ":18555/tcp"
|
||||
ice_servers: []
|
||||
"""
|
||||
|
||||
_LOG_LEVEL_MAP = {
|
||||
"TRC": logging.DEBUG,
|
||||
"DBG": logging.DEBUG,
|
||||
"INF": logging.DEBUG,
|
||||
"WRN": logging.WARNING,
|
||||
"ERR": logging.WARNING,
|
||||
"FTL": logging.ERROR,
|
||||
"PNC": logging.ERROR,
|
||||
}
|
||||
|
||||
|
||||
class Go2RTCServerStartError(HomeAssistantError):
|
||||
"""Raised when server does not start."""
|
||||
|
||||
_message = "Go2rtc server didn't start correctly"
|
||||
|
||||
|
||||
class Go2RTCWatchdogError(HomeAssistantError):
|
||||
"""Raised on watchdog error."""
|
||||
|
||||
|
||||
def _create_temp_file(api_ip: str) -> str:
|
||||
"""Create temporary config file."""
|
||||
# Set delete=False to prevent the file from being deleted when the file is closed
|
||||
# Linux is clearing tmp folder on reboot, so no need to delete it manually
|
||||
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
|
||||
file.write(
|
||||
_GO2RTC_CONFIG_FORMAT.format(
|
||||
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
|
||||
).encode()
|
||||
)
|
||||
file.write(_GO2RTC_CONFIG_FORMAT.format(api_ip=api_ip).encode())
|
||||
return file.name
|
||||
|
||||
|
||||
@@ -82,24 +47,14 @@ class Server:
|
||||
"""Initialize the server."""
|
||||
self._hass = hass
|
||||
self._binary = binary
|
||||
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
|
||||
self._process: asyncio.subprocess.Process | None = None
|
||||
self._startup_complete = asyncio.Event()
|
||||
self._api_ip = _LOCALHOST_IP
|
||||
if enable_ui:
|
||||
# Listen on all interfaces for allowing access from all ips
|
||||
self._api_ip = ""
|
||||
self._watchdog_task: asyncio.Task | None = None
|
||||
self._watchdog_tasks: list[asyncio.Task] = []
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the server."""
|
||||
await self._start()
|
||||
self._watchdog_task = asyncio.create_task(
|
||||
self._watchdog(), name="Go2rtc respawn"
|
||||
)
|
||||
|
||||
async def _start(self) -> None:
|
||||
"""Start the server."""
|
||||
_LOGGER.debug("Starting go2rtc server")
|
||||
config_file = await self._hass.async_add_executor_job(
|
||||
@@ -127,13 +82,8 @@ class Server:
|
||||
except TimeoutError as err:
|
||||
msg = "Go2rtc server didn't start correctly"
|
||||
_LOGGER.exception(msg)
|
||||
self._log_server_output(logging.WARNING)
|
||||
await self._stop()
|
||||
raise Go2RTCServerStartError from err
|
||||
|
||||
# Check the server version
|
||||
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
|
||||
await client.validate_server_version()
|
||||
await self.stop()
|
||||
raise HomeAssistantError("Go2rtc server didn't start correctly") from err
|
||||
|
||||
async def _log_output(self, process: asyncio.subprocess.Process) -> None:
|
||||
"""Log the output of the process."""
|
||||
@@ -141,111 +91,21 @@ class Server:
|
||||
|
||||
async for line in process.stdout:
|
||||
msg = line[:-1].decode().strip()
|
||||
self._log_buffer.append(msg)
|
||||
loglevel = logging.WARNING
|
||||
if len(split_msg := msg.split(" ", 2)) == 3:
|
||||
loglevel = _LOG_LEVEL_MAP.get(split_msg[1], loglevel)
|
||||
_LOGGER.log(loglevel, msg)
|
||||
_LOGGER.debug(msg)
|
||||
if not self._startup_complete.is_set() and _SUCCESSFUL_BOOT_MESSAGE in msg:
|
||||
self._startup_complete.set()
|
||||
|
||||
def _log_server_output(self, loglevel: int) -> None:
|
||||
"""Log captured process output, then clear the log buffer."""
|
||||
for line in list(self._log_buffer): # Copy the deque to avoid mutation error
|
||||
_LOGGER.log(loglevel, line)
|
||||
self._log_buffer.clear()
|
||||
|
||||
async def _watchdog(self) -> None:
|
||||
"""Keep respawning go2rtc servers.
|
||||
|
||||
A new go2rtc server is spawned if the process terminates or the API
|
||||
stops responding.
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
monitor_process_task = asyncio.create_task(self._monitor_process())
|
||||
self._watchdog_tasks.append(monitor_process_task)
|
||||
monitor_process_task.add_done_callback(self._watchdog_tasks.remove)
|
||||
monitor_api_task = asyncio.create_task(self._monitor_api())
|
||||
self._watchdog_tasks.append(monitor_api_task)
|
||||
monitor_api_task.add_done_callback(self._watchdog_tasks.remove)
|
||||
try:
|
||||
await asyncio.gather(monitor_process_task, monitor_api_task)
|
||||
except Go2RTCWatchdogError:
|
||||
_LOGGER.debug("Caught Go2RTCWatchdogError")
|
||||
for task in self._watchdog_tasks:
|
||||
if task.done():
|
||||
if not task.cancelled():
|
||||
task.exception()
|
||||
continue
|
||||
task.cancel()
|
||||
await asyncio.sleep(_RESPAWN_COOLDOWN)
|
||||
try:
|
||||
await self._stop()
|
||||
_LOGGER.warning("Go2rtc unexpectedly stopped, server log:")
|
||||
self._log_server_output(logging.WARNING)
|
||||
_LOGGER.debug("Spawning new go2rtc server")
|
||||
with suppress(Go2RTCServerStartError):
|
||||
await self._start()
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error when restarting go2rtc server"
|
||||
)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error in go2rtc server watchdog")
|
||||
|
||||
async def _monitor_process(self) -> None:
|
||||
"""Raise if the go2rtc process terminates."""
|
||||
_LOGGER.debug("Monitoring go2rtc server process")
|
||||
if self._process:
|
||||
await self._process.wait()
|
||||
_LOGGER.debug("go2rtc server terminated")
|
||||
raise Go2RTCWatchdogError("Process ended")
|
||||
|
||||
async def _monitor_api(self) -> None:
|
||||
"""Raise if the go2rtc process terminates."""
|
||||
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
|
||||
|
||||
_LOGGER.debug("Monitoring go2rtc API")
|
||||
try:
|
||||
while True:
|
||||
await client.validate_server_version()
|
||||
await asyncio.sleep(10)
|
||||
except Exception as err:
|
||||
_LOGGER.debug("go2rtc API did not reply", exc_info=True)
|
||||
raise Go2RTCWatchdogError("API error") from err
|
||||
|
||||
async def _stop_watchdog(self) -> None:
|
||||
"""Handle watchdog stop request."""
|
||||
tasks: list[asyncio.Task] = []
|
||||
if watchdog_task := self._watchdog_task:
|
||||
self._watchdog_task = None
|
||||
tasks.append(watchdog_task)
|
||||
watchdog_task.cancel()
|
||||
for task in self._watchdog_tasks:
|
||||
tasks.append(task)
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the server and abort the watchdog task."""
|
||||
_LOGGER.debug("Server stop requested")
|
||||
await self._stop_watchdog()
|
||||
await self._stop()
|
||||
|
||||
async def _stop(self) -> None:
|
||||
"""Stop the server."""
|
||||
if self._process:
|
||||
_LOGGER.debug("Stopping go2rtc server")
|
||||
process = self._process
|
||||
self._process = None
|
||||
with suppress(ProcessLookupError):
|
||||
process.terminate()
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=_TERMINATE_TIMEOUT)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Go2rtc server didn't terminate gracefully. Killing it")
|
||||
with suppress(ProcessLookupError):
|
||||
process.kill()
|
||||
process.kill()
|
||||
else:
|
||||
_LOGGER.debug("Go2rtc server has been stopped")
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
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()
|
||||
return GoogleCloudOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class GoogleCloudOptionsFlowHandler(OptionsFlow):
|
||||
class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Google Cloud options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -169,7 +169,7 @@ class GoogleCloudOptionsFlowHandler(OptionsFlow):
|
||||
)
|
||||
),
|
||||
**tts_options_schema(
|
||||
self.config_entry.options, voices, from_config_flow=True
|
||||
self.options, voices, from_config_flow=True
|
||||
).schema,
|
||||
vol.Optional(
|
||||
CONF_STT_MODEL,
|
||||
@@ -182,6 +182,6 @@ class GoogleCloudOptionsFlowHandler(OptionsFlow):
|
||||
),
|
||||
}
|
||||
),
|
||||
self.config_entry.options,
|
||||
self.options,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_tts_voices(
|
||||
|
||||
|
||||
def tts_options_schema(
|
||||
config_options: Mapping[str, Any],
|
||||
config_options: dict[str, Any],
|
||||
voices: dict[str, list[str]],
|
||||
from_config_flow: bool = False,
|
||||
) -> vol.Schema:
|
||||
|
||||
@@ -163,6 +163,7 @@ 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
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CALENDAR,
|
||||
Platform.SENSOR,
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
"""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
|
||||
@@ -59,9 +59,9 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
tasks_response.extend(await self.api.tasks.user.get(type="completedTodos"))
|
||||
except ClientResponseError as error:
|
||||
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
_LOGGER.debug("Rate limit exceeded, will try again later")
|
||||
_LOGGER.debug("Currently rate limited, skipping update")
|
||||
return self.data
|
||||
raise UpdateFailed(f"Unable to connect to Habitica: {error}") from error
|
||||
raise UpdateFailed(f"Error communicating with API: {error}") from error
|
||||
|
||||
return HabiticaData(user=user_response, tasks=tasks_response)
|
||||
|
||||
|
||||
@@ -135,14 +135,6 @@
|
||||
"on": "mdi:sleep"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"pending_quest": {
|
||||
"default": "mdi:script-outline",
|
||||
"state": {
|
||||
"on": "mdi:script-text-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -9,7 +9,6 @@ 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,
|
||||
@@ -55,21 +54,6 @@ 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."""
|
||||
|
||||
@@ -102,7 +86,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def cast_skill(call: ServiceCall) -> ServiceResponse:
|
||||
"""Skill action."""
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
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",
|
||||
)
|
||||
coordinator = entry.runtime_data
|
||||
skill = {
|
||||
"pickpocket": {"spellId": "pickPocket", "cost": "10 MP"},
|
||||
|
||||
@@ -38,11 +38,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"pending_quest": {
|
||||
"name": "Pending quest invitation"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"run_cron": {
|
||||
"name": "Start my day"
|
||||
@@ -209,10 +204,10 @@
|
||||
"message": "Unable to create new to-do `{name}` for Habitica, please try again"
|
||||
},
|
||||
"setup_rate_limit_exception": {
|
||||
"message": "Rate limit exceeded, try again later"
|
||||
"message": "Currently rate limited, try again later"
|
||||
},
|
||||
"service_call_unallowed": {
|
||||
"message": "Unable to complete action, the required conditions are not met"
|
||||
"message": "Unable to carry out this action, because the required conditions are not met"
|
||||
},
|
||||
"service_call_exception": {
|
||||
"message": "Unable to connect to Habitica, try again later"
|
||||
@@ -224,10 +219,7 @@
|
||||
"message": "Unable to cast skill, your character does not have the skill or spell {skill}."
|
||||
},
|
||||
"entry_not_found": {
|
||||
"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."
|
||||
"message": "The selected character is currently not configured or loaded in Home Assistant."
|
||||
},
|
||||
"task_not_found": {
|
||||
"message": "Unable to cast skill, could not find the task {task}"
|
||||
|
||||
@@ -103,7 +103,6 @@ PLACEHOLDER_KEY_ADDON_URL = "addon_url"
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
PLACEHOLDER_KEY_COMPONENTS = "components"
|
||||
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL = "issue_addon_boot_fail"
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
@@ -137,3 +136,17 @@ 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"
|
||||
|
||||
@@ -131,11 +131,11 @@ class HassIODiscovery(HomeAssistantView):
|
||||
config=data.config,
|
||||
name=addon_info.name,
|
||||
slug=data.addon,
|
||||
uuid=data.uuid.hex,
|
||||
uuid=str(data.uuid),
|
||||
),
|
||||
discovery_key=discovery_flow.DiscoveryKey(
|
||||
domain=DOMAIN,
|
||||
key=data.uuid.hex,
|
||||
key=str(data.uuid),
|
||||
version=1,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -91,6 +91,15 @@ 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."""
|
||||
@@ -236,6 +245,26 @@ 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
|
||||
@@ -275,6 +304,14 @@ 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,10 +7,6 @@ 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
|
||||
@@ -24,8 +20,12 @@ 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,
|
||||
@@ -36,7 +36,6 @@ from .const import (
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
@@ -45,9 +44,10 @@ from .const import (
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .coordinator import get_addons_info
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
from .handler import HassIO, HassioAPIError
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
@@ -94,7 +94,6 @@ UNHEALTHY_REASONS = {
|
||||
|
||||
# Keys (type + context) of issues that when found should be made into a repair
|
||||
ISSUE_KEYS_FOR_REPAIRS = {
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
"issue_mount_mount_failed",
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
@@ -119,9 +118,9 @@ class SuggestionDataType(TypedDict):
|
||||
class Suggestion:
|
||||
"""Suggestion from Supervisor which resolves an issue."""
|
||||
|
||||
uuid: UUID
|
||||
uuid: str
|
||||
type: str
|
||||
context: ContextType
|
||||
context: SupervisorIssueContext
|
||||
reference: str | None = None
|
||||
|
||||
@property
|
||||
@@ -133,9 +132,9 @@ class Suggestion:
|
||||
def from_dict(cls, data: SuggestionDataType) -> Suggestion:
|
||||
"""Convert from dictionary representation."""
|
||||
return cls(
|
||||
uuid=UUID(data["uuid"]),
|
||||
uuid=data["uuid"],
|
||||
type=data["type"],
|
||||
context=ContextType(data["context"]),
|
||||
context=SupervisorIssueContext(data["context"]),
|
||||
reference=data["reference"],
|
||||
)
|
||||
|
||||
@@ -154,9 +153,9 @@ class IssueDataType(TypedDict):
|
||||
class Issue:
|
||||
"""Issue from Supervisor."""
|
||||
|
||||
uuid: UUID
|
||||
uuid: str
|
||||
type: str
|
||||
context: ContextType
|
||||
context: SupervisorIssueContext
|
||||
reference: str | None = None
|
||||
suggestions: list[Suggestion] = field(default_factory=list, compare=False)
|
||||
|
||||
@@ -170,9 +169,9 @@ class Issue:
|
||||
"""Convert from dictionary representation."""
|
||||
suggestions: list[SuggestionDataType] = data.get("suggestions", [])
|
||||
return cls(
|
||||
uuid=UUID(data["uuid"]),
|
||||
uuid=data["uuid"],
|
||||
type=data["type"],
|
||||
context=ContextType(data["context"]),
|
||||
context=SupervisorIssueContext(data["context"]),
|
||||
reference=data["reference"],
|
||||
suggestions=[
|
||||
Suggestion.from_dict(suggestion) for suggestion in suggestions
|
||||
@@ -189,8 +188,7 @@ class SupervisorIssues:
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[UUID, Issue] = {}
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
self._issues: dict[str, Issue] = {}
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
@@ -283,7 +281,7 @@ class SupervisorIssues:
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
issue.uuid.hex,
|
||||
issue.uuid,
|
||||
is_fixable=bool(issue.suggestions),
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue.key,
|
||||
@@ -292,37 +290,19 @@ class SupervisorIssues:
|
||||
|
||||
self._issues[issue.uuid] = issue
|
||||
|
||||
async def add_issue_from_data(self, data: SupervisorIssue) -> None:
|
||||
async def add_issue_from_data(self, data: IssueDataType) -> None:
|
||||
"""Add issue from data to list after getting latest suggestions."""
|
||||
try:
|
||||
suggestions = (
|
||||
await self._supervisor_client.resolution.suggestions_for_issue(
|
||||
data.uuid
|
||||
)
|
||||
)
|
||||
except SupervisorError:
|
||||
data["suggestions"] = (
|
||||
await self._client.get_suggestions_for_issue(data["uuid"])
|
||||
)[ATTR_SUGGESTIONS]
|
||||
except HassioAPIError:
|
||||
_LOGGER.error(
|
||||
"Could not get suggestions for supervisor issue %s, skipping it",
|
||||
data.uuid.hex,
|
||||
data["uuid"],
|
||||
)
|
||||
return
|
||||
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
|
||||
],
|
||||
)
|
||||
)
|
||||
self.add_issue(Issue.from_dict(data))
|
||||
|
||||
def remove_issue(self, issue: Issue) -> None:
|
||||
"""Remove an issue from the list. Delete a repair if necessary."""
|
||||
@@ -330,13 +310,13 @@ class SupervisorIssues:
|
||||
return
|
||||
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid)
|
||||
|
||||
del self._issues[issue.uuid]
|
||||
|
||||
def get_issue(self, issue_id: str) -> Issue | None:
|
||||
"""Get issue from key."""
|
||||
return self._issues.get(UUID(issue_id))
|
||||
return self._issues.get(issue_id)
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
@@ -349,8 +329,8 @@ class SupervisorIssues:
|
||||
async def _update(self, _: datetime | None = None) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
try:
|
||||
data = await self._supervisor_client.resolution.info()
|
||||
except SupervisorError as err:
|
||||
data = await self._client.get_resolution_info()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.error("Failed to update supervisor issues: %r", err)
|
||||
async_call_later(
|
||||
self._hass,
|
||||
@@ -358,16 +338,18 @@ class SupervisorIssues:
|
||||
HassJob(self._update, cancel_on_shutdown=True),
|
||||
)
|
||||
return
|
||||
self.unhealthy_reasons = set(data.unhealthy)
|
||||
self.unsupported_reasons = set(data.unsupported)
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
# Remove any cached issues that weren't returned
|
||||
for issue_id in set(self._issues) - {issue.uuid for issue in data.issues}:
|
||||
for issue_id in set(self._issues.keys()) - {
|
||||
issue["uuid"] for issue in data[ATTR_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.issues]
|
||||
*[self.add_issue_from_data(issue) for issue in data[ATTR_ISSUES]]
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -6,8 +6,6 @@ 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
|
||||
@@ -16,14 +14,14 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import get_addons_info, get_issues_info
|
||||
from .const import (
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_COMPONENTS,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .handler import get_supervisor_client
|
||||
from .handler import async_apply_suggestion
|
||||
from .issues import Issue, Suggestion
|
||||
|
||||
HELP_URLS = {
|
||||
@@ -52,10 +50,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
_data: dict[str, Any] | None = None
|
||||
_issue: Issue | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant, issue_id: str) -> None:
|
||||
def __init__(self, issue_id: str) -> None:
|
||||
"""Initialize repair flow."""
|
||||
self._issue_id = issue_id
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
@@ -126,12 +123,9 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
|
||||
return self._async_form_for_suggestion(suggestion)
|
||||
|
||||
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={})
|
||||
if await async_apply_suggestion(self.hass, suggestion.uuid):
|
||||
return self.async_create_entry(data={})
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
|
||||
@staticmethod
|
||||
def _async_step(
|
||||
@@ -168,9 +162,9 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
if issue.key == self.issue.key or issue.type != self.issue.type:
|
||||
continue
|
||||
|
||||
if issue.context == ContextType.CORE:
|
||||
if issue.context == SupervisorIssueContext.CORE:
|
||||
components.insert(0, "Home Assistant")
|
||||
elif issue.context == ContextType.ADDON:
|
||||
elif issue.context == SupervisorIssueContext.ADDON:
|
||||
components.append(
|
||||
next(
|
||||
(
|
||||
@@ -187,8 +181,8 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
return placeholders
|
||||
|
||||
|
||||
class AddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for addon issue fixing flows."""
|
||||
class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for detached addon issue fixing flows."""
|
||||
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
@@ -215,11 +209,8 @@ 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(hass, issue_id)
|
||||
if issue and issue.key in {
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
}:
|
||||
return AddonIssueRepairFlow(hass, issue_id)
|
||||
return DockerConfigIssueRepairFlow(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
|
||||
return DetachedAddonIssueRepairFlow(issue_id)
|
||||
|
||||
return SupervisorIssueRepairFlow(hass, issue_id)
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
||||
|
||||
@@ -17,23 +17,6 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_addon_boot_fail": {
|
||||
"title": "Add-on failed to start at boot",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"fix_menu": {
|
||||
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
|
||||
"menu_options": {
|
||||
"addon_execute_start": "Start",
|
||||
"addon_disable_boot": "Disable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not apply the fix. Check the Supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_addon_detached_addon_missing": {
|
||||
"title": "Missing repository for an installed add-on",
|
||||
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
|
||||
|
||||
@@ -113,7 +113,7 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> HERETravelTimeOptionsFlow:
|
||||
"""Get the options flow."""
|
||||
return HERETravelTimeOptionsFlow()
|
||||
return HERETravelTimeOptionsFlow(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -297,8 +297,9 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class HERETravelTimeOptionsFlow(OptionsFlow):
|
||||
"""Handle HERE Travel Time options."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize HERE Travel Time options flow."""
|
||||
self.config_entry = config_entry
|
||||
self._config: dict[str, Any] = {}
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -182,6 +182,7 @@ 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(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.60", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.59", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .api import HomeConnectDevice
|
||||
from .const import (
|
||||
@@ -210,9 +206,3 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
"items": "\n".join([f"- {item}" for item in items]),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
|
||||
)
|
||||
|
||||
@@ -37,8 +37,11 @@
|
||||
"set_light_color": {
|
||||
"message": "Error while trying to set color of {entity_id}: {description}"
|
||||
},
|
||||
"set_light_effect": {
|
||||
"message": "Error while trying to set effect of {entity_id}: {description}"
|
||||
},
|
||||
"set_setting": {
|
||||
"message": "Error while trying to assign the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
|
||||
"message": "Error while trying to set \"{value}\" to \"{key}\" setting for {entity_id}: {description}"
|
||||
},
|
||||
"turn_on": {
|
||||
"message": "Error while trying to turn on {entity_id} ({key}): {description}"
|
||||
|
||||
@@ -362,14 +362,15 @@ class HomeKitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for homekit."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.hk_options: dict[str, Any] = {}
|
||||
self.included_cameras: list[str] = []
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.9.2",
|
||||
"HAP-python==4.9.1",
|
||||
"fnv-hash-fast==1.0.2",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
||||
@@ -420,7 +420,6 @@ 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."""
|
||||
@@ -452,7 +451,6 @@ 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."""
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Could not connect to the controller.",
|
||||
"credentials_needed": "The controller needs credentials.",
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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%]"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user