forked from home-assistant/core
Compare commits
233 Commits
2025.4.0b5
...
get_config
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d082a87a4 | ||
|
|
c0e8f14745 | ||
|
|
0488012c77 | ||
|
|
f247183e11 | ||
|
|
c662b94d06 | ||
|
|
ee4bf165b5 | ||
|
|
92ac396d19 | ||
|
|
03366038ce | ||
|
|
0b91aa9202 | ||
|
|
ffc4fa1c2a | ||
|
|
15e03957a9 | ||
|
|
0be881bca6 | ||
|
|
e88b321741 | ||
|
|
0c4cb27fe9 | ||
|
|
1639163c2e | ||
|
|
f043404cd9 | ||
|
|
018651ff1d | ||
|
|
704d7a037c | ||
|
|
7336178e03 | ||
|
|
1c16fb8e42 | ||
|
|
3ab2cd3fb7 | ||
|
|
5057343b6a | ||
|
|
6c3e85fd5e | ||
|
|
f046456445 | ||
|
|
e81a08916a | ||
|
|
85d2e3d006 | ||
|
|
936b0b32ed | ||
|
|
0d511c697c | ||
|
|
5bfe034b4d | ||
|
|
cf786b3b04 | ||
|
|
0f9f090db2 | ||
|
|
302eea7418 | ||
|
|
b5e1f7e03e | ||
|
|
02397a8d2d | ||
|
|
ea9437eab2 | ||
|
|
aaea30bee0 | ||
|
|
9c869fa701 | ||
|
|
5106548f2c | ||
|
|
506d485c0d | ||
|
|
da190ec96f | ||
|
|
9567929484 | ||
|
|
dc16494332 | ||
|
|
933f422588 | ||
|
|
663d0691a7 | ||
|
|
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 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -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
|
||||
|
||||
@@ -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%]",
|
||||
@@ -16,8 +16,8 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "State"
|
||||
"state": "State",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon Monoxide",
|
||||
"n2": "Nitrogen Dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur Dioxide"
|
||||
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"p1": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"p2": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
|
||||
}
|
||||
},
|
||||
"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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
homeassistant/components/balay/__init__.py
Normal file
1
homeassistant/components/balay/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Balay virtual integration."""
|
||||
6
homeassistant/components/balay/manifest.json
Normal file
6
homeassistant/components/balay/manifest.json
Normal file
@@ -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."""
|
||||
|
||||
@@ -98,13 +98,13 @@
|
||||
"name": "Preset",
|
||||
"state": {
|
||||
"none": "None",
|
||||
"eco": "Eco",
|
||||
"away": "Away",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"activity": "Activity",
|
||||
"boost": "Boost",
|
||||
"comfort": "Comfort",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"sleep": "Sleep",
|
||||
"activity": "Activity"
|
||||
"eco": "Eco",
|
||||
"sleep": "Sleep"
|
||||
}
|
||||
},
|
||||
"preset_modes": {
|
||||
@@ -257,7 +257,7 @@
|
||||
"selector": {
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"off": "Off",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"auto": "Auto",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
1
homeassistant/components/constructa/__init__.py
Normal file
1
homeassistant/components/constructa/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Constructa virtual integration."""
|
||||
6
homeassistant/components/constructa/manifest.json
Normal file
6
homeassistant/components/constructa/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "constructa",
|
||||
"name": "Constructa",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -354,6 +354,35 @@ class ChatLog:
|
||||
if self.delta_listener:
|
||||
self.delta_listener(self, asdict(tool_result))
|
||||
|
||||
async def _async_expand_prompt_template(
|
||||
self,
|
||||
llm_context: llm.LLMContext,
|
||||
prompt: str,
|
||||
language: str,
|
||||
user_name: str | None = None,
|
||||
) -> str:
|
||||
try:
|
||||
return template.Template(prompt, self.hass).async_render(
|
||||
{
|
||||
"ha_name": self.hass.config.location_name,
|
||||
"user_name": user_name,
|
||||
"llm_context": llm_context,
|
||||
},
|
||||
parse_result=False,
|
||||
)
|
||||
except TemplateError as err:
|
||||
LOGGER.error("Error rendering prompt: %s", err)
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
"Sorry, I had a problem with my template",
|
||||
)
|
||||
raise ConverseError(
|
||||
"Error rendering prompt",
|
||||
conversation_id=self.conversation_id,
|
||||
response=intent_response,
|
||||
) from err
|
||||
|
||||
async def async_update_llm_data(
|
||||
self,
|
||||
conversing_domain: str,
|
||||
@@ -409,38 +438,28 @@ class ChatLog:
|
||||
):
|
||||
user_name = user.name
|
||||
|
||||
try:
|
||||
prompt_parts = [
|
||||
template.Template(
|
||||
llm.BASE_PROMPT
|
||||
+ (user_llm_prompt or llm.DEFAULT_INSTRUCTIONS_PROMPT),
|
||||
self.hass,
|
||||
).async_render(
|
||||
{
|
||||
"ha_name": self.hass.config.location_name,
|
||||
"user_name": user_name,
|
||||
"llm_context": llm_context,
|
||||
},
|
||||
parse_result=False,
|
||||
)
|
||||
]
|
||||
|
||||
except TemplateError as err:
|
||||
LOGGER.error("Error rendering prompt: %s", err)
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
"Sorry, I had a problem with my template",
|
||||
prompt_parts = []
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
(user_llm_prompt or llm.DEFAULT_INSTRUCTIONS_PROMPT),
|
||||
user_input.language,
|
||||
user_name,
|
||||
)
|
||||
raise ConverseError(
|
||||
"Error rendering prompt",
|
||||
conversation_id=self.conversation_id,
|
||||
response=intent_response,
|
||||
) from err
|
||||
)
|
||||
|
||||
if llm_api:
|
||||
prompt_parts.append(llm_api.api_prompt)
|
||||
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
llm.BASE_PROMPT,
|
||||
user_input.language,
|
||||
user_name,
|
||||
)
|
||||
)
|
||||
|
||||
if extra_system_prompt := (
|
||||
# Take new system prompt if one was given
|
||||
user_input.extra_system_prompt or self.extra_system_prompt
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
"protect_mode": {
|
||||
"name": "Protect mode",
|
||||
"state": {
|
||||
"away": "Away",
|
||||
"home": "Home",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
|
||||
def operation_list(self) -> list[str]:
|
||||
"""List of available operation modes."""
|
||||
econet_modes = self.water_heater.modes
|
||||
op_list = []
|
||||
operation_modes = set()
|
||||
for mode in econet_modes:
|
||||
if (
|
||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||
and mode is not WaterHeaterOperationMode.VACATION
|
||||
):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
op_list.append(ha_mode)
|
||||
return op_list
|
||||
operation_modes.add(ha_mode)
|
||||
return list(operation_modes)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
|
||||
@@ -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%]",
|
||||
|
||||
@@ -100,7 +100,11 @@ class ElkEntity(Entity):
|
||||
return {"index": self._element.index + 1}
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
pass
|
||||
"""Handle changes to the element.
|
||||
|
||||
This method is called when the element changes. It should be
|
||||
overridden by subclasses to handle the changes.
|
||||
"""
|
||||
|
||||
@callback
|
||||
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
@@ -111,7 +115,7 @@ class ElkEntity(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
self._element.add_callback(self._element_callback)
|
||||
self._element_callback(self._element, {})
|
||||
self._element_changed(self._element, {})
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -128,8 +128,23 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._password = ""
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
if error is None and entry_data.get(CONF_NOISE_PSK):
|
||||
return await self.async_step_reauth_encryption_removed_confirm()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_encryption_removed_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthorization flow when encryption was removed."""
|
||||
if user_input is not None:
|
||||
self._noise_psk = None
|
||||
return self._async_get_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_encryption_removed_confirm",
|
||||
description_placeholders={"name": self._name},
|
||||
)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@@ -570,6 +571,7 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.7.0",
|
||||
"aioesphomeapi==29.8.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
},
|
||||
"description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration."
|
||||
},
|
||||
"reauth_encryption_removed_confirm": {
|
||||
"description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to add the ESPHome node `{name}` to Home Assistant?",
|
||||
"title": "Discovered ESPHome node"
|
||||
|
||||
@@ -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==20250327.1"]
|
||||
"requirements": ["home-assistant-frontend==20250328.0"]
|
||||
}
|
||||
|
||||
1
homeassistant/components/gaggenau/__init__.py
Normal file
1
homeassistant/components/gaggenau/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Gaggenau virtual integration."""
|
||||
6
homeassistant/components/gaggenau/manifest.json
Normal file
6
homeassistant/components/gaggenau/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "gaggenau",
|
||||
"name": "Gaggenau",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -539,10 +539,14 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
assert self._cur_temp is not None and self._target_temp is not None
|
||||
too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance
|
||||
too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance
|
||||
|
||||
min_temp = self._target_temp - self._cold_tolerance
|
||||
max_temp = self._target_temp + self._hot_tolerance
|
||||
|
||||
if self._is_device_active:
|
||||
if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot):
|
||||
if (self.ac_mode and self._cur_temp <= min_temp) or (
|
||||
not self.ac_mode and self._cur_temp >= max_temp
|
||||
):
|
||||
_LOGGER.debug("Turning off heater %s", self.heater_entity_id)
|
||||
await self._async_heater_turn_off()
|
||||
elif time is not None:
|
||||
@@ -552,7 +556,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
self.heater_entity_id,
|
||||
)
|
||||
await self._async_heater_turn_on()
|
||||
elif (self.ac_mode and too_hot) or (not self.ac_mode and too_cold):
|
||||
elif (self.ac_mode and self._cur_temp > max_temp) or (
|
||||
not self.ac_mode and self._cur_temp < min_temp
|
||||
):
|
||||
_LOGGER.debug("Turning on heater %s", self.heater_entity_id)
|
||||
await self._async_heater_turn_on()
|
||||
elif time is not None:
|
||||
|
||||
@@ -21,17 +21,17 @@
|
||||
"heater": "Switch entity used to cool or heat depending on A/C mode.",
|
||||
"target_sensor": "Temperature sensor that reflects the current temperature.",
|
||||
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.",
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor goes below 24.5.",
|
||||
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5."
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Temperature presets",
|
||||
"data": {
|
||||
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"away_temp": "[%key:common::state::not_home%]",
|
||||
"comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
||||
"eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]",
|
||||
"activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]"
|
||||
}
|
||||
@@ -63,10 +63,10 @@
|
||||
"presets": {
|
||||
"title": "[%key:component::generic_thermostat::config::step::presets::title%]",
|
||||
"data": {
|
||||
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"away_temp": "[%key:common::state::not_home%]",
|
||||
"comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
||||
"eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]",
|
||||
"activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]"
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": [
|
||||
"google-cloud-texttospeech==2.17.2",
|
||||
"google-cloud-speech==2.27.0"
|
||||
"google-cloud-texttospeech==2.25.1",
|
||||
"google-cloud-speech==2.31.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -356,6 +356,15 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
messages.append(_convert_content(chat_content))
|
||||
|
||||
# The SDK requires the first message to be a user message
|
||||
# This is not the case if user used `start_conversation`
|
||||
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
|
||||
if messages and messages[0].role != "user":
|
||||
messages.insert(
|
||||
0,
|
||||
Content(role="user", parts=[Part.from_text(text=" ")]),
|
||||
)
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
generateContentConfig = GenerateContentConfig(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -511,7 +511,7 @@
|
||||
},
|
||||
"spin_speed": {
|
||||
"options": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
|
||||
@@ -521,7 +521,7 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "High"
|
||||
@@ -529,7 +529,7 @@
|
||||
},
|
||||
"vario_perfect": {
|
||||
"options": {
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "Off",
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "Eco perfect",
|
||||
"laundry_care_common_enum_type_vario_perfect_speed_perfect": "Speed perfect"
|
||||
}
|
||||
@@ -1494,7 +1494,7 @@
|
||||
"spin_speed": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_700%]",
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1200%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]"
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"vario_perfect": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_vario_perfect::name%]",
|
||||
"state": {
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_eco_perfect%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_speed_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_speed_perfect%]"
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"hold": "Hold",
|
||||
"away": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"none": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::none%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,14 @@
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"normal": "Normal",
|
||||
"eco": "Eco",
|
||||
"away": "Away",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"auto": "Auto",
|
||||
"baby": "Baby",
|
||||
"boost": "Boost",
|
||||
"comfort": "Comfort",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"sleep": "Sleep",
|
||||
"auto": "Auto",
|
||||
"baby": "Baby"
|
||||
"eco": "Eco",
|
||||
"sleep": "Sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -227,9 +227,9 @@ async def async_unload_entry(
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||
isy_data: IsyData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
isy: ISY = isy_data.root
|
||||
isy = isy_data.root
|
||||
|
||||
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
||||
isy.websocket.stop()
|
||||
|
||||
@@ -181,6 +181,7 @@ class ISYProgramEntity(ISYEntity):
|
||||
|
||||
_actions: Program
|
||||
_status: Program
|
||||
_node: Program
|
||||
|
||||
def __init__(self, name: str, status: Program, actions: Program = None) -> None:
|
||||
"""Initialize the ISY program-based entity."""
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.1.14"],
|
||||
"requirements": ["pyisy==3.4.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.helpers.service import entity_service_call
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .models import IsyData
|
||||
|
||||
# Common Services for All Platforms:
|
||||
SERVICE_SEND_PROGRAM_COMMAND = "send_program_command"
|
||||
@@ -149,7 +150,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
isy_name = service.data.get(CONF_ISY)
|
||||
|
||||
for config_entry_id in hass.data[DOMAIN]:
|
||||
isy_data = hass.data[DOMAIN][config_entry_id]
|
||||
isy_data: IsyData = hass.data[DOMAIN][config_entry_id]
|
||||
isy = isy_data.root
|
||||
if isy_name and isy_name != isy.conf["name"]:
|
||||
continue
|
||||
|
||||
@@ -157,7 +157,7 @@ class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity):
|
||||
device_info=device_info,
|
||||
)
|
||||
self._attr_name = description.name # Override super
|
||||
self._change_handler: EventListener = None
|
||||
self._change_handler: EventListener | None = None
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user