forked from home-assistant/core
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20a1c046c9 | |||
| 97a0b9272e | |||
| 3d49000c75 | |||
| 68d1a3c0a2 | |||
| b06de7a687 | |||
| 963ea6141c | |||
| 7232d36494 | |||
| 12eb071e8a | |||
| 86be626c69 | |||
| ec20e41836 | |||
| b3564b6cff | |||
| acbee815be | |||
| 5a1aeff85c | |||
| 4463e4c42b | |||
| 4103ef71c9 | |||
| dccaa2dd2d | |||
| 5ac6096e08 | |||
| 4734a82f99 | |||
| c6c2309dee | |||
| 5e1bbd8bff | |||
| 4761207097 | |||
| b4a6ca63b3 | |||
| a5b320180a | |||
| bcc767136c | |||
| d3257d96d0 | |||
| 89df6a82b0 | |||
| e725ba403b | |||
| 578fece13e | |||
| 0eeb6b5fd5 | |||
| ad3f7f041f | |||
| dce9bfd359 | |||
| 8f96ccc835 | |||
| a48dd05035 | |||
| eb90958341 | |||
| 31ed6a48cb | |||
| dfa80f0787 | |||
| 73acfa6a8e | |||
| efad20cdff | |||
| 11d68cef54 | |||
| 9c28e60475 | |||
| 9ee79b87ee | |||
| 600aedc9a1 | |||
| 5b5efb5aaa | |||
| 391b3ed1e7 | |||
| 24277259ad | |||
| 65261de7cc | |||
| beb92a7f9c | |||
| f1b059c75d | |||
| d4970f81aa | |||
| 29219afb7f | |||
| 7fbf15edc9 | |||
| c8d3fa6768 | |||
| ea5cf3d854 | |||
| 4a833fb489 | |||
| 02aa823d25 | |||
| 92034aeecc | |||
| 9f2232fad1 | |||
| 2be2d54a5c | |||
| ed99686cc1 | |||
| a6c1f1e485 | |||
| a219445751 | |||
| b65b5aacb6 | |||
| bcead72265 | |||
| 35b9564ed4 | |||
| aba01d4361 | |||
| 4398af51c8 | |||
| 83f4f4cc96 | |||
| 1800e6fb8e | |||
| 43b83c855f | |||
| 20e2de200f | |||
| ed4ebe1222 | |||
| 4e4446cef4 | |||
| e2ff0b265d | |||
| 6d48fc183a | |||
| ea8392a4a1 | |||
| bcd296822d | |||
| 6ee97f341d | |||
| aa2ab74ee9 | |||
| 49b2ab9889 | |||
| 2549e2cc0f | |||
| b15fa81a44 | |||
| 09f6246d1b | |||
| 96ff389fd1 | |||
| b55f1df297 | |||
| d88f7b8600 | |||
| df2a94bb5b | |||
| c4ac492c6e | |||
| fcd4d3e2df | |||
| 42d6bd3839 | |||
| d6b48003b6 | |||
| ba8f69d956 | |||
| f22bb72d18 | |||
| f7a0a9fa41 | |||
| 3795d653c5 | |||
| 8ee014b855 | |||
| 1ab5bdf85f | |||
| 5283e1a39f | |||
| 17c56208ee | |||
| 8474d9fefe | |||
| fd9f002e9f | |||
| 26268357a0 | |||
| 82b463b22f | |||
| 7ae397a211 | |||
| ea4ad681e4 | |||
| a150f9d5ad | |||
| afb7fe0d40 | |||
| 8bf42b9d3e | |||
| ba00707d89 | |||
| 2121b943a3 | |||
| ef06d2c06e | |||
| cc1fac5776 | |||
| 6cb3430c60 | |||
| 4cea90f773 | |||
| 4da5f6188d | |||
| e7f8b9ad92 | |||
| 473a28c5f2 | |||
| d765936be3 | |||
| 0db643d9d1 | |||
| af29159e2f | |||
| 65c38d8e31 | |||
| adb7aa237b | |||
| 577f86b83a | |||
| 01169e9184 | |||
| dde037291a | |||
| 6971a189f9 | |||
| c860686138 | |||
| 2eb507863f | |||
| 54ee5c6998 | |||
| 63df2474a9 | |||
| 93f12fb7c6 | |||
| b7a995ac53 | |||
| 267a80e70c | |||
| 078be3b8df | |||
| 7b6c967c3a | |||
| a405ccd044 | |||
| f6c55ebf05 | |||
| 6b3b4cce4b | |||
| 8887c979b4 | |||
| 665541409a | |||
| 195919b5fb | |||
| 31479056ed | |||
| 9f0976d94a | |||
| a049d2b7db | |||
| 6959017d55 | |||
| 4ff5a04a72 | |||
| ea0c4a7263 | |||
| 9633f03ddc | |||
| 4c0d8ce87c | |||
| d92728e533 | |||
| 799962ef0e | |||
| 631f817f11 | |||
| 52f7bdeb5d | |||
| 51db140aed | |||
| 1ad12d5945 | |||
| 9f5d94046d | |||
| de1e06c39b | |||
| abbabc11d2 | |||
| 62be82fd3c | |||
| f0fd5a639a | |||
| dea00fac3f | |||
| c30f17f592 | |||
| e9e95f45d8 | |||
| e8aa3e6d34 | |||
| 3646884d79 | |||
| 5747c6b1a8 | |||
| 43a5c7ddc8 | |||
| d9d74107fe | |||
| 373cca9857 | |||
| 284b3f444d | |||
| dfb088e524 | |||
| 5546f1d73d | |||
| 13fc871806 | |||
| 0f9fd78656 | |||
| 4f318c0be3 | |||
| 5eb1d0a28e | |||
| 66c03713b7 | |||
| d51070c99b | |||
| 50d050e63e | |||
| 89bf426163 | |||
| 377548e3a1 | |||
| 543c6929e6 | |||
| 42ae572948 | |||
| c3f8b7e200 | |||
| 3a207e2571 | |||
| 6bfd39f094 | |||
| 002ca9611d | |||
| 46ee3d2b26 | |||
| eb901bcf3a | |||
| 930b4a2c81 | |||
| 22d1b8e1cd |
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
HA_SHORT_VERSION: "2025.5"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
Generated
-2
@@ -1480,8 +1480,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
/tests/components/sunweg/ @rokam
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
||||
+17
-11
@@ -859,14 +859,8 @@ async def _async_set_up_integrations(
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
# Detect all cycles
|
||||
integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, all_integrations.values(), set(all_integrations)
|
||||
)
|
||||
)
|
||||
all_domains = set(integrations_after_dependencies)
|
||||
domains = set(integrations) & all_domains
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
@@ -874,8 +868,6 @@ async def _async_set_up_integrations(
|
||||
all_domains - domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, all_domains)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
@@ -908,12 +900,24 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in integrations_after_dependencies[domain]
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
if dep not in stage_domains
|
||||
}
|
||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||
|
||||
stage_all_domains = stage_domains | stage_dep_domains
|
||||
stage_all_integrations = {
|
||||
domain: all_integrations[domain] for domain in stage_all_domains
|
||||
}
|
||||
# Detect all cycles
|
||||
stage_integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, stage_all_integrations.values(), stage_all_domains
|
||||
)
|
||||
)
|
||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
||||
stage_domains &= stage_all_domains
|
||||
stage_dep_domains &= stage_all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||
@@ -924,6 +928,8 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bosch",
|
||||
"name": "Bosch",
|
||||
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
|
||||
}
|
||||
@@ -68,8 +68,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "LED bar mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"co2": "Carbon dioxide",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "Particulate matter"
|
||||
}
|
||||
},
|
||||
@@ -143,8 +143,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||
"state": {
|
||||
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
||||
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from pyairnow import WebServiceAPI
|
||||
from pyairnow.conv import aqi_to_concentration
|
||||
from pyairnow.errors import AirNowError
|
||||
from pyairnow.errors import AirNowError, InvalidJsonError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
distance=self.distance,
|
||||
)
|
||||
|
||||
except (AirNowError, ClientConnectorError) as error:
|
||||
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
if not obs:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"title": "Configure a geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
@@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon Monoxide",
|
||||
"n2": "Nitrogen Dioxide",
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur Dioxide"
|
||||
"s2": "Sulfur dioxide"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"air_quality": {
|
||||
"name": "Air Quality mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"auto": "Auto"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["boto3==1.34.131"]
|
||||
"requirements": ["boto3==1.37.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pydroid-ipcam==2.0.0"]
|
||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
config_entry: ApSystemsConfigEntry
|
||||
device_version: str
|
||||
battery_system: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
self.device_version = device_info.devVer
|
||||
self.battery_system = device_info.isBatterySystem
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
try:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.4.0"]
|
||||
"requirements": ["apsystems-ez1==2.5.0"]
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
super().__init__(data)
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
||||
if data.coordinator.battery_system:
|
||||
self._attr_available = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update switch status and availability."""
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -15,6 +17,8 @@ from .const import (
|
||||
CONNECTION_TEST_DATA,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
PREANNOUNCE_FILENAME,
|
||||
PREANNOUNCE_URL,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import (
|
||||
@@ -56,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
@@ -71,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("start_message"): str,
|
||||
vol.Optional("start_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
@@ -84,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
|
||||
# Default preannounce sound
|
||||
await hass.http.async_register_static_paths(
|
||||
[
|
||||
StaticPathConfig(
|
||||
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
|
||||
f"{DOMAIN}_connection_tests"
|
||||
)
|
||||
|
||||
PREANNOUNCE_FILENAME = "preannounce.mp3"
|
||||
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
|
||||
|
||||
|
||||
class AssistSatelliteEntityFeature(IntFlag):
|
||||
"""Supported features of Assist satellite entity."""
|
||||
|
||||
@@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, entity
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
|
||||
from .const import AssistSatelliteEntityFeature
|
||||
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
|
||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -180,7 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce_media_id: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
@@ -190,7 +190,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce_media_id is provided, it is played before the announcement.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
If preannounce_media_id is None, no sound is played.
|
||||
|
||||
Calls async_announce with message and media id.
|
||||
"""
|
||||
@@ -228,7 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message: str | None = None,
|
||||
start_media_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
preannounce_media_id: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Start a conversation from the satellite.
|
||||
|
||||
@@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce_media_id is provided, it is played before the announcement.
|
||||
If preannounce_media_id is None, no sound is played.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ announce:
|
||||
message:
|
||||
required: false
|
||||
example: "Time to wake up!"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
media_id:
|
||||
@@ -28,6 +29,7 @@ start_conversation:
|
||||
start_message:
|
||||
required: false
|
||||
example: "You left the lights on in the living room. Turn them off?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
start_media_id:
|
||||
|
||||
@@ -198,7 +198,8 @@ async def websocket_test_connection(
|
||||
|
||||
hass.async_create_background_task(
|
||||
satellite.async_internal_announce(
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}"
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||
preannounce_media_id=None,
|
||||
),
|
||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"backup_manager_state": {
|
||||
"name": "Backup Manager State",
|
||||
"name": "Backup Manager state",
|
||||
"state": {
|
||||
"idle": "Idle",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"create_backup": "Creating a backup",
|
||||
"receive_backup": "Receiving a backup",
|
||||
"restore_backup": "Restoring a backup"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Balay virtual integration."""
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "balay",
|
||||
"name": "Balay",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -132,7 +132,7 @@
|
||||
"name": "Charging",
|
||||
"state": {
|
||||
"off": "Not charging",
|
||||
"on": "Charging"
|
||||
"on": "[%key:common::state::charging%]"
|
||||
}
|
||||
},
|
||||
"carbon_monoxide": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"vehicle_status": {
|
||||
"name": "Vehicle status",
|
||||
"state": {
|
||||
"standby": "Standby",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"vehicle_detected": "Detected",
|
||||
"ready": "Ready",
|
||||
"no_power": "No power",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "ConnectedDrive Region"
|
||||
"region": "ConnectedDrive region"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The email address of your MyBMW/MINI Connected account.",
|
||||
@@ -113,10 +113,10 @@
|
||||
},
|
||||
"select": {
|
||||
"ac_limit": {
|
||||
"name": "AC Charging Limit"
|
||||
"name": "AC charging limit"
|
||||
},
|
||||
"charging_mode": {
|
||||
"name": "Charging Mode",
|
||||
"name": "Charging mode",
|
||||
"state": {
|
||||
"immediate_charging": "Immediate charging",
|
||||
"delayed_charging": "Delayed charging",
|
||||
@@ -181,7 +181,7 @@
|
||||
"cooling": "Cooling",
|
||||
"heating": "Heating",
|
||||
"inactive": "Inactive",
|
||||
"standby": "Standby",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"ventilation": "Ventilation"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,6 +142,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media, music track only."""
|
||||
if (
|
||||
not self.client.play_state.metadata.artist
|
||||
and self.client.state.source == "IR"
|
||||
):
|
||||
# Return channel instead of artist when playing internet radio
|
||||
return self.client.play_state.metadata.station
|
||||
return self.client.play_state.metadata.artist
|
||||
|
||||
@property
|
||||
@@ -169,6 +175,11 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Last time the media position was updated."""
|
||||
return self.client.position_last_updated
|
||||
|
||||
@property
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
return self.client.play_state.metadata.station
|
||||
|
||||
@property
|
||||
def is_volume_muted(self) -> bool | None:
|
||||
"""Volume mute status."""
|
||||
|
||||
@@ -4,13 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from hass_nabucasa import Cloud, CloudError
|
||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
|
||||
from hass_nabucasa.cloud_api import (
|
||||
FilesHandlerListEntry,
|
||||
async_files_delete_file,
|
||||
@@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
|
||||
"""
|
||||
if not backup.protected:
|
||||
raise BackupAgentError("Cloud backups must be protected")
|
||||
if self._cloud.subscription_expired:
|
||||
raise BackupAgentError("Cloud subscription has expired")
|
||||
|
||||
size = backup.size
|
||||
try:
|
||||
@@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
|
||||
) from err
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
except CloudError as err:
|
||||
if (
|
||||
isinstance(err, CloudApiError)
|
||||
and isinstance(err.orig_exc, ClientResponseError)
|
||||
and err.orig_exc.status == HTTPStatus.FORBIDDEN
|
||||
and self._cloud.subscription_expired
|
||||
):
|
||||
raise BackupAgentError("Cloud subscription has expired") from err
|
||||
if tries == _RETRY_LIMIT:
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
tries += 1
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
import pycfdns
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -118,8 +117,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
@@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
|
||||
|
||||
|
||||
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
||||
DISABLE: 0,
|
||||
HOME_P1: 1,
|
||||
HOME_P2: 2,
|
||||
NIGHT: 3,
|
||||
@@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
|
||||
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||
}.get(self._area.human_status)
|
||||
|
||||
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
|
||||
"""Update state after action."""
|
||||
self._area.human_status = area_state
|
||||
self._area.armed = armed
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code != str(self._api.device_pin):
|
||||
return
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -98,13 +98,20 @@ class ComelitCoverEntity(
|
||||
"""Return if the cover is opening."""
|
||||
return self._current_action("opening")
|
||||
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_OFF)
|
||||
await self._cover_set_state(STATE_OFF, 2)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_ON)
|
||||
await self._cover_set_state(STATE_ON, 1)
|
||||
|
||||
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
@@ -112,13 +119,7 @@ class ComelitCoverEntity(
|
||||
return
|
||||
|
||||
action = STATE_ON if self.is_closing else STATE_OFF
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle device update."""
|
||||
self._last_state = self.state
|
||||
self.async_write_ha_state()
|
||||
await self._cover_set_state(action, 0)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
||||
@@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
||||
async def _light_set_state(self, state: int) -> None:
|
||||
"""Set desired light state."""
|
||||
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[LIGHT][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
|
||||
@@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
||||
await self.coordinator.api.set_device_status(
|
||||
self._device.type, self._device.index, state
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self._device.type][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Constructa virtual integration."""
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "constructa",
|
||||
"name": "Constructa",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
if (
|
||||
(maybe_result is None) # first result
|
||||
or (num_matched_entities > best_num_matched_entities)
|
||||
or (
|
||||
# More literal text matched
|
||||
result.text_chunks_matched > maybe_result.text_chunks_matched
|
||||
)
|
||||
or (
|
||||
# More entities matched
|
||||
num_matched_entities > best_num_matched_entities
|
||||
)
|
||||
or (
|
||||
# Fewer unmatched entities
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
@@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||
)
|
||||
or (
|
||||
# More literal text matched
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||
and (
|
||||
result.text_chunks_matched
|
||||
> maybe_result.text_chunks_matched
|
||||
)
|
||||
)
|
||||
or (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.24"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "Email used to access your {cookidoo} account.",
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
"name": "[%key:component::cover::title%]",
|
||||
"state": {
|
||||
"open": "[%key:common::state::open%]",
|
||||
"opening": "Opening",
|
||||
"opening": "[%key:common::state::opening%]",
|
||||
"closed": "[%key:common::state::closed%]",
|
||||
"closing": "Closing",
|
||||
"stopped": "Stopped"
|
||||
"closing": "[%key:common::state::closing%]",
|
||||
"stopped": "[%key:common::state::stopped%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"current_position": {
|
||||
|
||||
@@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = auth["cdp_internal_user_id"].lower()
|
||||
username = auth["internalUserID"].lower()
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
email = auth["email"].lower()
|
||||
email = auth["loginEmailAddress"].lower()
|
||||
data = {
|
||||
CONF_EMAIL: email,
|
||||
CONF_USERNAME: username,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.2.2"]
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.3.1"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"step": {
|
||||
"auth": {
|
||||
"data": {
|
||||
"country": "Country",
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"override_rest_url": "REST URL",
|
||||
"override_mqtt_url": "MQTT URL",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -46,6 +46,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
type EmulatedRokuConfigEntry = ConfigEntry[EmulatedRoku]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the emulated roku component."""
|
||||
@@ -65,22 +67,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
|
||||
) -> bool:
|
||||
"""Set up an emulated roku server from a config entry."""
|
||||
config = config_entry.data
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
name = config[CONF_NAME]
|
||||
listen_port = config[CONF_LISTEN_PORT]
|
||||
host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
|
||||
advertise_ip = config.get(CONF_ADVERTISE_IP)
|
||||
advertise_port = config.get(CONF_ADVERTISE_PORT)
|
||||
upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST)
|
||||
config = entry.data
|
||||
name: str = config[CONF_NAME]
|
||||
listen_port: int = config[CONF_LISTEN_PORT]
|
||||
host_ip: str = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
|
||||
advertise_ip: str | None = config.get(CONF_ADVERTISE_IP)
|
||||
advertise_port: int | None = config.get(CONF_ADVERTISE_PORT)
|
||||
upnp_bind_multicast: bool | None = config.get(CONF_UPNP_BIND_MULTICAST)
|
||||
|
||||
server = EmulatedRoku(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
name,
|
||||
host_ip,
|
||||
listen_port,
|
||||
@@ -88,14 +89,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
advertise_port,
|
||||
upnp_bind_multicast,
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][name] = server
|
||||
|
||||
entry.runtime_data = server
|
||||
return await server.setup()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
server = hass.data[DOMAIN].pop(name)
|
||||
return await server.unload()
|
||||
return await entry.runtime_data.unload()
|
||||
|
||||
@@ -5,7 +5,13 @@ import logging
|
||||
from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CoreState, EventOrigin
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
CoreState,
|
||||
Event,
|
||||
EventOrigin,
|
||||
HomeAssistant,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
@@ -27,16 +33,18 @@ class EmulatedRoku:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
name,
|
||||
host_ip,
|
||||
listen_port,
|
||||
advertise_ip,
|
||||
advertise_port,
|
||||
upnp_bind_multicast,
|
||||
):
|
||||
hass: HomeAssistant,
|
||||
entry_id: str,
|
||||
name: str,
|
||||
host_ip: str,
|
||||
listen_port: int,
|
||||
advertise_ip: str | None,
|
||||
advertise_port: int | None,
|
||||
upnp_bind_multicast: bool | None,
|
||||
) -> None:
|
||||
"""Initialize the properties."""
|
||||
self.hass = hass
|
||||
self.entry_id = entry_id
|
||||
|
||||
self.roku_usn = name
|
||||
self.host_ip = host_ip
|
||||
@@ -47,21 +55,21 @@ class EmulatedRoku:
|
||||
|
||||
self.bind_multicast = upnp_bind_multicast
|
||||
|
||||
self._api_server = None
|
||||
self._api_server: EmulatedRokuServer | None = None
|
||||
|
||||
self._unsub_start_listener = None
|
||||
self._unsub_stop_listener = None
|
||||
self._unsub_start_listener: CALLBACK_TYPE | None = None
|
||||
self._unsub_stop_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
async def setup(self):
|
||||
async def setup(self) -> bool:
|
||||
"""Start the emulated_roku server."""
|
||||
|
||||
class EventCommandHandler(EmulatedRokuCommandHandler):
|
||||
"""emulated_roku command handler to turn commands into events."""
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
self.hass = hass
|
||||
|
||||
def on_keydown(self, roku_usn, key):
|
||||
def on_keydown(self, roku_usn: str, key: str) -> None:
|
||||
"""Handle keydown event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -73,7 +81,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def on_keyup(self, roku_usn, key):
|
||||
def on_keyup(self, roku_usn: str, key: str) -> None:
|
||||
"""Handle keyup event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -85,7 +93,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def on_keypress(self, roku_usn, key):
|
||||
def on_keypress(self, roku_usn: str, key: str) -> None:
|
||||
"""Handle keypress event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -97,7 +105,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def launch(self, roku_usn, app_id):
|
||||
def launch(self, roku_usn: str, app_id: str) -> None:
|
||||
"""Handle launch event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -129,17 +137,19 @@ class EmulatedRoku:
|
||||
bind_multicast=self.bind_multicast,
|
||||
)
|
||||
|
||||
async def emulated_roku_stop(event):
|
||||
async def emulated_roku_stop(event: Event | None) -> None:
|
||||
"""Wrap the call to emulated_roku.close."""
|
||||
LOGGER.debug("Stopping emulated_roku %s", self.roku_usn)
|
||||
self._unsub_stop_listener = None
|
||||
assert self._api_server is not None
|
||||
await self._api_server.close()
|
||||
|
||||
async def emulated_roku_start(event):
|
||||
async def emulated_roku_start(event: Event | None) -> None:
|
||||
"""Wrap the call to emulated_roku.start."""
|
||||
try:
|
||||
LOGGER.debug("Starting emulated_roku %s", self.roku_usn)
|
||||
self._unsub_start_listener = None
|
||||
assert self._api_server is not None
|
||||
await self._api_server.start()
|
||||
except OSError:
|
||||
LOGGER.exception(
|
||||
@@ -165,7 +175,7 @@ class EmulatedRoku:
|
||||
|
||||
return True
|
||||
|
||||
async def unload(self):
|
||||
async def unload(self) -> bool:
|
||||
"""Unload the emulated_roku server."""
|
||||
LOGGER.debug("Unloading emulated_roku %s", self.roku_usn)
|
||||
|
||||
@@ -177,6 +187,7 @@ class EmulatedRoku:
|
||||
self._unsub_stop_listener()
|
||||
self._unsub_stop_listener = None
|
||||
|
||||
assert self._api_server is not None
|
||||
await self._api_server.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"country": "Country"
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -310,12 +310,13 @@ class EsphomeAssistSatellite(
|
||||
self.entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER:
|
||||
media_id = tts_output["media_id"]
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER and (
|
||||
stream := tts.async_get_stream(self.hass, tts_output["token"])
|
||||
):
|
||||
self._tts_streaming_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._stream_tts_audio(media_id),
|
||||
self._stream_tts_audio(stream),
|
||||
"esphome_voice_assistant_tts",
|
||||
)
|
||||
)
|
||||
@@ -564,7 +565,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
async def _stream_tts_audio(
|
||||
self,
|
||||
media_id: str,
|
||||
tts_result: tts.ResultStream,
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
sample_channels: int = 1,
|
||||
@@ -579,15 +580,14 @@ class EsphomeAssistSatellite(
|
||||
if not self._is_running:
|
||||
return
|
||||
|
||||
extension, data = await tts.async_get_media_source_audio(
|
||||
self.hass,
|
||||
media_id,
|
||||
)
|
||||
|
||||
if extension != "wav":
|
||||
_LOGGER.error("Only WAV audio can be streamed, got %s", extension)
|
||||
if tts_result.extension != "wav":
|
||||
_LOGGER.error(
|
||||
"Only WAV audio can be streamed, got %s", tts_result.extension
|
||||
)
|
||||
return
|
||||
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
if (
|
||||
(wav_file.getframerate() != sample_rate)
|
||||
|
||||
@@ -282,15 +282,18 @@ class RuntimeEntryData:
|
||||
) -> None:
|
||||
"""Distribute an update of static infos to all platforms."""
|
||||
# First, load all platforms
|
||||
needed_platforms = set()
|
||||
if async_get_dashboard(hass):
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
needed_platforms: set[Platform] = set()
|
||||
|
||||
if self.device_info and self.device_info.voice_assistant_feature_flags_compat(
|
||||
self.api_version
|
||||
):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
if self.device_info:
|
||||
if async_get_dashboard(hass):
|
||||
# Only load the update platform if the device_info is set
|
||||
# When we restore the entry, the device_info may not be set yet
|
||||
# and we don't want to load the update platform since it needs
|
||||
# a complete device_info.
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
if self.device_info.voice_assistant_feature_flags_compat(self.api_version):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
registry_get_entity = ent_reg.async_get_entity_id
|
||||
@@ -312,18 +315,19 @@ class RuntimeEntryData:
|
||||
|
||||
# Make a dict of the EntityInfo by type and send
|
||||
# them to the listeners for each specific EntityInfo type
|
||||
infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {}
|
||||
infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict(
|
||||
list
|
||||
)
|
||||
for info in infos:
|
||||
info_type = type(info)
|
||||
if info_type not in infos_by_type:
|
||||
infos_by_type[info_type] = []
|
||||
infos_by_type[info_type].append(info)
|
||||
infos_by_type[type(info)].append(info)
|
||||
|
||||
callbacks_by_type = self.entity_info_callbacks
|
||||
for type_, entity_infos in infos_by_type.items():
|
||||
if callbacks_ := callbacks_by_type.get(type_):
|
||||
for callback_ in callbacks_:
|
||||
callback_(entity_infos)
|
||||
for type_, callbacks in self.entity_info_callbacks.items():
|
||||
# If all entities for a type are removed, we
|
||||
# still need to call the callbacks with an empty list
|
||||
# to make sure the entities are removed.
|
||||
entity_infos = infos_by_type.get(type_, [])
|
||||
for callback_ in callbacks:
|
||||
callback_(entity_infos)
|
||||
|
||||
# Finally update static info subscriptions
|
||||
for callback_ in self.static_info_update_subscriptions:
|
||||
|
||||
@@ -33,6 +33,16 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity):
|
||||
self._trigger_event(self._state.event_type)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Call when device updates or entry data changes."""
|
||||
super()._on_device_update()
|
||||
if self._entry_data.available:
|
||||
# Event entities should go available directly
|
||||
# when the device comes online and not wait
|
||||
# for the next data push.
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
|
||||
@@ -193,7 +193,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@@ -201,7 +200,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@@ -225,6 +223,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@@ -232,6 +231,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
ClimateEntity,
|
||||
@@ -38,7 +39,7 @@ from .sensor import value_scheduled_preset
|
||||
HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF]
|
||||
PRESET_HOLIDAY = "holiday"
|
||||
PRESET_SUMMER = "summer"
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
SUPPORTED_FEATURES = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
@@ -194,6 +195,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return PRESET_HOLIDAY
|
||||
if self.data.summer_active:
|
||||
return PRESET_SUMMER
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
||||
return PRESET_BOOST
|
||||
if self.data.target_temperature == self.data.comfort_temperature:
|
||||
return PRESET_COMFORT
|
||||
if self.data.target_temperature == self.data.eco_temperature:
|
||||
@@ -211,6 +214,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
elif preset_mode == PRESET_BOOST:
|
||||
await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
|
||||
@@ -182,10 +182,10 @@
|
||||
"state": {
|
||||
"startup": "Startup",
|
||||
"running": "Running",
|
||||
"standby": "Standby",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"bootloading": "Bootloading",
|
||||
"error": "Error",
|
||||
"idle": "Idle",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"ready": "Ready",
|
||||
"sleeping": "Sleeping"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250326.0"]
|
||||
"requirements": ["home-assistant-frontend==20250328.0"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Gaggenau virtual integration."""
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "gaggenau",
|
||||
"name": "Gaggenau",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.1"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
@@ -43,7 +43,7 @@ CONF_FILENAMES = "filenames"
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (Platform.CONVERSATION,)
|
||||
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client]
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[Client]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -139,7 +139,11 @@ async def async_setup_entry(
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
|
||||
try:
|
||||
client = genai.Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
def _init_client() -> Client:
|
||||
return Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
await client.aio.models.get(
|
||||
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google import genai
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
"name": "Panel light"
|
||||
},
|
||||
"quiet": {
|
||||
"name": "Quiet"
|
||||
"name": "Quiet mode"
|
||||
},
|
||||
"fresh_air": {
|
||||
"name": "Fresh air"
|
||||
},
|
||||
"xfan": {
|
||||
"name": "XFan"
|
||||
"name": "Xtra fan"
|
||||
},
|
||||
"health_mode": {
|
||||
"name": "Health mode"
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_QUEUE_IDS = "queue_ids"
|
||||
DOMAIN = "heos"
|
||||
ENTRY_TITLE = "HEOS System"
|
||||
SERVICE_GET_QUEUE = "get_queue"
|
||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
||||
SERVICE_SIGN_IN = "sign_in"
|
||||
SERVICE_SIGN_OUT = "sign_out"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"get_queue": {
|
||||
"service": "mdi:playlist-music"
|
||||
},
|
||||
"remove_from_queue": {
|
||||
"service": "mdi:playlist-remove"
|
||||
},
|
||||
"group_volume_set": {
|
||||
"service": "mdi:volume-medium"
|
||||
},
|
||||
|
||||
@@ -24,12 +24,10 @@ from pyheos import (
|
||||
const as heos_const,
|
||||
)
|
||||
from pyheos.util import mediauri as heos_source
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
@@ -43,30 +41,16 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.components.media_source import BrowseMediaSource
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
DOMAIN as HEOS_DOMAIN,
|
||||
SERVICE_GET_QUEUE,
|
||||
SERVICE_GROUP_VOLUME_DOWN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
SERVICE_GROUP_VOLUME_UP,
|
||||
)
|
||||
from . import services
|
||||
from .const import DOMAIN as HEOS_DOMAIN
|
||||
from .coordinator import HeosConfigEntry, HeosCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -137,25 +121,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add media players for a config entry."""
|
||||
# Register custom entity services
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GET_QUEUE,
|
||||
None,
|
||||
"async_get_queue",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
|
||||
"async_set_group_volume_level",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_DOWN, None, "async_group_volume_down"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_UP, None, "async_group_volume_up"
|
||||
)
|
||||
services.register_media_player_services()
|
||||
|
||||
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
||||
"""Add entities for each player."""
|
||||
@@ -387,6 +353,15 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
await self._player.play_preset_station(index)
|
||||
return
|
||||
|
||||
if media_type == "queue":
|
||||
# media_id must be an int
|
||||
try:
|
||||
queue_id = int(media_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid queue id '{media_id}'") from None
|
||||
await self._player.play_queue(queue_id)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unsupported media type '{media_type}'")
|
||||
|
||||
@catch_action_error("select source")
|
||||
@@ -500,6 +475,10 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
await self.coordinator.heos.set_group(new_members)
|
||||
return
|
||||
|
||||
async def async_remove_from_queue(self, queue_ids: list[int]) -> None:
|
||||
"""Remove items from the queue."""
|
||||
await self._player.remove_from_queue(queue_ids)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if the device is available."""
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
"""Services for the HEOS integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from pyheos import CommandAuthenticationError, Heos, HeosError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType, VolSchemaType
|
||||
|
||||
from .const import (
|
||||
ATTR_PASSWORD,
|
||||
ATTR_QUEUE_IDS,
|
||||
ATTR_USERNAME,
|
||||
DOMAIN,
|
||||
SERVICE_GET_QUEUE,
|
||||
SERVICE_GROUP_VOLUME_DOWN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
SERVICE_GROUP_VOLUME_UP,
|
||||
SERVICE_REMOVE_FROM_QUEUE,
|
||||
SERVICE_SIGN_IN,
|
||||
SERVICE_SIGN_OUT,
|
||||
)
|
||||
@@ -44,6 +58,62 @@ def register(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntityServiceDescription:
|
||||
"""Describe an entity service."""
|
||||
|
||||
name: str
|
||||
method_name: str
|
||||
schema: VolDictType | VolSchemaType | None = None
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE
|
||||
|
||||
def async_register(self, platform: entity_platform.EntityPlatform) -> None:
|
||||
"""Register the service with the platform."""
|
||||
platform.async_register_entity_service(
|
||||
self.name,
|
||||
self.schema,
|
||||
self.method_name,
|
||||
supports_response=self.supports_response,
|
||||
)
|
||||
|
||||
|
||||
REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_QUEUE_IDS): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.All(cv.positive_int, vol.Range(min=1))],
|
||||
vol.Unique(),
|
||||
)
|
||||
}
|
||||
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
|
||||
}
|
||||
|
||||
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
||||
# Player queue services
|
||||
EntityServiceDescription(
|
||||
SERVICE_GET_QUEUE, "async_get_queue", supports_response=SupportsResponse.ONLY
|
||||
),
|
||||
EntityServiceDescription(
|
||||
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
|
||||
),
|
||||
# Group volume services
|
||||
EntityServiceDescription(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
"async_set_group_volume_level",
|
||||
GROUP_VOLUME_SET_SCHEMA,
|
||||
),
|
||||
EntityServiceDescription(SERVICE_GROUP_VOLUME_DOWN, "async_group_volume_down"),
|
||||
EntityServiceDescription(SERVICE_GROUP_VOLUME_UP, "async_group_volume_up"),
|
||||
)
|
||||
|
||||
|
||||
def register_media_player_services() -> None:
|
||||
"""Register media_player entity services."""
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
for service in MEDIA_PLAYER_ENTITY_SERVICES:
|
||||
service.async_register(platform)
|
||||
|
||||
|
||||
def _get_controller(hass: HomeAssistant) -> Heos:
|
||||
"""Get the HEOS controller instance."""
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -4,6 +4,19 @@ get_queue:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
|
||||
remove_from_queue:
|
||||
target:
|
||||
entity:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
fields:
|
||||
queue_ids:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
type: number
|
||||
|
||||
group_volume_set:
|
||||
target:
|
||||
entity:
|
||||
|
||||
@@ -90,6 +90,16 @@
|
||||
"name": "Get queue",
|
||||
"description": "Retrieves the queue of the media player."
|
||||
},
|
||||
"remove_from_queue": {
|
||||
"name": "Remove from queue",
|
||||
"description": "Removes items from the play queue.",
|
||||
"fields": {
|
||||
"queue_ids": {
|
||||
"name": "Queue IDs",
|
||||
"description": "The IDs (indexes) of the items in the queue to remove."
|
||||
}
|
||||
}
|
||||
},
|
||||
"group_volume_down": {
|
||||
"name": "Turn down group volume",
|
||||
"description": "Turns down the group volume."
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -244,6 +244,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
BSH_DOOR_STATE_LOCKED: False,
|
||||
BSH_DOOR_STATE_OPEN: True,
|
||||
},
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
|
||||
@@ -283,7 +284,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
DOMAIN,
|
||||
f"deprecated_binary_common_door_sensor_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.5.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_binary_common_door_sensor",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from asyncio import sleep as asyncio_sleep
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@@ -119,8 +120,11 @@ class HomeConnectCoordinator(
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
def remove_listener_and_invalidate_context_listeners() -> None:
|
||||
remove_listener()
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
# There are cases where the remove_listener will be called
|
||||
# although it has been already removed somewhere else
|
||||
with suppress(KeyError):
|
||||
remove_listener()
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
return remove_listener_and_invalidate_context_listeners
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Provides a sensor for Home Connect."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
@@ -14,7 +17,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -42,7 +45,6 @@ class HomeConnectSensorEntityDescription(
|
||||
):
|
||||
"""Entity Description class for sensors."""
|
||||
|
||||
default_value: str | None = None
|
||||
appliance_types: tuple[str, ...] | None = None
|
||||
fetch_unit: bool = False
|
||||
|
||||
@@ -198,7 +200,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_PROGRAM_ABORTED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="program_aborted",
|
||||
appliance_types=("Dishwasher", "CleaningRobot", "CookProcessor"),
|
||||
),
|
||||
@@ -206,7 +207,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_PROGRAM_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="program_finished",
|
||||
appliance_types=(
|
||||
"Oven",
|
||||
@@ -222,7 +222,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_ALARM_CLOCK_ELAPSED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="alarm_clock_elapsed",
|
||||
appliance_types=("Oven", "Cooktop"),
|
||||
),
|
||||
@@ -230,7 +229,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_OVEN_EVENT_PREHEAT_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="preheat_finished",
|
||||
appliance_types=("Oven", "Cooktop"),
|
||||
),
|
||||
@@ -238,7 +236,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_OVEN_EVENT_REGULAR_PREHEAT_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="regular_preheat_finished",
|
||||
appliance_types=("Oven",),
|
||||
),
|
||||
@@ -246,7 +243,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_DRYER_EVENT_DRYING_PROCESS_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="drying_process_finished",
|
||||
appliance_types=("Dryer",),
|
||||
),
|
||||
@@ -254,7 +250,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="salt_nearly_empty",
|
||||
appliance_types=("Dishwasher",),
|
||||
),
|
||||
@@ -262,7 +257,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="rinse_aid_nearly_empty",
|
||||
appliance_types=("Dishwasher",),
|
||||
),
|
||||
@@ -270,7 +264,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="bean_container_empty",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -278,7 +271,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="water_tank_empty",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -286,7 +278,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="drip_tray_full",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -294,7 +285,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_KEEP_MILK_TANK_COOL,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="keep_milk_tank_cool",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -302,7 +292,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_20_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_20_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -310,7 +299,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_15_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_15_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -318,7 +306,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_10_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_10_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -326,7 +313,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_5_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_5_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -334,7 +320,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_DESCALED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_descaled",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -342,7 +327,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_descaling_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -350,7 +334,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_BLOCKAGE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_descaling_blockage",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -358,7 +341,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CLEANED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_cleaned",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -366,7 +348,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CLEANING_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_cleaning_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -374,7 +355,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN20CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in20cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -382,7 +362,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN15CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in15cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -390,7 +369,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN10CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in10cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -398,7 +376,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN5CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in5cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -406,7 +383,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CALC_N_CLEANED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_calc_n_cleaned",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -414,7 +390,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_calc_n_clean_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -422,7 +397,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_BLOCKAGE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_calc_n_clean_blockage",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -430,7 +404,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="freezer_door_alarm",
|
||||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
@@ -438,7 +411,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="refrigerator_door_alarm",
|
||||
appliance_types=("FridgeFreezer", "Refrigerator"),
|
||||
),
|
||||
@@ -446,7 +418,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="freezer_temperature_alarm",
|
||||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
@@ -454,7 +425,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_EMPTY_DUST_BOX_AND_CLEAN_FILTER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="empty_dust_box_and_clean_filter",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -462,7 +432,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_ROBOT_IS_STUCK,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="robot_is_stuck",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -470,7 +439,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_DOCKING_STATION_NOT_FOUND,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="docking_station_not_found",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -478,7 +446,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="poor_i_dos_1_fill_level",
|
||||
appliance_types=("Washer", "WasherDryer"),
|
||||
),
|
||||
@@ -486,7 +453,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_2_FILL_LEVEL_POOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="poor_i_dos_2_fill_level",
|
||||
appliance_types=("Washer", "WasherDryer"),
|
||||
),
|
||||
@@ -494,7 +460,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_NEARLY_REACHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="grease_filter_max_saturation_nearly_reached",
|
||||
appliance_types=("Hood",),
|
||||
),
|
||||
@@ -502,7 +467,6 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_REACHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="grease_filter_max_saturation_reached",
|
||||
appliance_types=("Hood",),
|
||||
),
|
||||
@@ -515,12 +479,6 @@ def _get_entities_for_appliance(
|
||||
) -> list[HomeConnectEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
*[
|
||||
HomeConnectEventSensor(entry.runtime_data, appliance, description)
|
||||
for description in EVENT_SENSORS
|
||||
if description.appliance_types
|
||||
and appliance.info.type in description.appliance_types
|
||||
],
|
||||
*[
|
||||
HomeConnectProgramSensor(entry.runtime_data, appliance, desc)
|
||||
for desc in BSH_PROGRAM_SENSORS
|
||||
@@ -534,6 +492,72 @@ def _get_entities_for_appliance(
|
||||
]
|
||||
|
||||
|
||||
def _add_event_sensor_entity(
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
appliance: HomeConnectApplianceData,
|
||||
description: HomeConnectSensorEntityDescription,
|
||||
remove_event_sensor_listener_list: list[Callable[[], None]],
|
||||
) -> None:
|
||||
"""Add an event sensor entity."""
|
||||
if (
|
||||
(appliance_data := entry.runtime_data.data.get(appliance.info.ha_id)) is None
|
||||
) or description.key not in appliance_data.events:
|
||||
return
|
||||
|
||||
for remove_listener in remove_event_sensor_listener_list:
|
||||
remove_listener()
|
||||
async_add_entities(
|
||||
[
|
||||
HomeConnectEventSensor(entry.runtime_data, appliance, description),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _add_event_sensor_listeners(
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]],
|
||||
) -> None:
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
if appliance.info.ha_id in remove_event_sensor_listener_dict:
|
||||
continue
|
||||
for event_sensor_description in EVENT_SENSORS:
|
||||
if appliance.info.type not in cast(
|
||||
tuple[str, ...], event_sensor_description.appliance_types
|
||||
):
|
||||
continue
|
||||
# We use a list as a kind of lazy initializer, as we can use the
|
||||
# remove_listener while we are initializing it.
|
||||
remove_event_sensor_listener_list = remove_event_sensor_listener_dict[
|
||||
appliance.info.ha_id
|
||||
]
|
||||
remove_listener = entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_add_event_sensor_entity,
|
||||
entry,
|
||||
async_add_entities,
|
||||
appliance,
|
||||
event_sensor_description,
|
||||
remove_event_sensor_listener_list,
|
||||
),
|
||||
(appliance.info.ha_id, event_sensor_description.key),
|
||||
)
|
||||
remove_event_sensor_listener_list.append(remove_listener)
|
||||
entry.async_on_unload(remove_listener)
|
||||
|
||||
|
||||
def _remove_event_sensor_listeners_on_depaired(
|
||||
entry: HomeConnectConfigEntry,
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]],
|
||||
) -> None:
|
||||
registered_listeners_ha_id = set(remove_event_sensor_listener_dict)
|
||||
actual_appliances = set(entry.runtime_data.data)
|
||||
for appliance_ha_id in registered_listeners_ha_id - actual_appliances:
|
||||
for listener in remove_event_sensor_listener_dict.pop(appliance_ha_id):
|
||||
listener()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -546,6 +570,32 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]] = defaultdict(
|
||||
list
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_add_event_sensor_listeners,
|
||||
entry,
|
||||
async_add_entities,
|
||||
remove_event_sensor_listener_dict,
|
||||
),
|
||||
(EventKey.BSH_COMMON_APPLIANCE_PAIRED,),
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_remove_event_sensor_listeners_on_depaired,
|
||||
entry,
|
||||
remove_event_sensor_listener_dict,
|
||||
),
|
||||
(EventKey.BSH_COMMON_APPLIANCE_DEPAIRED,),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
||||
"""Sensor class for Home Connect."""
|
||||
@@ -650,8 +700,5 @@ class HomeConnectEventSensor(HomeConnectSensor):
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the sensor's status."""
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
if event:
|
||||
self._update_native_value(event.value)
|
||||
elif not self._attr_native_value:
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
event = self.appliance.events[cast(EventKey, self.bsh_key)]
|
||||
self._update_native_value(event.value)
|
||||
|
||||
@@ -64,7 +64,6 @@ set_program_and_options:
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
|
||||
@@ -134,15 +134,47 @@
|
||||
},
|
||||
"deprecated_binary_common_door_sensor": {
|
||||
"title": "Deprecated binary door sensor detected in some automations or scripts",
|
||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
|
||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_command_actions": {
|
||||
"title": "The command related actions are deprecated in favor of the new buttons",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch_in_automations_scripts": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
"title": "Deprecated program switch entities",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
|
||||
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_set_program_and_option_actions": {
|
||||
"title": "The executed action is deprecated",
|
||||
|
||||
@@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
|
||||
SwitchEntityDescription(
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_name = f"{appliance.info.name} {desc}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
|
||||
@@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_key="deprecated_program_switch_in_automations_scripts",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"items": "\n".join(items_list),
|
||||
@@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
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_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
)
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
|
||||
)
|
||||
|
||||
def create_action_handler_issue(self) -> None:
|
||||
"""Create deprecation issue."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.start_program(
|
||||
self.appliance.info.ha_id, program_key=self.program.key
|
||||
@@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
|
||||
except HomeConnectError as err:
|
||||
|
||||
@@ -31,7 +31,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
||||
@@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity(
|
||||
# This entity is not currently associated with a device so we must manually
|
||||
# give it a name
|
||||
self._attr_name = f"{self._config_entry.title} Update"
|
||||
self._attr_title = self.entity_description.firmware_name or "unknown"
|
||||
self._attr_title = self.entity_description.firmware_name or "Unknown"
|
||||
|
||||
if (
|
||||
self._current_firmware_info is None
|
||||
|
||||
@@ -15,14 +15,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -21,11 +21,20 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import FIRMWARE, FIRMWARE_VERSION, NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
HardwareVariant,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,7 +51,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="skyconnect_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
@@ -55,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="skyconnect_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
@@ -77,9 +108,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=config_entry.data["device"],
|
||||
@@ -130,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
bootloader_reset_type = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -141,8 +180,18 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Initialize the SkyConnect firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._config_entry.data['serial_number']}_{self.entity_description.key}"
|
||||
variant = HardwareVariant.from_usb_product_name(
|
||||
self._config_entry.data[PRODUCT]
|
||||
)
|
||||
serial_number = self._config_entry.data[SERIAL_NUMBER]
|
||||
|
||||
self._attr_unique_id = f"{serial_number}_{self.entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"{variant.full_name} ({serial_number[:8]})",
|
||||
model=variant.full_name,
|
||||
manufacturer="Nabu Casa",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
@@ -155,6 +204,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_sky_connect",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
||||
@@ -62,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
DOMAIN = "homeassistant_yellow"
|
||||
|
||||
RADIO_MODEL = "Home Assistant Yellow"
|
||||
RADIO_MANUFACTURER = "Nabu Casa"
|
||||
MODEL = "Home Assistant Yellow"
|
||||
MANUFACTURER = "Nabu Casa"
|
||||
|
||||
RADIO_DEVICE = "/dev/ttyAMA1"
|
||||
|
||||
ZHA_HW_DISCOVERY_DATA = {
|
||||
|
||||
@@ -149,5 +149,12 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"firmware": {
|
||||
"name": "Radio firmware"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
MODEL,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
)
|
||||
@@ -39,7 +43,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
ApplicationType | None, FirmwareUpdateEntityDescription
|
||||
] = {
|
||||
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -47,10 +51,10 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="yellow_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -60,12 +64,34 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="yellow_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None,
|
||||
version_key=None,
|
||||
expected_firmware_type=None,
|
||||
@@ -82,9 +108,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=RADIO_DEVICE,
|
||||
@@ -135,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
bootloader_reset_type = "yellow" # Triggers a GPIO reset
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -145,8 +179,13 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize the Yellow firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = self.entity_description.key
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, "yellow")},
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
if self._config_entry.data[FIRMWARE] is not None:
|
||||
@@ -158,6 +197,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_yellow",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
||||
@@ -297,8 +297,8 @@
|
||||
"open": "[%key:common::state::open%]",
|
||||
"closed": "[%key:common::state::closed%]",
|
||||
"partial": "Partially open",
|
||||
"opening": "Opening",
|
||||
"closing": "Closing"
|
||||
"opening": "[%key:common::state::opening%]",
|
||||
"closing": "[%key:common::state::closing%]"
|
||||
}
|
||||
},
|
||||
"uv": {
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.8"],
|
||||
"requirements": ["aiohomekit==3.2.13"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"air_purifier_state_current": {
|
||||
"state": {
|
||||
"inactive": "Inactive",
|
||||
"idle": "Idle",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"purifying": "Purifying"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_controller_id": {
|
||||
"message": "Invalid controller_id \"{controller_id}\", expected one of \"{controller_ids}\""
|
||||
"message": "Invalid controller ID \"{controller_id}\", expected one of \"{controller_ids}\""
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
{
|
||||
"entity": {
|
||||
"light": {
|
||||
"hue_light": {
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"state": {
|
||||
"candle": "mdi:candle",
|
||||
"sparkle": "mdi:shimmer",
|
||||
"glisten": "mdi:creation",
|
||||
"sunrise": "mdi:weather-sunset-up",
|
||||
"sunset": "mdi:weather-sunset",
|
||||
"fire": "mdi:fire",
|
||||
"prism": "mdi:triangle-outline",
|
||||
"opal": "mdi:diamond-stone",
|
||||
"underwater": "mdi:waves",
|
||||
"cosmos": "mdi:star-shooting",
|
||||
"sunbeam": "mdi:spotlight-beam",
|
||||
"enchant": "mdi:magic-staff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"hue_activate_scene": {
|
||||
"service": "mdi:palette"
|
||||
|
||||
@@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
@callback
|
||||
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
||||
"""Return the name of the current work area."""
|
||||
if data.mower.work_area_id is None:
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if values are None
|
||||
assert data.work_areas is not None
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
if (
|
||||
data.mower.work_area_id is not None
|
||||
and data.mower.work_area_id in data.work_areas
|
||||
):
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -32,7 +32,6 @@ class AqualinkEntity(Entity):
|
||||
manufacturer=dev.manufacturer,
|
||||
model=dev.model,
|
||||
name=dev.label,
|
||||
via_device=(DOMAIN, dev.system.serial),
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"],
|
||||
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
|
||||
"requirements": ["iaqualink==0.5.3", "h2==4.1.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"entity": {
|
||||
"button": {
|
||||
"connect": {
|
||||
"name": "Connect"
|
||||
"name": "[%key:common::action::connect%]"
|
||||
},
|
||||
"disconnect": {
|
||||
"name": "Disconnect"
|
||||
"name": "[%key:common::action::disconnect%]"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
"tapwater_int": "Tap water internal",
|
||||
"sensor_test": "Sensor test",
|
||||
"central_heating": "Central heating",
|
||||
"standby": "Stand-by",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"postrun_boyler": "Post run boiler",
|
||||
"service": "Service",
|
||||
"tapwater": "Tap water",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"state": {
|
||||
"printing": "Printing",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"stopped": "Stopped"
|
||||
"stopped": "[%key:common::state::stopped%]"
|
||||
}
|
||||
},
|
||||
"uptime": {
|
||||
|
||||
@@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
for vtype, _, vid in isy.variables.children:
|
||||
numbers.append(isy.variables[vtype][vid])
|
||||
if (
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL]
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf.get(CONFIG_PORTAL)
|
||||
) and isy.networking.nobjs:
|
||||
isy_data.devices[CONF_NETWORK] = _create_service_device_info(
|
||||
isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.1.14"],
|
||||
"requirements": ["pyisy==3.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
||||
@@ -6,6 +6,7 @@ count_omer:
|
||||
selector:
|
||||
date:
|
||||
nusach:
|
||||
required: true
|
||||
example: "sfarad"
|
||||
default: "sfarad"
|
||||
selector:
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"sensor": {
|
||||
"hebrew_date": {
|
||||
"state_attributes": {
|
||||
"hebrew_year": { "name": "Hebrew Year" },
|
||||
"hebrew_month_name": { "name": "Hebrew Month Name" },
|
||||
"hebrew_day": { "name": "Hebrew Day" }
|
||||
"hebrew_year": { "name": "Hebrew year" },
|
||||
"hebrew_month_name": { "name": "Hebrew month name" },
|
||||
"hebrew_day": { "name": "Hebrew day" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"diaspora": "Outside of Israel?",
|
||||
"language": "Language for Holidays and Dates",
|
||||
"language": "Language for holidays and dates",
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"elevation": "[%key:common::config_flow::data::elevation%]",
|
||||
"time_zone": "Time Zone"
|
||||
"time_zone": "Time zone"
|
||||
},
|
||||
"data_description": {
|
||||
"time_zone": "If you specify a location, make sure to specify the time zone for correct calendar times calculations"
|
||||
@@ -36,7 +36,7 @@
|
||||
"init": {
|
||||
"title": "Configure options for Jewish Calendar",
|
||||
"data": {
|
||||
"candle_lighting_minutes_before_sunset": "Minutes before sunset for candle lighthing",
|
||||
"candle_lighting_minutes_before_sunset": "Minutes before sunset for candle lighting",
|
||||
"havdalah_minutes_after_sunset": "Minutes after sunset for Havdalah"
|
||||
},
|
||||
"data_description": {
|
||||
@@ -70,7 +70,7 @@
|
||||
"description": "Nusach to count the Omer in."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"name": "[%key:common::config_flow::data::language%]",
|
||||
"description": "Language to count the Omer in."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,10 +316,10 @@
|
||||
"name": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::name%]",
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"building_protection": "Building protection",
|
||||
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
||||
"standby": "Standby",
|
||||
"economy": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
|
||||
"building_protection": "Building protection"
|
||||
"standby": "[%key:common::state::standby%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"import_confirm": {
|
||||
"title": "Import Konnected Device",
|
||||
"description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
"title": "Import Konnected device",
|
||||
"description": "A Konnected alarm panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
},
|
||||
"user": {
|
||||
"description": "Please enter the host information for your Konnected Panel.",
|
||||
"description": "Please enter the host information for your Konnected panel.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Konnected Device Ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings."
|
||||
"title": "Konnected device ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected alarm panel settings."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -45,8 +45,8 @@
|
||||
}
|
||||
},
|
||||
"options_io_ext": {
|
||||
"title": "Configure Extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"title": "Configure extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"data": {
|
||||
"8": "Zone 8",
|
||||
"9": "Zone 9",
|
||||
@@ -59,25 +59,25 @@
|
||||
}
|
||||
},
|
||||
"options_binary": {
|
||||
"title": "Configure Binary Sensor",
|
||||
"title": "Configure binary sensor",
|
||||
"description": "{zone} options",
|
||||
"data": {
|
||||
"type": "Binary Sensor Type",
|
||||
"type": "Binary sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"inverse": "Invert the open/close state"
|
||||
}
|
||||
},
|
||||
"options_digital": {
|
||||
"title": "Configure Digital Sensor",
|
||||
"title": "Configure digital sensor",
|
||||
"description": "[%key:component::konnected::options::step::options_binary::description%]",
|
||||
"data": {
|
||||
"type": "Sensor Type",
|
||||
"type": "Sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"poll_interval": "Poll Interval (minutes)"
|
||||
"poll_interval": "Poll interval (minutes)"
|
||||
}
|
||||
},
|
||||
"options_switch": {
|
||||
"title": "Configure Switchable Output",
|
||||
"title": "Configure switchable output",
|
||||
"description": "{zone} options: state {state}",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
@@ -89,18 +89,18 @@
|
||||
}
|
||||
},
|
||||
"options_misc": {
|
||||
"title": "Configure Misc",
|
||||
"title": "Configure misc",
|
||||
"description": "Please select the desired behavior for your panel",
|
||||
"data": {
|
||||
"discovery": "Respond to discovery requests on your network",
|
||||
"blink": "Blink panel LED on when sending state change",
|
||||
"override_api_host": "Override default Home Assistant API host panel URL",
|
||||
"api_host": "Override API host URL"
|
||||
"override_api_host": "Override default Home Assistant API host URL",
|
||||
"api_host": "Custom API host URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"bad_host": "Invalid Override API host URL"
|
||||
"bad_host": "Invalid custom API host URL"
|
||||
},
|
||||
"abort": {
|
||||
"not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]"
|
||||
|
||||
@@ -123,6 +123,9 @@ DEVICE_TYPE_NUMBER_MAP: dict[DeviceType, tuple[NumberEntityDescription, ...]] =
|
||||
NUMBER_DESC[ThinQProperty.LIGHT_STATUS],
|
||||
NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],
|
||||
),
|
||||
DeviceType.VENTILATOR: (
|
||||
TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP],
|
||||
),
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"description": "Please enter a ThinQ [PAT(Personal Access Token)]({pat_url}) created with your LG ThinQ account.",
|
||||
"data": {
|
||||
"access_token": "Personal Access Token",
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:lightbulb"
|
||||
"default": "mdi:lightbulb",
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"off": "mdi:star-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user