mirror of
https://github.com/home-assistant/core.git
synced 2026-01-17 04:56:59 +01:00
Compare commits
146 Commits
multi_doma
...
2026.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae17b620f6 | ||
|
|
c5b72ac286 | ||
|
|
5f6dce5503 | ||
|
|
6dd8692bb8 | ||
|
|
16f4849e88 | ||
|
|
04809e47f1 | ||
|
|
691cf67f68 | ||
|
|
c2e1646473 | ||
|
|
9ac5560c41 | ||
|
|
68cbdcf3c9 | ||
|
|
6474a1bf63 | ||
|
|
572092d362 | ||
|
|
01ea5a1634 | ||
|
|
b202c8b43e | ||
|
|
79fd98753a | ||
|
|
447da083c0 | ||
|
|
0643b36ed5 | ||
|
|
4ee3ac16af | ||
|
|
6824f38c68 | ||
|
|
b8f23bb388 | ||
|
|
e238d67818 | ||
|
|
992a9bdd3b | ||
|
|
ceaae1c1cc | ||
|
|
1c163c92dc | ||
|
|
a42aa9372c | ||
|
|
013592bd54 | ||
|
|
2101bae095 | ||
|
|
cfa1107135 | ||
|
|
a269ef660a | ||
|
|
c43c4f17e9 | ||
|
|
de25e6af51 | ||
|
|
18d3629b6c | ||
|
|
50c477a408 | ||
|
|
ea9cd7d905 | ||
|
|
2bf4ac20ea | ||
|
|
94ff881897 | ||
|
|
2975b3c1b9 | ||
|
|
0143c4ff85 | ||
|
|
f59566d20b | ||
|
|
395f0ad2a7 | ||
|
|
2af1fc6759 | ||
|
|
c1e7122d1c | ||
|
|
e5624b1224 | ||
|
|
6e380bafca | ||
|
|
bb9fd94430 | ||
|
|
07bc5d5c6b | ||
|
|
651b7116dd | ||
|
|
34438bd039 | ||
|
|
7b53b8691c | ||
|
|
8748d6f200 | ||
|
|
8d95511650 | ||
|
|
9aa5953a86 | ||
|
|
5ccdfda747 | ||
|
|
00ad44cb91 | ||
|
|
b7519cd880 | ||
|
|
ac44769539 | ||
|
|
9e95b80805 | ||
|
|
50086ca5c7 | ||
|
|
49086b2a76 | ||
|
|
1f28fe9933 | ||
|
|
4465aa264c | ||
|
|
2c1bc96161 | ||
|
|
7127159a5b | ||
|
|
9f0eb6f077 | ||
|
|
da19cc06e3 | ||
|
|
fd92377cf2 | ||
|
|
c201938b8b | ||
|
|
b3765204b1 | ||
|
|
786257e051 | ||
|
|
9559634151 | ||
|
|
cf12ed8f08 | ||
|
|
e213f49c75 | ||
|
|
09c7cc113a | ||
|
|
e1e7e039a9 | ||
|
|
05a0f0d23f | ||
|
|
d3853019eb | ||
|
|
ccbaac55b3 | ||
|
|
771292ced9 | ||
|
|
5d4262e8b3 | ||
|
|
d96da9a639 | ||
|
|
288a805d0f | ||
|
|
8e55ceea77 | ||
|
|
14f1d9fbad | ||
|
|
eb6582bc24 | ||
|
|
4afe67f33d | ||
|
|
5d7b10f569 | ||
|
|
340c2e48df | ||
|
|
86257b1865 | ||
|
|
eea1adccfd | ||
|
|
242be14f88 | ||
|
|
7e013b723d | ||
|
|
4d55939f53 | ||
|
|
e5e7546d49 | ||
|
|
e560795d04 | ||
|
|
15b0342bd7 | ||
|
|
8d05a5f3d4 | ||
|
|
358ad29b59 | ||
|
|
5c4f99b828 | ||
|
|
b3f123c715 | ||
|
|
85c2351af2 | ||
|
|
ec19529c99 | ||
|
|
d5ebd02afe | ||
|
|
37d82ab795 | ||
|
|
5d08481137 | ||
|
|
0861b7541d | ||
|
|
abf7078842 | ||
|
|
c4012fae4e | ||
|
|
d6082ab6c3 | ||
|
|
77367e415f | ||
|
|
6c006c68c1 | ||
|
|
026fdeb4ce | ||
|
|
1034218e6e | ||
|
|
a21062f502 | ||
|
|
2e157f1bc6 | ||
|
|
a697e63b8c | ||
|
|
d28d55c7db | ||
|
|
8863488286 | ||
|
|
53cfdef1ac | ||
|
|
42ea7ecbd6 | ||
|
|
d58d08c350 | ||
|
|
65a259b9df | ||
|
|
cbfbfbee13 | ||
|
|
e503b37ddc | ||
|
|
217eef39f3 | ||
|
|
dcdbce9b21 | ||
|
|
71db8fe185 | ||
|
|
9b96cb66d5 | ||
|
|
78bccbbbc2 | ||
|
|
b0a8f9575c | ||
|
|
61104a9970 | ||
|
|
8d13dbdd0c | ||
|
|
9afb41004e | ||
|
|
cdd542f6e6 | ||
|
|
f520686002 | ||
|
|
e4d09bb615 | ||
|
|
10f6ccf6cc | ||
|
|
d9fa67b16f | ||
|
|
cf228ae02b | ||
|
|
cb4d62ab9a | ||
|
|
d2f75aec04 | ||
|
|
a609fbc07b | ||
|
|
1b9c7ae0ac | ||
|
|
492f2117fb | ||
|
|
2346f83635 | ||
|
|
8925bfb182 | ||
|
|
8f2b1f0eff |
@@ -13,5 +13,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyairobotrest==0.1.0"]
|
||||
"requirements": ["pyairobotrest==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -85,6 +85,22 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
value = system[key]
|
||||
return value
|
||||
|
||||
async def _async_update_sys_params(self, params: dict[str, Any]) -> None:
|
||||
"""Send system parameters to API."""
|
||||
_params = {
|
||||
API_SYSTEM_ID: self.system_id,
|
||||
**params,
|
||||
}
|
||||
_LOGGER.debug("update_sys_params=%s", _params)
|
||||
try:
|
||||
await self.coordinator.airzone.set_sys_parameters(_params)
|
||||
except AirzoneError as error:
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set system {self.entity_id}: {error}"
|
||||
) from error
|
||||
|
||||
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
|
||||
|
||||
|
||||
class AirzoneHotWaterEntity(AirzoneEntity):
|
||||
"""Define an Airzone Hot Water entity."""
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.4"]
|
||||
"requirements": ["aioairzone==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ from aioairzone.const import (
|
||||
AZD_MODES,
|
||||
AZD_Q_ADAPT,
|
||||
AZD_SLEEP,
|
||||
AZD_SYSTEMS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -85,14 +86,7 @@ def main_zone_options(
|
||||
return [k for k, v in options.items() if v in modes]
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
key=AZD_MODE,
|
||||
options_dict=MODE_DICT,
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_Q_ADAPT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -104,6 +98,17 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
)
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
key=AZD_MODE,
|
||||
options_dict=MODE_DICT,
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_COLD_ANGLE,
|
||||
@@ -140,16 +145,37 @@ async def async_setup_entry(
|
||||
"""Add Airzone select from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_systems: set[str] = set()
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of select."""
|
||||
|
||||
entities: list[AirzoneBaseSelect] = []
|
||||
|
||||
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
|
||||
received_systems = set(systems_data)
|
||||
new_systems = received_systems - added_systems
|
||||
if new_systems:
|
||||
entities.extend(
|
||||
AirzoneSystemSelect(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_id,
|
||||
systems_data.get(system_id),
|
||||
)
|
||||
for system_id in new_systems
|
||||
for description in SYSTEM_SELECT_TYPES
|
||||
if description.key in systems_data.get(system_id)
|
||||
)
|
||||
added_systems.update(new_systems)
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
entities: list[AirzoneZoneSelect] = [
|
||||
entities.extend(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -161,8 +187,8 @@ async def async_setup_entry(
|
||||
for description in MAIN_ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
|
||||
]
|
||||
entities += [
|
||||
)
|
||||
entities.extend(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -173,10 +199,11 @@ async def async_setup_entry(
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
]
|
||||
async_add_entities(entities)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
@@ -203,6 +230,38 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||
self._attr_current_option = self._get_current_option()
|
||||
|
||||
|
||||
class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone System select."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneSelectDescription,
|
||||
entry: ConfigEntry,
|
||||
system_id: str,
|
||||
system_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_data)
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_options = self.entity_description.options_fn(
|
||||
system_data, description.options_dict
|
||||
)
|
||||
|
||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
param = self.entity_description.api_param
|
||||
value = self.entity_description.options_dict[option]
|
||||
await self._async_update_sys_params({param: value})
|
||||
|
||||
|
||||
class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
||||
"""Define an Airzone Zone select."""
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==10.0.0"]
|
||||
"requirements": ["aioamazondevices==11.0.2"]
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, llm
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
@@ -193,7 +194,7 @@ def _convert_content(
|
||||
tool_result_block = ToolResultBlockParam(
|
||||
type="tool_result",
|
||||
tool_use_id=content.tool_call_id,
|
||||
content=json.dumps(content.tool_result),
|
||||
content=json_dumps(content.tool_result),
|
||||
)
|
||||
external_tool = False
|
||||
if not messages or messages[-1]["role"] != (
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import math
|
||||
|
||||
from pysilero_vad import SileroVoiceActivityDetector
|
||||
from pymicro_vad import MicroVad
|
||||
from pyspeex_noise import AudioProcessor
|
||||
|
||||
from .const import BYTES_PER_CHUNK
|
||||
@@ -43,8 +42,8 @@ class AudioEnhancer(ABC):
|
||||
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
|
||||
|
||||
class SileroVadSpeexEnhancer(AudioEnhancer):
|
||||
"""Audio enhancer that runs Silero VAD and speex."""
|
||||
class MicroVadSpeexEnhancer(AudioEnhancer):
|
||||
"""Audio enhancer that runs microVAD and speex."""
|
||||
|
||||
def __init__(
|
||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||
@@ -70,49 +69,21 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
|
||||
self.noise_suppression,
|
||||
)
|
||||
|
||||
self.vad: SileroVoiceActivityDetector | None = None
|
||||
|
||||
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
|
||||
# buffer audio. The previous speech probability is used until enough
|
||||
# audio has been buffered.
|
||||
self._vad_buffer: bytearray | None = None
|
||||
self._vad_buffer_chunks = 0
|
||||
self._vad_buffer_chunk_idx = 0
|
||||
self._last_speech_probability: float | None = None
|
||||
self.vad: MicroVad | None = None
|
||||
|
||||
if self.is_vad_enabled:
|
||||
self.vad = SileroVoiceActivityDetector()
|
||||
|
||||
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
|
||||
self._vad_buffer_chunks = int(
|
||||
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
|
||||
)
|
||||
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
|
||||
self._vad_buffer = bytearray(self.vad.chunk_bytes())
|
||||
_LOGGER.debug("Initialized Silero VAD")
|
||||
self.vad = MicroVad()
|
||||
_LOGGER.debug("Initialized microVAD")
|
||||
|
||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||
speech_probability: float | None = None
|
||||
|
||||
assert len(audio) == BYTES_PER_CHUNK
|
||||
|
||||
if self.vad is not None:
|
||||
# Run VAD
|
||||
assert self._vad_buffer is not None
|
||||
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
|
||||
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
|
||||
|
||||
self._vad_buffer_chunk_idx += 1
|
||||
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
|
||||
# We have enough data to run Silero VAD (32 ms)
|
||||
self._last_speech_probability = self.vad.process_chunk(
|
||||
self._vad_buffer[: self.vad.chunk_bytes()]
|
||||
)
|
||||
|
||||
# Copy leftover audio that wasn't processed to start
|
||||
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
|
||||
-self._vad_leftover_bytes :
|
||||
]
|
||||
self._vad_buffer_chunk_idx = 0
|
||||
speech_probability = self.vad.Process10ms(audio)
|
||||
|
||||
if self.audio_processor is not None:
|
||||
# Run noise suppression and auto gain
|
||||
@@ -121,5 +92,5 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
|
||||
return EnhancedAudioChunk(
|
||||
audio=audio,
|
||||
timestamp_ms=timestamp_ms,
|
||||
speech_probability=self._last_speech_probability,
|
||||
speech_probability=speech_probability,
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
||||
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ from homeassistant.util import (
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||
|
||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer
|
||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
|
||||
from .const import (
|
||||
ACKNOWLEDGE_PATH,
|
||||
BYTES_PER_CHUNK,
|
||||
@@ -633,7 +633,7 @@ class PipelineRun:
|
||||
# Initialize with audio settings
|
||||
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
||||
# Default audio enhancer
|
||||
self.audio_enhancer = SileroVadSpeexEnhancer(
|
||||
self.audio_enhancer = MicroVadSpeexEnhancer(
|
||||
self.audio_settings.auto_gain_dbfs,
|
||||
self.audio_settings.noise_suppression_level,
|
||||
self.audio_settings.is_vad_enabled,
|
||||
|
||||
@@ -7,7 +7,7 @@ import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Protocol, cast
|
||||
from typing import Any, Literal, Protocol, cast
|
||||
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
@@ -16,7 +16,10 @@ from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.components.labs import async_listen as async_labs_listen
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FLOOR_ID,
|
||||
ATTR_LABEL_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ACTIONS,
|
||||
@@ -30,6 +33,7 @@ from homeassistant.const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TARGET,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
@@ -588,20 +592,32 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._async_detach_triggers is not None or self._is_enabled
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def referenced_labels(self) -> set[str]:
|
||||
"""Return a set of referenced labels."""
|
||||
return self.action_script.referenced_labels
|
||||
referenced = self.action_script.referenced_labels
|
||||
|
||||
@property
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
|
||||
return referenced
|
||||
|
||||
@cached_property
|
||||
def referenced_floors(self) -> set[str]:
|
||||
"""Return a set of referenced floors."""
|
||||
return self.action_script.referenced_floors
|
||||
referenced = self.action_script.referenced_floors
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
|
||||
return referenced
|
||||
|
||||
@cached_property
|
||||
def referenced_areas(self) -> set[str]:
|
||||
"""Return a set of referenced areas."""
|
||||
return self.action_script.referenced_areas
|
||||
referenced = self.action_script.referenced_areas
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
|
||||
return referenced
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
@@ -1209,6 +1225,9 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
|
||||
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
|
||||
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
|
||||
|
||||
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
|
||||
return target_devices
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@@ -1239,9 +1258,28 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
|
||||
|
||||
if target_entities := _get_targets_from_trigger_config(
|
||||
trigger_conf, CONF_ENTITY_ID
|
||||
):
|
||||
return target_entities
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
def _get_targets_from_trigger_config(
|
||||
config: dict,
|
||||
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
|
||||
) -> list[str]:
|
||||
"""Extract targets from a target config."""
|
||||
if not (target_conf := config.get(CONF_TARGET)):
|
||||
return []
|
||||
if not (targets := target_conf.get(target)):
|
||||
return []
|
||||
|
||||
return [targets] if isinstance(targets, str) else targets
|
||||
|
||||
|
||||
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
|
||||
def websocket_config(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -36,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Cache TTL for backup list (in seconds)
|
||||
CACHE_TTL = 300
|
||||
|
||||
# Timeout for upload operations (in seconds)
|
||||
# This prevents uploads from hanging indefinitely
|
||||
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
|
||||
|
||||
|
||||
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata files."""
|
||||
@@ -329,13 +333,28 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug("Uploading backup file %s with streaming", filename)
|
||||
try:
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
file_version = await self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
file_version = await asyncio.wait_for(
|
||||
self._hass.async_add_executor_job(
|
||||
self._upload_unbound_stream_sync,
|
||||
reader,
|
||||
filename,
|
||||
content_type or "application/x-tar",
|
||||
file_info,
|
||||
),
|
||||
timeout=UPLOAD_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
|
||||
)
|
||||
reader.abort()
|
||||
raise BackupAgentError(
|
||||
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
|
||||
) from None
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.warning("Upload of %s was cancelled", filename)
|
||||
reader.abort()
|
||||
raise
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ def _ws_with_blueprint_domain(
|
||||
return with_domain_blueprints
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/list",
|
||||
@@ -97,6 +98,7 @@ async def ws_list_blueprints(
|
||||
connection.send_result(msg["id"], results)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/import",
|
||||
@@ -150,6 +152,7 @@ async def ws_import_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/save",
|
||||
@@ -206,6 +209,7 @@ async def ws_save_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/delete",
|
||||
@@ -233,6 +237,7 @@ async def ws_delete_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/substitute",
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_USE_SSL
|
||||
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
|
||||
|
||||
PLATFORMS: Final[list[Platform]] = [
|
||||
@@ -26,11 +27,12 @@ async def async_setup_entry(
|
||||
"""Set up a config entry."""
|
||||
host = config_entry.data[CONF_HOST]
|
||||
mac = config_entry.data[CONF_MAC]
|
||||
ssl = config_entry.data.get(CONF_USE_SSL, False)
|
||||
|
||||
session = async_create_clientsession(
|
||||
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
client = BraviaClient(host, mac, session=session)
|
||||
client = BraviaClient(host, mac, session=session, ssl=ssl)
|
||||
coordinator = BraviaTVCoordinator(
|
||||
hass=hass,
|
||||
config_entry=config_entry,
|
||||
|
||||
@@ -28,6 +28,7 @@ from .const import (
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
DOMAIN,
|
||||
NICKNAME_PREFIX,
|
||||
)
|
||||
@@ -46,11 +47,12 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def create_client(self) -> None:
|
||||
"""Create Bravia TV client from config."""
|
||||
host = self.device_config[CONF_HOST]
|
||||
ssl = self.device_config[CONF_USE_SSL]
|
||||
session = async_create_clientsession(
|
||||
self.hass,
|
||||
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
|
||||
)
|
||||
self.client = BraviaClient(host=host, session=session)
|
||||
self.client = BraviaClient(host=host, session=session, ssl=ssl)
|
||||
|
||||
async def gen_instance_ids(self) -> tuple[str, str]:
|
||||
"""Generate client_id and nickname."""
|
||||
@@ -123,10 +125,10 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle authorize step."""
|
||||
self.create_client()
|
||||
|
||||
if user_input is not None:
|
||||
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
|
||||
self.device_config[CONF_USE_SSL] = user_input[CONF_USE_SSL]
|
||||
self.create_client()
|
||||
if user_input[CONF_USE_PSK]:
|
||||
return await self.async_step_psk()
|
||||
return await self.async_step_pin()
|
||||
@@ -136,6 +138,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USE_PSK, default=False): bool,
|
||||
vol.Required(CONF_USE_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
CONF_USE_SSL: Final = "use_ssl"
|
||||
|
||||
DOMAIN: Final = "braviatv"
|
||||
LEGACY_CLIENT_ID: Final = "HomeAssistant"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pybravia"],
|
||||
"requirements": ["pybravia==0.3.4"],
|
||||
"requirements": ["pybravia==0.4.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Sony Corporation",
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
"step": {
|
||||
"authorize": {
|
||||
"data": {
|
||||
"use_psk": "Use PSK authentication"
|
||||
"use_psk": "Use PSK authentication",
|
||||
"use_ssl": "Use SSL connection"
|
||||
},
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
|
||||
"description": "Make sure that «Control remotely» is enabled on your TV. Go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended, as it is more stable. \n\nUse an SSL connection only if your TV supports this connection type.",
|
||||
"title": "Authorize Sony Bravia TV"
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -7,11 +7,12 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -33,28 +34,27 @@ ATTR_SUNDAY_SLOTS = "sunday_slots"
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule"
|
||||
|
||||
|
||||
def _parse_time_value(value: time | str) -> time:
|
||||
"""Parse a time value from either a time object or string.
|
||||
# Schema for a single time slot
|
||||
_SLOT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("start_time"): cv.time,
|
||||
vol.Required("end_time"): cv.time,
|
||||
}
|
||||
)
|
||||
|
||||
Raises ServiceValidationError if the format is invalid.
|
||||
"""
|
||||
if isinstance(value, time):
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parts = value.split(":")
|
||||
return time(int(parts[0]), int(parts[1]))
|
||||
except (ValueError, IndexError):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_time_format",
|
||||
) from None
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_time_format",
|
||||
)
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
vol.Optional(ATTR_MONDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_TUESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_WEDNESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_THURSDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_FRIDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SATURDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
vol.Optional(ATTR_SUNDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _convert_time_slots_to_day_schedule(
|
||||
@@ -62,8 +62,8 @@ def _convert_time_slots_to_day_schedule(
|
||||
) -> DaySchedule | None:
|
||||
"""Convert list of time slot dicts to a DaySchedule object.
|
||||
|
||||
Example: [{"start_time": "06:00", "end_time": "08:00"},
|
||||
{"start_time": "17:00", "end_time": "21:00"}]
|
||||
Example: [{"start_time": time(6, 0), "end_time": time(8, 0)},
|
||||
{"start_time": time(17, 0), "end_time": time(21, 0)}]
|
||||
becomes: DaySchedule with two TimeSlot objects
|
||||
|
||||
None returns None (don't modify this day).
|
||||
@@ -77,31 +77,27 @@ def _convert_time_slots_to_day_schedule(
|
||||
|
||||
time_slots = []
|
||||
for slot in slots:
|
||||
start = slot.get("start_time")
|
||||
end = slot.get("end_time")
|
||||
start_time = slot["start_time"]
|
||||
end_time = slot["end_time"]
|
||||
|
||||
if start and end:
|
||||
start_time = _parse_time_value(start)
|
||||
end_time = _parse_time_value(end)
|
||||
|
||||
# Validate that end time is after start time
|
||||
if end_time <= start_time:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="end_time_before_start_time",
|
||||
translation_placeholders={
|
||||
"start_time": start_time.strftime("%H:%M"),
|
||||
"end_time": end_time.strftime("%H:%M"),
|
||||
},
|
||||
)
|
||||
|
||||
time_slots.append(TimeSlot(start=start_time, end=end_time))
|
||||
LOGGER.debug(
|
||||
"Created time slot: %s-%s",
|
||||
start_time.strftime("%H:%M"),
|
||||
end_time.strftime("%H:%M"),
|
||||
# Validate that end time is after start time
|
||||
if end_time <= start_time:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="end_time_before_start_time",
|
||||
translation_placeholders={
|
||||
"start_time": start_time.strftime("%H:%M"),
|
||||
"end_time": end_time.strftime("%H:%M"),
|
||||
},
|
||||
)
|
||||
|
||||
time_slots.append(TimeSlot(start=start_time, end=end_time))
|
||||
LOGGER.debug(
|
||||
"Created time slot: %s-%s",
|
||||
start_time.strftime("%H:%M"),
|
||||
end_time.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
LOGGER.debug("Created DaySchedule with %d slots", len(time_slots))
|
||||
return DaySchedule(slots=time_slots)
|
||||
|
||||
@@ -214,4 +210,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SCHEDULE,
|
||||
set_hot_water_schedule,
|
||||
schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.17.0"]
|
||||
"requirements": ["bthome-ble==3.16.0"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [HVACMode]
|
||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2025.12.2"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.1.6"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==17.0.1"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittSensor, EcoWittSensorTypes
|
||||
@@ -39,6 +40,9 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
from . import EcowittConfigEntry
|
||||
from .entity import EcowittEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_METRIC: Final = (
|
||||
EcoWittSensorTypes.TEMPERATURE_C,
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM,
|
||||
@@ -57,6 +61,40 @@ _IMPERIAL: Final = (
|
||||
)
|
||||
|
||||
|
||||
_RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING: Final = {
|
||||
"eventrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"hourlyrainin": None,
|
||||
"totalrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"dailyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"weeklyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"monthlyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"yearlyrainin": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrainin": None,
|
||||
"eventrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"hourlyrainmm": None,
|
||||
"totalrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"dailyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"weeklyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"monthlyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"yearlyrainmm": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrainmm": None,
|
||||
"erain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"hrain_piezo": None,
|
||||
"drain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"wrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"mrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"yrain_piezo": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrain_piezo": None,
|
||||
"erain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"hrain_piezomm": None,
|
||||
"drain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"wrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"mrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"yrain_piezomm": SensorStateClass.TOTAL_INCREASING,
|
||||
"last24hrain_piezomm": None,
|
||||
}
|
||||
|
||||
|
||||
ECOWITT_SENSORS_MAPPING: Final = {
|
||||
EcoWittSensorTypes.HUMIDITY: SensorEntityDescription(
|
||||
key="HUMIDITY",
|
||||
@@ -285,15 +323,15 @@ async def async_setup_entry(
|
||||
name=sensor.name,
|
||||
)
|
||||
|
||||
# Only total rain needs state class for long-term statistics
|
||||
if sensor.key in (
|
||||
"totalrainin",
|
||||
"totalrainmm",
|
||||
if sensor.stype in (
|
||||
EcoWittSensorTypes.RAIN_COUNT_INCHES,
|
||||
EcoWittSensorTypes.RAIN_COUNT_MM,
|
||||
):
|
||||
description = dataclasses.replace(
|
||||
description,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
if sensor.key not in _RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING:
|
||||
_LOGGER.warning("Unknown rain count sensor: %s", sensor.key)
|
||||
return
|
||||
state_class = _RAIN_COUNT_SENSORS_STATE_CLASS_MAPPING[sensor.key]
|
||||
description = dataclasses.replace(description, state_class=state_class)
|
||||
|
||||
async_add_entities([EcowittSensorEntity(sensor, description)])
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["eheimdigital==1.4.0"],
|
||||
"requirements": ["eheimdigital==1.5.0"],
|
||||
"zeroconf": [
|
||||
{ "name": "eheimdigital._http._tcp.local.", "type": "_http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.4.2"],
|
||||
"requirements": ["pyenphase==2.4.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -783,7 +783,7 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="available_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("available_energy"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
@@ -791,14 +791,14 @@ ENCHARGE_AGGREGATE_SENSORS = (
|
||||
translation_key="reserve_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("backup_reserve"),
|
||||
),
|
||||
EnvoyEnchargeAggregateSensorEntityDescription(
|
||||
key="max_capacity",
|
||||
translation_key="max_capacity",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
value_fn=attrgetter("max_available_capacity"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==43.9.0",
|
||||
"aioesphomeapi==43.9.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@ from enum import StrEnum
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "essent"
|
||||
UPDATE_INTERVAL: Final = timedelta(hours=12)
|
||||
UPDATE_INTERVAL: Final = timedelta(hours=1)
|
||||
ATTRIBUTION: Final = "Data provided by Essent"
|
||||
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ from homeassistant.util.hass_dict import HassKey
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FeedReaderConfigEntry, FeedReaderCoordinator, StoredData
|
||||
|
||||
CONF_URLS = "urls"
|
||||
|
||||
MY_KEY: HassKey[StoredData] = HassKey(DOMAIN)
|
||||
FEEDREADER_KEY: HassKey[StoredData] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) -> bool:
|
||||
"""Set up Feedreader from a config entry."""
|
||||
storage = hass.data.setdefault(MY_KEY, StoredData(hass))
|
||||
storage = hass.data.setdefault(FEEDREADER_KEY, StoredData(hass))
|
||||
if not storage.is_initialized:
|
||||
await storage.async_setup()
|
||||
|
||||
@@ -42,5 +40,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry)
|
||||
)
|
||||
# if this is the last entry, remove the storage
|
||||
if len(entries) == 1:
|
||||
hass.data.pop(MY_KEY)
|
||||
hass.data.pop(FEEDREADER_KEY)
|
||||
return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT])
|
||||
|
||||
@@ -42,16 +42,15 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity):
|
||||
_attr_event_types = [EVENT_FEEDREADER]
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "latest_feed"
|
||||
_unrecorded_attributes = frozenset(
|
||||
{ATTR_CONTENT, ATTR_DESCRIPTION, ATTR_TITLE, ATTR_LINK}
|
||||
)
|
||||
coordinator: FeedReaderCoordinator
|
||||
|
||||
def __init__(self, coordinator: FeedReaderCoordinator) -> None:
|
||||
"""Initialize the feedreader event."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_latest_feed"
|
||||
self._attr_translation_key = "latest_feed"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
name=coordinator.config_entry.title,
|
||||
|
||||
@@ -21,12 +21,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"import_yaml_error_url_error": {
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that the URL is reachable and accessible for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually.",
|
||||
"title": "The Feedreader YAML configuration import failed"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -35,6 +35,6 @@ BACKEND_MODELS = ["s1", "speech-1.5", "speech-1.6"]
|
||||
SORT_BY_OPTIONS = ["task_count", "score", "created_at"]
|
||||
LATENCY_OPTIONS = ["normal", "balanced"]
|
||||
|
||||
SIGNUP_URL = "https://fish.audio/?fpr=homeassistant" # codespell:ignore fpr
|
||||
SIGNUP_URL = "https://fish.audio/"
|
||||
BILLING_URL = "https://fish.audio/app/billing/"
|
||||
API_KEYS_URL = "https://fish.audio/app/api-keys/"
|
||||
|
||||
@@ -461,7 +461,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
|
||||
key="sleep/timeInBed",
|
||||
translation_key="sleep_time_in_bed",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
icon="mdi:hotel",
|
||||
icon="mdi:bed",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
scope=FitbitScope.SLEEP,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
|
||||
@@ -31,7 +31,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
STEP_SMS_CODE_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SMS_CODE): int,
|
||||
vol.Required(CONF_SMS_CODE): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ class FressnapfTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return errors, False
|
||||
|
||||
async def _async_verify_sms_code(
|
||||
self, sms_code: int
|
||||
self, sms_code: str
|
||||
) -> tuple[dict[str, str], str | None]:
|
||||
"""Verify SMS code and return errors and access_token."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["fressnapftracker==0.2.0"]
|
||||
"requirements": ["fressnapftracker==0.2.1"]
|
||||
}
|
||||
|
||||
@@ -77,9 +77,14 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
)
|
||||
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
|
||||
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
try:
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
except HTTPError:
|
||||
# Fritz!OS < 7.39 just don't have this api endpoint
|
||||
# so we need to fetch the HTTPError here and assume no triggers
|
||||
self.has_triggers = False
|
||||
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
|
||||
|
||||
self.configuration_url = self.fritz.get_prefixed_host()
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251229.0"]
|
||||
"requirements": ["home-assistant-frontend==20260107.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==2.0.2"]
|
||||
"requirements": ["google_air_quality_api==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ async def async_tts_voices(
|
||||
list_voices_response = await client.list_voices()
|
||||
for voice in list_voices_response.voices:
|
||||
language_code = voice.language_codes[0]
|
||||
if not voice.name.startswith(language_code):
|
||||
continue
|
||||
if language_code not in voices:
|
||||
voices[language_code] = []
|
||||
voices[language_code].append(voice.name)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greeclimate"],
|
||||
"requirements": ["greeclimate==2.1.0"]
|
||||
"requirements": ["greeclimate==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyhik.constants import SENSOR_MAP
|
||||
from pyhik.hikvision import HikCamera
|
||||
import requests
|
||||
|
||||
@@ -70,13 +71,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
device_type=device_type,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Device %s (type=%s) initial event_states: %s",
|
||||
device_name,
|
||||
device_type,
|
||||
camera.current_event_states,
|
||||
)
|
||||
|
||||
# For NVRs or devices with no detected events, try to fetch events from ISAPI
|
||||
# Use broader notification methods for NVRs since they often use 'record' etc.
|
||||
if device_type == "NVR" or not camera.current_event_states:
|
||||
nvr_notification_methods = {"center", "HTTP", "record", "email", "beep"}
|
||||
|
||||
def fetch_and_inject_nvr_events() -> None:
|
||||
"""Fetch and inject NVR events in a single executor job."""
|
||||
if nvr_events := camera.get_event_triggers():
|
||||
camera.inject_events(nvr_events)
|
||||
nvr_events = camera.get_event_triggers(nvr_notification_methods)
|
||||
_LOGGER.debug("NVR events fetched with extended methods: %s", nvr_events)
|
||||
if nvr_events:
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
|
||||
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -227,7 +227,10 @@ class HikvisionBinarySensor(BinarySensorEntity):
|
||||
# Register callback with pyhik
|
||||
self._camera.add_update_callback(self._update_callback, self._callback_id)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, msg: str) -> None:
|
||||
"""Update the sensor's state when callback is triggered."""
|
||||
self.async_write_ha_state()
|
||||
"""Update the sensor's state when callback is triggered.
|
||||
|
||||
This is called from pyhik's event stream thread, so we use
|
||||
schedule_update_ha_state which is thread-safe.
|
||||
"""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyHik==0.3.4"]
|
||||
"requirements": ["pyHik==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.5.0",
|
||||
"serialx==0.6.2",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==10.0.0"],
|
||||
"requirements": ["python-homewizard-energy==10.0.1"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": true,
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb"
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb",
|
||||
"service_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@flip-dots"],
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"trigger": "mdi:air-humidifier-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:air-humidifier-on"
|
||||
"trigger": "mdi:air-humidifier"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,14 +31,11 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -67,21 +67,21 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
|
||||
HydrawiseSensorEntityDescription(
|
||||
key="daily_total_water_use",
|
||||
translation_key="daily_total_water_use",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda sensor: _get_water_use(sensor).total_use,
|
||||
),
|
||||
HydrawiseSensorEntityDescription(
|
||||
key="daily_active_water_use",
|
||||
translation_key="daily_active_water_use",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda sensor: _get_water_use(sensor).total_active_use,
|
||||
),
|
||||
HydrawiseSensorEntityDescription(
|
||||
key="daily_inactive_water_use",
|
||||
translation_key="daily_inactive_water_use",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda sensor: _get_water_use(sensor).total_inactive_use,
|
||||
),
|
||||
@@ -91,7 +91,7 @@ FLOW_ZONE_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
HydrawiseSensorEntityDescription(
|
||||
key="daily_active_water_use",
|
||||
translation_key="daily_active_water_use",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda sensor: float(
|
||||
_get_water_use(sensor).active_use_by_zone_id.get(sensor.zone.id, 0.0)
|
||||
@@ -204,7 +204,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit_of_measurement of the sensor."""
|
||||
if self.entity_description.device_class != SensorDeviceClass.VOLUME:
|
||||
if self.entity_description.device_class != SensorDeviceClass.WATER:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
return (
|
||||
UnitOfVolume.GALLONS
|
||||
@@ -217,7 +217,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
"""Icon of the entity based on the value."""
|
||||
if (
|
||||
self.entity_description.key in FLOW_MEASUREMENT_KEYS
|
||||
and self.entity_description.device_class == SensorDeviceClass.VOLUME
|
||||
and self.entity_description.device_class == SensorDeviceClass.WATER
|
||||
and round(self.state, 2) == 0.0
|
||||
):
|
||||
return "mdi:water-outline"
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["incomfort-client==0.6.10"]
|
||||
"requirements": ["incomfort-client==0.6.11"]
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ class IsraelRailEntitySensor(
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
if self.entity_description.index >= len(self.coordinator.data):
|
||||
return None
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.entity_description.index]
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.13.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.12.28.215221"
|
||||
"knx-frontend==2026.1.15.112308"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -154,6 +154,27 @@
|
||||
}
|
||||
},
|
||||
"config_panel": {
|
||||
"dashboard": {
|
||||
"connection_flow": {
|
||||
"description": "Reconfigure KNX connection or import a new KNX keyring file",
|
||||
"title": "Connection settings"
|
||||
},
|
||||
"options_flow": {
|
||||
"description": "Configure integration settings",
|
||||
"title": "Integration options"
|
||||
},
|
||||
"project_upload": {
|
||||
"description": "Import a KNX project file to help configure group addresses and datapoint types",
|
||||
"title": "[%key:component::knx::config_panel::dialogs::project_upload::title%]"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
"project_upload": {
|
||||
"description": "Details such as group address names, datapoint types, devices and group objects are extracted from your project file. The ETS project file itself and its optional password are not stored.\n\n`.knxproj` files exported by ETS 4, 5 or 6 are supported.",
|
||||
"file_upload_label": "ETS project file",
|
||||
"title": "Import ETS project"
|
||||
}
|
||||
},
|
||||
"dpt": {
|
||||
"options": {
|
||||
"5": "Generic 1-byte unsigned integer",
|
||||
@@ -845,9 +866,9 @@
|
||||
},
|
||||
"mode": {
|
||||
"description": "Select how the entity is displayed in Home Assistant.",
|
||||
"label": "[%common::config_flow::data::mode%]",
|
||||
"label": "[%key:common::config_flow::data::mode%]",
|
||||
"options": {
|
||||
"password": "[%common::config_flow::data::password%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"text": "[%key:component::text::entity_component::_::state_attributes::mode::state::text%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,6 @@ async def register_panel(hass: HomeAssistant) -> None:
|
||||
hass=hass,
|
||||
frontend_url_path=DOMAIN,
|
||||
webcomponent_name=knx_panel.webcomponent_name,
|
||||
sidebar_title=DOMAIN.upper(),
|
||||
sidebar_icon="mdi:bus-electric",
|
||||
module_url=f"{URL_BASE}/{knx_panel.entrypoint_js}",
|
||||
embed_iframe=True,
|
||||
require_admin=True,
|
||||
|
||||
@@ -256,6 +256,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
@@ -289,6 +291,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -149,6 +149,8 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
and WidgetType.CM_BREW_BY_WEIGHT_DOSES
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
@@ -27,10 +31,6 @@
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
turned_on: *trigger_common
|
||||
@@ -48,6 +48,7 @@ brightness_crossed_threshold:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type:
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
|
||||
@@ -154,6 +154,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8027),
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x131A, 0x1000),
|
||||
}
|
||||
|
||||
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiomealie==1.1.1"]
|
||||
"requirements": ["aiomealie==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -116,8 +116,12 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator[MetWeatherData]):
|
||||
"""Fetch data from Met."""
|
||||
try:
|
||||
return await self.weather.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
except CannotConnect as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
def track_home(self) -> None:
|
||||
"""Start tracking changes to HA home setting."""
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"update_failed": {
|
||||
"message": "Update of data from the web site failed: {error}"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -69,7 +69,7 @@ class RegistrationsView(HomeAssistantView):
|
||||
|
||||
webhook_id = secrets.token_hex()
|
||||
|
||||
if cloud.async_active_subscription(hass):
|
||||
if cloud.async_active_subscription(hass) and cloud.async_is_connected(hass):
|
||||
data[CONF_CLOUDHOOK_URL] = await async_create_cloud_hook(
|
||||
hass, webhook_id, None
|
||||
)
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nacl"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyNaCl==1.6.0"]
|
||||
"requirements": ["PyNaCl==1.6.2"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynetgear"],
|
||||
"requirements": ["pynetgear==0.10.10"],
|
||||
|
||||
@@ -51,7 +51,6 @@ ALL_BINARY_SENSORS = [
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NOTIFY,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
@@ -61,6 +60,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Netgear LTE component."""
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -96,19 +96,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) -
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
await discovery.async_load_platform(
|
||||
hass,
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
{CONF_NAME: entry.title, "modem": modem},
|
||||
{CONF_NAME: entry.title, "modem": modem, "entry": entry},
|
||||
hass.data[DATA_HASS_CONFIG],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -118,7 +114,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hass.data.pop(DOMAIN, None)
|
||||
for service_name in hass.services.async_services()[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import DEFAULT_HOST, DOMAIN, LOGGER, MANUFACTURER
|
||||
from .const import DEFAULT_HOST, DOMAIN, MANUFACTURER
|
||||
|
||||
|
||||
class NetgearLTEFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -72,9 +72,6 @@ class NetgearLTEFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
info = await modem.information()
|
||||
except Error as ex:
|
||||
raise InputValidationError("cannot_connect") from ex
|
||||
except Exception as ex:
|
||||
LOGGER.exception("Unexpected exception")
|
||||
raise InputValidationError("unknown") from ex
|
||||
await modem.logout()
|
||||
return info
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eternalegypt"],
|
||||
"requirements": ["eternalegypt==0.0.16"]
|
||||
"requirements": ["eternalegypt==0.0.18"]
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class NetgearNotifyService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self.config = config
|
||||
self.modem: Modem = discovery_info["modem"]
|
||||
discovery_info["entry"].async_on_unload(self.async_unregister_services)
|
||||
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
|
||||
@@ -4,6 +4,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
@@ -14,7 +15,6 @@ from .const import (
|
||||
AUTOCONNECT_MODES,
|
||||
DOMAIN,
|
||||
FAILOVER_MODES,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import NetgearLTEConfigEntry
|
||||
|
||||
@@ -56,8 +56,11 @@ async def _service_handler(call: ServiceCall) -> None:
|
||||
break
|
||||
|
||||
if not entry or not (modem := entry.runtime_data.modem).token:
|
||||
LOGGER.error("%s: host %s unavailable", call.service, host)
|
||||
return
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_found",
|
||||
translation_placeholders={"service": call.service},
|
||||
)
|
||||
|
||||
if call.service == SERVICE_DELETE_SMS:
|
||||
for sms_id in call.data[ATTR_SMS_ID]:
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_found": {
|
||||
"message": "Failed to perform action \"{service}\". Config entry for target not found"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"connect_lte": {
|
||||
"description": "Asks the modem to establish the LTE connection.",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoauth", "pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.0"]
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nuheat",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nuheat"],
|
||||
"requirements": ["nuheat==1.0.1"]
|
||||
|
||||
@@ -112,45 +112,49 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except openai.APIConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except openai.AuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="ChatGPT",
|
||||
data=user_input,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
||||
"title": DEFAULT_CONVERSATION_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
{
|
||||
"subentry_type": "ai_task_data",
|
||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||
"title": DEFAULT_AI_TASK_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except openai.APIConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except openai.AuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="ChatGPT",
|
||||
data=user_input,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": RECOMMENDED_CONVERSATION_OPTIONS,
|
||||
"title": DEFAULT_CONVERSATION_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
{
|
||||
"subentry_type": "ai_task_data",
|
||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||
"title": DEFAULT_AI_TASK_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"instructions_url": "https://www.home-assistant.io/integrations/openai_conversation/#generate-an-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your OpenAI API key."
|
||||
},
|
||||
"description": "Set up OpenAI Conversation integration by providing your OpenAI API key. Instructions to obtain an API key can be found [here]({instructions_url})."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.9"]
|
||||
"requirements": ["opower==0.16.3"]
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ SENSOR_TYPES: dict[str, OSOEnergySensorEntityDescription] = {
|
||||
key="optimization_mode",
|
||||
translation_key="optimization_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced"],
|
||||
options=["off", "oso", "gridcompany", "smartcompany", "advanced", "nettleie"],
|
||||
value_fn=lambda entity_data: entity_data.state.lower(),
|
||||
),
|
||||
"power_load": OSOEnergySensorEntityDescription(
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"state": {
|
||||
"advanced": "Advanced",
|
||||
"gridcompany": "Grid company",
|
||||
"nettleie": "Nettleie",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"oso": "OSO",
|
||||
"smartcompany": "Smart company"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.7.0"]
|
||||
"requirements": ["python-otbr-api==2.7.1"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.19.3"],
|
||||
"requirements": ["pyoverkiz==1.19.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "gateway*",
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nacl"],
|
||||
"requirements": ["PyNaCl==1.6.0"],
|
||||
"requirements": ["PyNaCl==1.6.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@IsakNyberg"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/permobil",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["mypermobil==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/pooldose",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-pooldose==0.8.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@haemishkyd"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/poolsense",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["poolsense"],
|
||||
"requirements": ["poolsense==0.0.8"]
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PortainerConfigEntry
|
||||
from .const import CONTAINER_STATE_RUNNING
|
||||
from .coordinator import PortainerContainerData, PortainerCoordinator
|
||||
from .entity import (
|
||||
PortainerContainerEntity,
|
||||
@@ -41,7 +42,7 @@ CONTAINER_SENSORS: tuple[PortainerContainerBinarySensorEntityDescription, ...] =
|
||||
PortainerContainerBinarySensorEntityDescription(
|
||||
key="status",
|
||||
translation_key="status",
|
||||
state_fn=lambda data: data.container.state == "running",
|
||||
state_fn=lambda data: data.container.state == CONTAINER_STATE_RUNNING,
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
|
||||
@@ -4,3 +4,5 @@ DOMAIN = "portainer"
|
||||
DEFAULT_NAME = "Portainer"
|
||||
|
||||
ENDPOINT_STATUS_DOWN = 2
|
||||
|
||||
CONTAINER_STATE_RUNNING = "running"
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, ENDPOINT_STATUS_DOWN
|
||||
from .const import CONTAINER_STATE_RUNNING, DOMAIN, ENDPOINT_STATUS_DOWN
|
||||
|
||||
type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]
|
||||
|
||||
@@ -158,10 +158,11 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
),
|
||||
)
|
||||
for container in containers
|
||||
if container.state == CONTAINER_STATE_RUNNING
|
||||
]
|
||||
|
||||
container_stats_gather = await asyncio.gather(
|
||||
*[task for _, task in container_stats_task],
|
||||
*[task for _, task in container_stats_task]
|
||||
)
|
||||
for (container, _), container_stats in zip(
|
||||
container_stats_task, container_stats_gather, strict=False
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.17"]
|
||||
"requirements": ["pyportainer==1.0.19"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ktnrg45"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ps4",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyps4_2ndscreen"],
|
||||
"requirements": ["pyps4-2ndscreen==1.3.1"]
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .entity import (
|
||||
ReolinkChannelCoordinatorEntity,
|
||||
@@ -157,16 +158,16 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this light between 0.255."""
|
||||
"""Return the brightness of this light between 1.255."""
|
||||
assert self.entity_description.get_brightness_fn is not None
|
||||
|
||||
bright_pct = self.entity_description.get_brightness_fn(
|
||||
self._host.api, self._channel
|
||||
)
|
||||
if bright_pct is None:
|
||||
if not bright_pct:
|
||||
return None
|
||||
|
||||
return round(255 * bright_pct / 100.0)
|
||||
return color_util.value_to_brightness((1, 100), bright_pct)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
@@ -189,7 +190,7 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
if (
|
||||
brightness := kwargs.get(ATTR_BRIGHTNESS)
|
||||
) is not None and self.entity_description.set_brightness_fn is not None:
|
||||
brightness_pct = int(brightness / 255.0 * 100)
|
||||
brightness_pct = round(color_util.brightness_to_value((1, 100), brightness))
|
||||
await self.entity_description.set_brightness_fn(
|
||||
self._host.api, self._channel, brightness_pct
|
||||
)
|
||||
|
||||
@@ -128,8 +128,9 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self._device = self._get_coordinator_data().get_video_device(
|
||||
self._device.device_api_id
|
||||
)
|
||||
|
||||
history_data = self._device.last_history
|
||||
if history_data:
|
||||
if history_data and self._device.has_subscription:
|
||||
self._last_event = history_data[0]
|
||||
# will call async_update to update the attributes and get the
|
||||
# video url from the api
|
||||
@@ -154,8 +155,16 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
if self._video_url is None:
|
||||
if not self._device.has_subscription:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_subscription",
|
||||
)
|
||||
return None
|
||||
|
||||
key = (width, height)
|
||||
if not (image := self._images.get(key)) and self._video_url is not None:
|
||||
if not (image := self._images.get(key)):
|
||||
image = await ffmpeg.async_get_image(
|
||||
self.hass,
|
||||
self._video_url,
|
||||
|
||||
@@ -151,6 +151,9 @@
|
||||
"api_timeout": {
|
||||
"message": "Timeout communicating with Ring API"
|
||||
},
|
||||
"no_subscription": {
|
||||
"message": "Ring Protect subscription required for snapshots"
|
||||
},
|
||||
"sdp_m_line_index_required": {
|
||||
"message": "Error negotiating stream for {device}"
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
map_scale=MAP_SCALE,
|
||||
),
|
||||
mqtt_session_unauthorized_hook=lambda: entry.async_start_reauth(hass),
|
||||
prefer_cache=False,
|
||||
)
|
||||
except RoborockInvalidCredentials as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==3.21.0",
|
||||
"python-roborock==4.2.1",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -391,15 +391,6 @@ Q7_B01_SENSOR_DESCRIPTIONS = [
|
||||
translation_key="mop_life_time_left",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
RoborockSensorDescriptionB01(
|
||||
key="total_cleaning_time",
|
||||
value_fn=lambda data: data.real_clean_time,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
translation_key="total_cleaning_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -325,8 +325,7 @@ class ShoppingData:
|
||||
)
|
||||
return self.items
|
||||
|
||||
@callback
|
||||
def async_reorder(
|
||||
async def async_reorder(
|
||||
self, item_ids: list[str], context: Context | None = None
|
||||
) -> None:
|
||||
"""Reorder items."""
|
||||
@@ -351,7 +350,7 @@ class ShoppingData:
|
||||
)
|
||||
new_items.append(value)
|
||||
self.items = new_items
|
||||
self.hass.async_add_executor_job(self.save)
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
@@ -388,7 +387,7 @@ class ShoppingData:
|
||||
) -> None:
|
||||
"""Sort items by name."""
|
||||
self.items = sorted(self.items, key=lambda item: item["name"], reverse=reverse) # type: ignore[arg-type,return-value]
|
||||
self.hass.async_add_executor_job(self.save)
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
@@ -591,7 +590,8 @@ async def websocket_handle_clear(
|
||||
vol.Required("item_ids"): [str],
|
||||
}
|
||||
)
|
||||
def websocket_handle_reorder(
|
||||
@websocket_api.async_response
|
||||
async def websocket_handle_reorder(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -599,7 +599,9 @@ def websocket_handle_reorder(
|
||||
"""Handle reordering shopping_list items."""
|
||||
msg_id = msg.pop("id")
|
||||
try:
|
||||
hass.data[DOMAIN].async_reorder(msg.pop("item_ids"), connection.context(msg))
|
||||
await hass.data[DOMAIN].async_reorder(
|
||||
msg.pop("item_ids"), connection.context(msg)
|
||||
)
|
||||
except NoMatchingShoppingListItem:
|
||||
connection.send_error(
|
||||
msg_id,
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysma"],
|
||||
"requirements": ["pysma==1.0.2"]
|
||||
"requirements": ["pysma==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ SENSOR_TYPES = [
|
||||
key="lifetime_energy",
|
||||
json_key="lifeTimeData",
|
||||
translation_key="lifetime_energy",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
@@ -55,6 +55,7 @@ SENSOR_TYPES = [
|
||||
json_key="lastYearData",
|
||||
translation_key="energy_this_year",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
@@ -63,6 +64,7 @@ SENSOR_TYPES = [
|
||||
json_key="lastMonthData",
|
||||
translation_key="energy_this_month",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
@@ -71,6 +73,7 @@ SENSOR_TYPES = [
|
||||
json_key="lastDayData",
|
||||
translation_key="energy_today",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
@@ -123,24 +126,32 @@ SENSOR_TYPES = [
|
||||
json_key="LOAD",
|
||||
translation_key="power_consumption",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="solar_power",
|
||||
json_key="PV",
|
||||
translation_key="solar_power",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="grid_power",
|
||||
json_key="GRID",
|
||||
translation_key="grid_power",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="storage_power",
|
||||
json_key="STORAGE",
|
||||
translation_key="storage_power",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SolarEdgeSensorEntityDescription(
|
||||
key="purchased_energy",
|
||||
@@ -194,6 +205,7 @@ SENSOR_TYPES = [
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["solarlog_cli"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["solarlog_cli==0.6.1"]
|
||||
"requirements": ["solarlog_cli==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -958,6 +958,23 @@ class SonosSpeaker:
|
||||
# as those "invisible" speakers will bypass the single speaker check
|
||||
return
|
||||
|
||||
# Clear coordinator on speakers that are no longer in this group
|
||||
old_members = set(self.sonos_group[1:])
|
||||
new_members = set(sonos_group[1:])
|
||||
removed_members = old_members - new_members
|
||||
for removed_speaker in removed_members:
|
||||
# Only clear if this speaker was coordinated by self and in the same group
|
||||
if (
|
||||
removed_speaker.coordinator == self
|
||||
and removed_speaker.sonos_group is self.sonos_group
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Zone %s Cleared coordinator [%s] (removed from group)",
|
||||
removed_speaker.zone_name,
|
||||
self.zone_name,
|
||||
)
|
||||
removed_speaker.clear_coordinator()
|
||||
|
||||
self.coordinator = None
|
||||
self.sonos_group = sonos_group
|
||||
self.sonos_group_entities = sonos_group_entities
|
||||
@@ -990,6 +1007,19 @@ class SonosSpeaker:
|
||||
|
||||
return _async_handle_group_event(event)
|
||||
|
||||
@callback
|
||||
def clear_coordinator(self) -> None:
|
||||
"""Clear coordinator from speaker."""
|
||||
self.coordinator = None
|
||||
self.sonos_group = [self]
|
||||
entity_registry = er.async_get(self.hass)
|
||||
speaker_entity_id = cast(
|
||||
str,
|
||||
entity_registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.uid),
|
||||
)
|
||||
self.sonos_group_entities = [speaker_entity_id]
|
||||
self.async_write_entity_states()
|
||||
|
||||
@soco_error()
|
||||
def join(self, speakers: list[SonosSpeaker]) -> list[SonosSpeaker]:
|
||||
"""Form a group with other players."""
|
||||
@@ -1038,7 +1068,6 @@ class SonosSpeaker:
|
||||
if self.sonos_group == [self]:
|
||||
return
|
||||
self.soco.unjoin()
|
||||
self.coordinator = None
|
||||
|
||||
@staticmethod
|
||||
async def unjoin_multi(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user