Compare commits

..

77 Commits

Author SHA1 Message Date
Franck Nijhof
7766649304 Bump version to 2025.4.0b9 2025-03-29 17:50:46 +00:00
Simone Chemelli
07e9020dfa Fix immediate state update for Comelit (#141735) 2025-03-29 17:50:36 +00:00
J. Diego Rodríguez Royo
f504a759e0 Set Home Connect program action field as not required (#141729)
* Set Home Connect program action field as not required

* Remove required field

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-29 17:50:32 +00:00
Joost Lekkerkerker
9927de4801 Only trigger events on button updates in SmartThings (#141720)
Only trigger events on button updates
2025-03-29 17:50:29 +00:00
Joost Lekkerkerker
1244fc4682 Only link the parent device if known in SmartThings (#141719)
Only link the parent device if we know the parent device
2025-03-29 17:50:26 +00:00
Norbert Rittel
e77a1b12f7 Sentence-case "Medium type" in mopeka (#141718) 2025-03-29 17:50:22 +00:00
J. Nick Koston
5459daaa10 Fix ESPHome entities not being removed when the ESPHome config removes an entire platform (#141708)
* Fix old ESPHome entities not being removed when configuration changes

fixes #140756

* make sure all callbacks fire

* make sure all callbacks fire

* make sure all callbacks fire

* make sure all callbacks fire

* revert

* cover
2025-03-29 17:50:18 +00:00
J. Nick Koston
400131df78 Fix ESPHome update entities being loaded before device_info is available (#141704)
* Fix ESPHome update entities being loaded before device_info is available

Since we load platforms when restoring config, the update
platform could be loaded before the connection to the
device was finished which meant device_info could still
be empty. Wait until device_info is available to
load the update platform.

fixes #135906

* Apply suggestions from code review

* move comment

* Update entry_data.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-03-29 17:50:15 +00:00
Franck Nijhof
28e1843ff9 Fix Tuya tdq category to pick up temp & humid (#141698) 2025-03-29 17:50:12 +00:00
Franck Nijhof
df777318d1 Handle invalid JSON errors in AirNow (#141695) 2025-03-29 17:50:08 +00:00
Jan Bouwhuis
6ad5e9e89c Improve MQTT translation strings (#141691)
* Improve MQTT options translation string

* more improvements
2025-03-29 17:50:05 +00:00
Norbert Rittel
a0bd8deee9 Replace "country" with common string in holiday (#141687) 2025-03-29 17:50:01 +00:00
Marcel van der Veldt
405cbd6a00 Always set pause feature on Music Assistant mediaplayers (#141686) 2025-03-29 17:49:58 +00:00
Marcel van der Veldt
3e0eb5ab2c Bump music assistant client to 1.2.0 (#141668)
* Bump music assistant client to 1.2.0

* Update test fixtures
2025-03-29 17:49:55 +00:00
Norbert Rittel
fad75a70b6 Add a common string for "country" (#141653) 2025-03-29 17:49:52 +00:00
Josef Zweck
d9720283df Add unkown to uncalibrated state for tedee (#141262) 2025-03-29 17:49:46 +00:00
Franck Nijhof
14eed1778b Bump version to 2025.4.0b8 2025-03-28 20:46:26 +00:00
Norbert Rittel
049aaa7e8b Fix grammar / sentence-casing in workday (#141682)
* Fix grammar / sentence-casing in `workday`

Also replace "country" with common string.

* Add two more references

* Fix second data description reference

* Add "given" to action description for better translations
2025-03-28 20:46:17 +00:00
J. Nick Koston
35717e8216 Increase websocket_api allowed peak time to 10s (#141680)
* Increase websocket_api allowed peak time to 10s

fixes #141624

During integration reload or startup, we can end up sending a message for
each entity being created for integrations that create them from an external
source (ie MQTT) because the messages come in one at a time. This can overload
the loop and/or client for more than 5s. While we have done significant work
to optimize for this path, we are at the limit at what we can expect clients
to be able to process in the time window, so increase the time window.

* adjust test
2025-03-28 20:46:13 +00:00
Franck Nijhof
2a081abc18 Fix camera proxy with sole image quality settings (#141676) 2025-03-28 20:46:10 +00:00
puddly
b7f29c7358 Handle all firmware types for ZBT-1 and Yellow update entities (#141674)
Handle other firmware types
2025-03-28 20:46:06 +00:00
Jason Hunter
3bb6373df5 Update Duke Energy package to fix integration (#141669)
* Update Duke Energy package to fix integration

* fix tests
2025-03-28 20:46:03 +00:00
Michael Hansen
e1b4edec50 Bump intents and always prefer more literal text (#141663) 2025-03-28 20:46:00 +00:00
puddly
147bee57e1 Include ZBT-1 and Yellow in device registry (#141623)
* Add the Yellow and ZBT-1 to the device registry

* Unload platforms

* Fix unit tests

* Rename the Yellow update entity to `Radio firmware`

* Rename `EmberZNet` to `EmberZNet Zigbee`

* Prefix the `sw_version` with the firmware type and clean up

* Fix unit tests

* Remove unnecessary `always_update=False` from data update coordinator
2025-03-28 20:45:56 +00:00
Erwin Douna
fcdaea64da Tado add proper off state (#135480)
* Add proper off state

* Remove current temp

* Add default frost temp
2025-03-28 20:45:53 +00:00
Franck Nijhof
d1512d46be Bump version to 2025.4.0b7 2025-03-28 16:00:45 +00:00
Bram Kragten
0be7db6270 Update frontend to 20250328.0 (#141659) 2025-03-28 15:09:56 +00:00
Paulus Schoutsen
2af0282725 Enable the message box on default for satelitte announcement actions (#141654) 2025-03-28 15:09:51 +00:00
Franck Nijhof
ff458c8417 Bump version to 2025.4.0b6 2025-03-28 15:04:34 +00:00
Franck Nijhof
cc93152ff0 Fix ESPHome event entity staying unavailable (#141650) 2025-03-28 14:05:40 +00:00
Paulus Schoutsen
9965f01609 Ensure connection test sound has no preannouncement (#141647) 2025-03-28 14:05:37 +00:00
Jan Bouwhuis
e9c76ce694 Fix duplicate 'device' term in MQTT translation strings (#141646)
* Fix duplicate 'device' from MQTT translation strings

* Update homeassistant/components/mqtt/strings.json
2025-03-28 14:05:34 +00:00
Norbert Rittel
58ab7d350d Fix sentence-casing in airvisual user strings (#141632) 2025-03-28 14:05:30 +00:00
Nick Pesce
e4d6e20ebd Use correct default value for multi press buttons in the Matter integration (#141630)
* Respect the min 2 constraint for the switch MultiPressMax attribute

* Update test_event.py

* Update generic_switch_multi.json

* Fix issue and update tests
2025-03-28 14:05:27 +00:00
Tsvi Mostovicz
45e273897a Jewish calendar match omer service variables requirement to documentation (#141620)
The documentation and the omer schema require a Nusach to be specified, but the YAML misses that requirement
2025-03-28 14:05:23 +00:00
Jan Bouwhuis
d9ec7142d7 Fix volatile_organic_compounds_parts translation string to be referenced for MQTT subentries device class selector (#141618)
* Fix ` volatile_organic_compounds_parts` translation string to be referenced for MQTT subentries device class selector

* Fix tests
2025-03-28 14:05:20 +00:00
Petro31
e162499267 Fix an issue with the switch preview in beta (#141617)
Fix an issue with the switch preview
2025-03-28 14:05:16 +00:00
Jan-Philipp Benecke
67f21429e3 Bump aiowebdav2 to 0.4.4 (#141615) 2025-03-28 14:05:12 +00:00
J. Nick Koston
a0563f06c9 Fix zeroconf logging level not being respected (#141601)
Removes an old logging workaround that is no longer needed

fixes #141558
2025-03-28 14:05:05 +00:00
Luke Lashley
e7c4fdc8bb Bump Python-Snoo to 0.6.5 (#141599)
* Bump Python-Snoo to 0.6.5

* add to event_types
2025-03-28 14:05:00 +00:00
Norbert Rittel
c490e350bc Make names of switch entities in gree consistent with docs (#141580) 2025-03-28 14:04:56 +00:00
Robert Resch
e11409ef99 Reverts #141363 "Deprecate SmartThings machine state sensors" (#141573)
Reverts #141363
2025-03-28 14:04:52 +00:00
Joost Lekkerkerker
5c8e415a76 Add default string and icon for light effect off (#141567) 2025-03-28 14:04:49 +00:00
alorente
e795fb9497 Fix missing response for queued mode scripts (#141460) 2025-03-28 14:04:45 +00:00
Norbert Rittel
d0afabb85c Fix misleading friendly names of pvoutput sensors (#141312)
* Fix misleading friendly names of `pvoutput` sensors

* Update test_sensor.py

* Update test_sensor.py - prettier
2025-03-28 14:04:41 +00:00
Franck Nijhof
4f3e8e9b94 Bump version to 2025.4.0b5 2025-03-27 20:03:14 +00:00
Paul Bottein
46c1cbbc9c Update frontend to 20250327.1 (#141596) 2025-03-27 20:03:01 +00:00
Simon Lamon
8d9a4ea278 Fix typing error in NMBS (#141589)
Fix typing error
2025-03-27 20:02:58 +00:00
Jan-Philipp Benecke
22c83e2393 Bump aiowebdav2 to 0.4.3 (#141586) 2025-03-27 20:02:55 +00:00
Joost Lekkerkerker
c83a75f6f9 Add brand for Bosch (#141561) 2025-03-27 20:02:51 +00:00
Franck Nijhof
841c727112 Bump version to 2025.4.0b4 2025-03-27 16:59:36 +00:00
Bram Kragten
d8c9655bfd Update frontend to 20250327.0 (#141585) 2025-03-27 16:59:29 +00:00
Erik Montnemery
942ed89cc4 Revert "Promote after dependencies in bootstrap" (#141584)
Revert "Promote after dependencies in bootstrap (#140352)"

This reverts commit 3766040960.
2025-03-27 16:59:25 +00:00
Franck Nijhof
a1fe6b9cf3 Bump version to 2025.4.0b3 2025-03-27 15:38:31 +00:00
Luke Lashley
2567181cc2 Better handle Roborock discovery (#141575) 2025-03-27 15:38:24 +00:00
Joost Lekkerkerker
028e4f6029 Also migrate completion time entities in SmartThings (#141572) 2025-03-27 15:38:21 +00:00
Martin Hjelmare
b82e1a9bef Handle cloud subscription expired for backup upload (#141564)
Handle cloud backup subscription expired for upload
2025-03-27 15:38:18 +00:00
Joost Lekkerkerker
438f226c31 Add icons to hue effects (#141559) 2025-03-27 15:38:15 +00:00
Erwin Douna
2f139e3cb1 Tado fix HomeKit flow (#141525)
* Initial commit

* Fix

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-03-27 15:38:07 +00:00
Franck Nijhof
5d75e96fbf Bump version to 2025.4.0b2 2025-03-27 10:19:35 +00:00
Norbert Rittel
dcf2ec5c37 Fix sentence-casing in konnected strings, replace "override" with "custom" (#141553)
Fix sentence-casing in `konnected`strings, replace "Override" with "Custom"

Make string consistent with HA standards.

As "Override" can be misunderstood as the verb, replace it with "Custom".
2025-03-27 10:19:22 +00:00
Simon Lamon
2431e1ba98 Bump linkplay to v0.2.2 (#141542)
Bump linkplay
2025-03-27 10:19:18 +00:00
Thomas55555
4ead108c15 Handle webcal prefix in remote calendar (#141541)
Handel webcal prefix in remote calendar
2025-03-27 10:19:14 +00:00
Michael Hansen
ec8363fa49 Add default preannounce sound to Assist satellites (#141522)
* Add default preannounce sound

* Allow None to disable sound

* Register static path instead of HTTP view

* Fix path

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-27 10:19:09 +00:00
J. Diego Rodríguez Royo
e7ff0a3f8b Improve some Home Connect deprecations (#141508) 2025-03-27 10:19:06 +00:00
Ivan Lopez Hernandez
f4c0eb4189 Initialize google.genai.Client in the executor (#141432)
* Intialize the client on an executor thread

* Fix MyPy error

* MyPy error

* Exception error

* Fix ruff

* Update __init__.py

---------

Co-authored-by: tronikos <tronikos@users.noreply.github.com>
2025-03-27 10:19:02 +00:00
Manu
b1ee5a76e1 Support for upcoming pyLoad-ng release in pyLoad integration (#141297)
Fix extra key `proxy` in pyLoad
2025-03-27 10:18:58 +00:00
Norbert Rittel
6b9e8c301b Fix wrong friendly name for storage_power in solaredge (#141269)
* Fix wrong friendly name for `storage_power` in `solaredge`

"Stored power" is a contradiction in itself.
You can only store energy.

* Two additional spelling fixes

* Sentence-case "site"
2025-03-27 10:18:53 +00:00
Franck Nijhof
89c3266c7e Bump version to 2025.4.0b1 2025-03-26 23:21:26 +00:00
Jan Bouwhuis
cff0a632e8 Fix QoS schema issue in MQTT subentries (#141531) 2025-03-26 23:21:17 +00:00
Jan Bouwhuis
e04d8557ae Fix MQTT options flow QoS selector can not serialize (#141528) 2025-03-26 23:21:14 +00:00
Thomas55555
ca6286f241 Fix work area sensor for Husqvarna Automower (#141527)
* Fix work area sensor for Husqvarna Automower

* simplify
2025-03-26 23:21:10 +00:00
Robert Resch
35bcc9d5af Show box for Smartthings rise number entity (#141526) 2025-03-26 23:21:07 +00:00
Joost Lekkerkerker
25b45ce867 Sort SmartThings devices to be created by parent device id (#141515) 2025-03-26 23:21:03 +00:00
Robert Resch
d568209bd5 Bump deebot-client to 12.4.0 (#141501) 2025-03-26 23:21:00 +00:00
Simone Chemelli
8a43e8af9e Fix refresh state for Comelit alarm (#141370) 2025-03-26 23:20:56 +00:00
Franck Nijhof
785e5b2c16 Bump version to 2025.4.0b0 2025-03-26 17:41:03 +00:00
507 changed files with 7740 additions and 16190 deletions

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 12
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.5"
HA_SHORT_VERSION: "2025.4"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.6.0
uses: actions/dependency-review-action@v4.5.0
with:
license-check: false # We use our own license audit checks

View File

@@ -364,7 +364,6 @@ homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.number.*
homeassistant.components.nut.*
homeassistant.components.ohme.*
homeassistant.components.onboarding.*
homeassistant.components.oncue.*
homeassistant.components.onedrive.*

2
CODEOWNERS generated
View File

@@ -1480,6 +1480,8 @@ 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

15
Dockerfile generated
View File

@@ -14,8 +14,21 @@ ARG QEMU_CPU
# Home Assistant S6-Overlay
COPY rootfs /
# Needs to be redefined inside the FROM statement to be set for RUN commands
ARG BUILD_ARCH
# Get go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:1.9.9 /usr/local/bin/go2rtc /bin/go2rtc
RUN \
case "${BUILD_ARCH}" in \
"aarch64") go2rtc_suffix='arm64' ;; \
"armhf") go2rtc_suffix='armv6' ;; \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.6.10

View File

@@ -1,5 +0,0 @@
{
"domain": "eve",
"name": "Eve",
"iot_standards": ["matter"]
}

View File

@@ -68,8 +68,8 @@
"led_bar_mode": {
"name": "LED bar mode",
"state": {
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "Off",
"co2": "Carbon dioxide",
"pm": "Particulate matter"
}
},
@@ -143,8 +143,8 @@
"led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": {
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
}
},

View File

@@ -16,8 +16,8 @@
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City",
"state": "State",
"country": "[%key:common::config_flow::data::country%]"
"country": "Country",
"state": "State"
}
},
"reauth_confirm": {
@@ -56,12 +56,12 @@
"sensor": {
"pollutant_label": {
"state": {
"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%]"
"co": "Carbon monoxide",
"n2": "Nitrogen dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur dioxide"
}
},
"pollutant_level": {

View File

@@ -32,8 +32,8 @@
"air_quality": {
"name": "Air Quality mode",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"off": "Off",
"on": "On",
"auto": "Auto"
}
},

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"],
"quality_scale": "legacy",
"requirements": ["boto3==1.37.1"]
"requirements": ["boto3==1.34.131"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"iot_class": "local_polling",
"requirements": ["pydroid-ipcam==3.0.0"]
"requirements": ["pydroid-ipcam==2.0.0"]
}

View File

@@ -43,7 +43,6 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
config_entry: ApSystemsConfigEntry
device_version: str
battery_system: bool
def __init__(
self,
@@ -69,7 +68,6 @@ 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:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==2.5.0"]
"requirements": ["apsystems-ez1==2.4.0"]
}

View File

@@ -36,8 +36,6 @@ 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."""

View File

@@ -60,8 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("message"): str,
vol.Optional("media_id"): str,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
}
),
cv.has_at_least_one_key("message", "media_id"),
@@ -76,8 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str,
vol.Optional("preannounce"): bool,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
vol.Optional("extra_system_prompt"): str,
}
),

View File

@@ -180,8 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
self,
message: str | None = None,
media_id: str | None = None,
preannounce: bool = True,
preannounce_media_id: str = PREANNOUNCE_URL,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Play and show an announcement on the satellite.
@@ -191,8 +190,8 @@ class AssistSatelliteEntity(entity.Entity):
If media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text.
If preannounce is True, a sound is played before the announcement.
If preannounce_media_id is provided, it overrides the default sound.
If preannounce_media_id is None, no sound is played.
Calls async_announce with message and media id.
"""
@@ -202,9 +201,7 @@ class AssistSatelliteEntity(entity.Entity):
message = ""
announcement = await self._resolve_announcement_media_id(
message,
media_id,
preannounce_media_id=preannounce_media_id if preannounce else None,
message, media_id, preannounce_media_id
)
if self._is_announcing:
@@ -232,8 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
start_message: str | None = None,
start_media_id: str | None = None,
extra_system_prompt: str | None = None,
preannounce: bool = True,
preannounce_media_id: str = PREANNOUNCE_URL,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Start a conversation from the satellite.
@@ -243,8 +239,8 @@ class AssistSatelliteEntity(entity.Entity):
If start_media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text.
If preannounce is True, a sound is played before the start message or media.
If preannounce_media_id is provided, it overrides the default sound.
If preannounce_media_id is provided, it is played before the announcement.
If preannounce_media_id is None, no sound is played.
Calls async_start_conversation.
"""
@@ -261,9 +257,7 @@ class AssistSatelliteEntity(entity.Entity):
start_message = ""
announcement = await self._resolve_announcement_media_id(
start_message,
start_media_id,
preannounce_media_id=preannounce_media_id if preannounce else None,
start_message, start_media_id, preannounce_media_id
)
if self._is_announcing:

View File

@@ -15,11 +15,6 @@ announce:
required: false
selector:
text:
preannounce:
required: false
default: true
selector:
boolean:
preannounce_media_id:
required: false
selector:
@@ -45,11 +40,6 @@ start_conversation:
required: false
selector:
text:
preannounce:
required: false
default: true
selector:
boolean:
preannounce_media_id:
required: false
selector:

View File

@@ -24,13 +24,9 @@
"name": "Media ID",
"description": "The media ID to announce instead of using text-to-speech."
},
"preannounce": {
"name": "Preannounce",
"description": "Play a sound before the announcement."
},
"preannounce_media_id": {
"name": "Preannounce media ID",
"description": "Custom media ID to play before the announcement."
"name": "Preannounce Media ID",
"description": "The media ID to play before the announcement."
}
}
},
@@ -50,13 +46,9 @@
"name": "Extra system prompt",
"description": "Provide background information to the AI about the request."
},
"preannounce": {
"name": "Preannounce",
"description": "Play a sound before the start message or media."
},
"preannounce_media_id": {
"name": "Preannounce media ID",
"description": "Custom media ID to play before the start message or media."
"name": "Preannounce Media ID",
"description": "The media ID to play before the start message or media."
}
}
}

View File

@@ -199,7 +199,7 @@ async def websocket_test_connection(
hass.async_create_background_task(
satellite.async_internal_announce(
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
preannounce=False,
preannounce_media_id=None,
),
f"assist_satellite_connection_test_{msg['entity_id']}",
)

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["aiobotocore", "botocore"],
"quality_scale": "legacy",
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
}

View File

@@ -175,8 +175,7 @@ class AzureStorageBackupAgent(BackupAgent):
"""Find a blob by backup id."""
async for blob in self._client.list_blobs(include="metadata"):
if (
blob.metadata is not None
and backup_id == blob.metadata.get("backup_id", "")
backup_id == blob.metadata.get("backup_id", "")
and blob.metadata.get("metadata_version") == METADATA_VERSION
):
return blob

View File

@@ -26,9 +26,9 @@
"entity": {
"sensor": {
"backup_manager_state": {
"name": "Backup Manager state",
"name": "Backup Manager State",
"state": {
"idle": "[%key:common::state::idle%]",
"idle": "Idle",
"create_backup": "Creating a backup",
"receive_backup": "Receiving a backup",
"restore_backup": "Restoring a backup"

View File

@@ -1 +0,0 @@
"""Balay virtual integration."""

View File

@@ -1,6 +0,0 @@
{
"domain": "balay",
"name": "Balay",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@@ -132,7 +132,7 @@
"name": "Charging",
"state": {
"off": "Not charging",
"on": "[%key:common::state::charging%]"
"on": "Charging"
}
},
"carbon_monoxide": {

View File

@@ -37,7 +37,7 @@
"vehicle_status": {
"name": "Vehicle status",
"state": {
"standby": "[%key:common::state::standby%]",
"standby": "Standby",
"vehicle_detected": "Detected",
"ready": "Ready",
"no_power": "No power",

View File

@@ -501,16 +501,18 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
return
# presets and inputs might have the same name; presets have priority
url: str | None = None
for input_ in self._inputs:
if input_.text == source:
await self._player.play_url(input_.url)
return
url = input_.url
for preset in self._presets:
if preset.name == source:
await self._player.load_preset(preset.id)
return
url = preset.url
raise ServiceValidationError(f"Source {source} not found")
if url is None:
raise ServiceValidationError(f"Source {source} not found")
await self._player.play_url(url)
async def async_clear_playlist(self) -> None:
"""Clear players playlist."""

View File

@@ -19,7 +19,7 @@
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.5",
"bluetooth-data-tools==1.26.5",
"bluetooth-data-tools==1.26.1",
"dbus-fast==2.43.0",
"habluetooth==3.37.0"
]

View File

@@ -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": "[%key:common::state::standby%]",
"standby": "Standby",
"ventilation": "Ventilation"
}
},

View File

@@ -142,12 +142,6 @@ 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
@@ -175,11 +169,6 @@ 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."""

View File

@@ -2,10 +2,17 @@
from __future__ import annotations
from contextlib import suppress
import logging
from typing import TYPE_CHECKING, Literal, cast
from turbojpeg import TurboJPEG
with suppress(Exception):
# TurboJPEG imports numpy which may or may not work so
# we have to guard the import here. We still want
# to import it at top level so it gets loaded
# in the import executor and not in the event loop.
from turbojpeg import TurboJPEG
if TYPE_CHECKING:
from . import Image

View File

@@ -98,13 +98,13 @@
"name": "Preset",
"state": {
"none": "None",
"home": "[%key:common::state::home%]",
"away": "[%key:common::state::not_home%]",
"activity": "Activity",
"eco": "Eco",
"away": "Away",
"boost": "Boost",
"comfort": "Comfort",
"eco": "Eco",
"sleep": "Sleep"
"home": "[%key:common::state::home%]",
"sleep": "Sleep",
"activity": "Activity"
}
},
"preset_modes": {
@@ -257,7 +257,7 @@
"selector": {
"hvac_mode": {
"options": {
"off": "[%key:common::state::off%]",
"off": "Off",
"auto": "Auto",
"cool": "Cool",
"dry": "Dry",

View File

@@ -127,11 +127,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
flow_id=flow_id, user_input=tokens
)
# It's a background task because it should be cancelled on shutdown and there's nothing else
# we can do in such case. There's also no need to wait for this during setup.
self.hass.async_create_background_task(
await_tokens(), name="Awaiting OAuth tokens"
)
self.hass.async_create_task(await_tokens())
return authorize_url

View File

@@ -9,6 +9,7 @@ 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
@@ -117,6 +118,8 @@ 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:

View File

@@ -42,9 +42,9 @@
"sensor": {
"zone_status": {
"state": {
"open": "[%key:common::state::open%]",
"alarm": "Alarm",
"armed": "Armed",
"open": "Open",
"excluded": "Excluded",
"faulty": "Faulty",
"inhibited": "Inhibited",

View File

@@ -1 +0,0 @@
"""Constructa virtual integration."""

View File

@@ -1,6 +0,0 @@
{
"domain": "constructa",
"name": "Constructa",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@@ -354,35 +354,6 @@ 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,
@@ -438,28 +409,38 @@ class ChatLog:
):
user_name = user.name
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,
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",
)
)
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

View File

@@ -6,7 +6,7 @@
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"country": "[%key:common::config_flow::data::country%]"
"country": "Country"
},
"data_description": {
"email": "Email used to access your {cookidoo} account.",

View File

@@ -38,10 +38,10 @@
"name": "[%key:component::cover::title%]",
"state": {
"open": "[%key:common::state::open%]",
"opening": "[%key:common::state::opening%]",
"opening": "Opening",
"closed": "[%key:common::state::closed%]",
"closing": "[%key:common::state::closing%]",
"stopped": "[%key:common::state::stopped%]"
"closing": "Closing",
"stopped": "Stopped"
},
"state_attributes": {
"current_position": {

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "To be able to use this integration, you have to enable the following option in Deluge settings: Daemon > Allow remote controls",
"description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",

View File

@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"],
"requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -7,7 +7,7 @@
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.44.0"],
"requirements": ["async-upnp-client==0.43.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

View File

@@ -38,8 +38,8 @@
"protect_mode": {
"name": "Protect mode",
"state": {
"away": "[%key:common::state::not_home%]",
"home": "[%key:common::state::home%]",
"away": "Away",
"home": "Home",
"schedule": "Schedule"
}
}

View File

@@ -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
operation_modes = set()
op_list = []
for mode in econet_modes:
if (
mode is not WaterHeaterOperationMode.UNKNOWN
and mode is not WaterHeaterOperationMode.VACATION
):
ha_mode = ECONET_STATE_TO_HA[mode]
operation_modes.add(ha_mode)
return list(operation_modes)
op_list.append(ha_mode)
return op_list
@property
def supported_features(self) -> WaterHeaterEntityFeature:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
}

View File

@@ -14,7 +14,7 @@
"step": {
"auth": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"country": "Country",
"override_rest_url": "REST URL",
"override_mqtt_url": "MQTT URL",
"password": "[%key:common::config_flow::data::password%]",

View File

@@ -100,11 +100,7 @@ class ElkEntity(Entity):
return {"index": self._element.index + 1}
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
"""Handle changes to the element.
This method is called when the element changes. It should be
overridden by subclasses to handle the changes.
"""
pass
@callback
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
@@ -115,7 +111,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_changed(self._element, {})
self._element_callback(self._element, {})
@property
def device_info(self) -> DeviceInfo:

View File

@@ -46,8 +46,6 @@ 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."""
@@ -67,21 +65,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up an emulated roku server from a config entry."""
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)
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)
server = EmulatedRoku(
hass,
entry.entry_id,
name,
host_ip,
listen_port,
@@ -89,12 +88,14 @@ async def async_setup_entry(
advertise_port,
upnp_bind_multicast,
)
entry.runtime_data = server
hass.data[DOMAIN][name] = server
return await server.setup()
async def async_unload_entry(
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await entry.runtime_data.unload()
name = entry.data[CONF_NAME]
server = hass.data[DOMAIN].pop(name)
return await server.unload()

View File

@@ -5,13 +5,7 @@ import logging
from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
CALLBACK_TYPE,
CoreState,
Event,
EventOrigin,
HomeAssistant,
)
from homeassistant.core import CoreState, EventOrigin
LOGGER = logging.getLogger(__package__)
@@ -33,18 +27,16 @@ class EmulatedRoku:
def __init__(
self,
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:
hass,
name,
host_ip,
listen_port,
advertise_ip,
advertise_port,
upnp_bind_multicast,
):
"""Initialize the properties."""
self.hass = hass
self.entry_id = entry_id
self.roku_usn = name
self.host_ip = host_ip
@@ -55,21 +47,21 @@ class EmulatedRoku:
self.bind_multicast = upnp_bind_multicast
self._api_server: EmulatedRokuServer | None = None
self._api_server = None
self._unsub_start_listener: CALLBACK_TYPE | None = None
self._unsub_stop_listener: CALLBACK_TYPE | None = None
self._unsub_start_listener = None
self._unsub_stop_listener = None
async def setup(self) -> bool:
async def setup(self):
"""Start the emulated_roku server."""
class EventCommandHandler(EmulatedRokuCommandHandler):
"""emulated_roku command handler to turn commands into events."""
def __init__(self, hass: HomeAssistant) -> None:
def __init__(self, hass):
self.hass = hass
def on_keydown(self, roku_usn: str, key: str) -> None:
def on_keydown(self, roku_usn, key):
"""Handle keydown event."""
self.hass.bus.async_fire(
EVENT_ROKU_COMMAND,
@@ -81,7 +73,7 @@ class EmulatedRoku:
EventOrigin.local,
)
def on_keyup(self, roku_usn: str, key: str) -> None:
def on_keyup(self, roku_usn, key):
"""Handle keyup event."""
self.hass.bus.async_fire(
EVENT_ROKU_COMMAND,
@@ -93,7 +85,7 @@ class EmulatedRoku:
EventOrigin.local,
)
def on_keypress(self, roku_usn: str, key: str) -> None:
def on_keypress(self, roku_usn, key):
"""Handle keypress event."""
self.hass.bus.async_fire(
EVENT_ROKU_COMMAND,
@@ -105,7 +97,7 @@ class EmulatedRoku:
EventOrigin.local,
)
def launch(self, roku_usn: str, app_id: str) -> None:
def launch(self, roku_usn, app_id):
"""Handle launch event."""
self.hass.bus.async_fire(
EVENT_ROKU_COMMAND,
@@ -137,19 +129,17 @@ class EmulatedRoku:
bind_multicast=self.bind_multicast,
)
async def emulated_roku_stop(event: Event | None) -> None:
async def emulated_roku_stop(event):
"""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: Event | None) -> None:
async def emulated_roku_start(event):
"""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(
@@ -175,7 +165,7 @@ class EmulatedRoku:
return True
async def unload(self) -> bool:
async def unload(self):
"""Unload the emulated_roku server."""
LOGGER.debug("Unloading emulated_roku %s", self.roku_usn)
@@ -187,7 +177,6 @@ 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

View File

@@ -25,7 +25,6 @@ from homeassistant.core import (
split_entity_id,
valid_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
@@ -123,10 +122,6 @@ SOURCE_ADAPTERS: Final = (
)
class EntityNotFoundError(HomeAssistantError):
"""When a referenced entity was not found."""
class SensorManager:
"""Class to handle creation/removal of sensor data."""
@@ -316,25 +311,43 @@ class EnergyCostSensor(SensorEntity):
except ValueError:
return
try:
energy_price, energy_price_unit = self._get_energy_price(
valid_units, default_price_unit
# Determine energy price
if self._config["entity_energy_price"] is not None:
energy_price_state = self.hass.states.get(
self._config["entity_energy_price"]
)
except EntityNotFoundError:
return
except ValueError:
energy_price = None
if energy_price_state is None:
return
try:
energy_price = float(energy_price_state.state)
except ValueError:
if self._last_energy_sensor_state is None:
# Initialize as it's the first time all required entities except
# price are in place. This means that the cost will update the first
# time the energy is updated after the price entity is in place.
self._reset(energy_state)
return
energy_price_unit: str | None = energy_price_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT, ""
).partition("/")[2]
# For backwards compatibility we don't validate the unit of the price
# If it is not valid, we assume it's our default price unit.
if energy_price_unit not in valid_units:
energy_price_unit = default_price_unit
else:
energy_price = cast(float, self._config["number_energy_price"])
energy_price_unit = default_price_unit
if self._last_energy_sensor_state is None:
# Initialize as it's the first time all required entities are in place or
# only the price is missing. In the later case, cost will update the first
# time the energy is updated after the price entity is in place.
# Initialize as it's the first time all required entities are in place.
self._reset(energy_state)
return
if energy_price is None:
return
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if energy_unit is None or energy_unit not in valid_units:
@@ -370,9 +383,20 @@ class EnergyCostSensor(SensorEntity):
old_energy_value = float(self._last_energy_sensor_state.state)
cur_value = cast(float, self._attr_native_value)
converted_energy_price = self._convert_energy_price(
energy_price, energy_price_unit, energy_unit
)
if energy_price_unit is None:
converted_energy_price = energy_price
else:
converter: Callable[[float, str, str], float]
if energy_unit in VALID_ENERGY_UNITS:
converter = unit_conversion.EnergyConverter.convert
else:
converter = unit_conversion.VolumeConverter.convert
converted_energy_price = converter(
energy_price,
energy_unit,
energy_price_unit,
)
self._attr_native_value = (
cur_value + (energy - old_energy_value) * converted_energy_price
@@ -380,52 +404,6 @@ class EnergyCostSensor(SensorEntity):
self._last_energy_sensor_state = energy_state
def _get_energy_price(
self, valid_units: set[str], default_unit: str | None
) -> tuple[float, str | None]:
"""Get the energy price.
Raises:
EntityNotFoundError: When the energy price entity is not found.
ValueError: When the entity state is not a valid float.
"""
if self._config["entity_energy_price"] is None:
return cast(float, self._config["number_energy_price"]), default_unit
energy_price_state = self.hass.states.get(self._config["entity_energy_price"])
if energy_price_state is None:
raise EntityNotFoundError
energy_price = float(energy_price_state.state)
energy_price_unit: str | None = energy_price_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT, ""
).partition("/")[2]
# For backwards compatibility we don't validate the unit of the price
# If it is not valid, we assume it's our default price unit.
if energy_price_unit not in valid_units:
energy_price_unit = default_unit
return energy_price, energy_price_unit
def _convert_energy_price(
self, energy_price: float, energy_price_unit: str | None, energy_unit: str
) -> float:
"""Convert the energy price to the correct unit."""
if energy_price_unit is None:
return energy_price
converter: Callable[[float, str, str], float]
if energy_unit in VALID_ENERGY_UNITS:
converter = unit_conversion.EnergyConverter.convert
else:
converter = unit_conversion.VolumeConverter.convert
return converter(energy_price, energy_unit, energy_price_unit)
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
energy_state = self.hass.states.get(self._config[self._adapter.stat_energy_key])

View File

@@ -3,8 +3,8 @@
"step": {
"user": {
"data": {
"language": "[%key:common::config_flow::data::language%]",
"country": "[%key:common::config_flow::data::country%]"
"language": "Language",
"country": "Country"
}
}
},

View File

@@ -310,13 +310,12 @@ class EsphomeAssistSatellite(
self.entry_data.api_version
)
)
if feature_flags & VoiceAssistantFeature.SPEAKER and (
stream := tts.async_get_stream(self.hass, tts_output["token"])
):
if feature_flags & VoiceAssistantFeature.SPEAKER:
media_id = tts_output["media_id"]
self._tts_streaming_task = (
self.config_entry.async_create_background_task(
self.hass,
self._stream_tts_audio(stream),
self._stream_tts_audio(media_id),
"esphome_voice_assistant_tts",
)
)
@@ -565,7 +564,7 @@ class EsphomeAssistSatellite(
async def _stream_tts_audio(
self,
tts_result: tts.ResultStream,
media_id: str,
sample_rate: int = 16000,
sample_width: int = 2,
sample_channels: int = 1,
@@ -580,13 +579,14 @@ class EsphomeAssistSatellite(
if not self._is_running:
return
if tts_result.extension != "wav":
_LOGGER.error(
"Only WAV audio can be streamed, got %s", tts_result.extension
)
return
extension, data = await tts.async_get_media_source_audio(
self.hass,
media_id,
)
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
if extension != "wav":
_LOGGER.error("Only WAV audio can be streamed, got %s", extension)
return
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
if (

View File

@@ -128,23 +128,8 @@ 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:

View File

@@ -13,7 +13,6 @@ from aioesphomeapi import (
APIConnectionError,
APIVersion,
DeviceInfo as EsphomeDeviceInfo,
EncryptionHelloAPIError,
EntityInfo,
HomeassistantServiceCall,
InvalidAuthAPIError,
@@ -571,7 +570,6 @@ class ESPHomeManager:
if isinstance(
err,
(
EncryptionHelloAPIError,
RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError,
InvalidAuthAPIError,

View File

@@ -16,7 +16,7 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==29.8.0",
"aioesphomeapi==29.7.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==2.12.0"
],

View File

@@ -43,9 +43,6 @@
},
"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"

View File

@@ -193,6 +193,7 @@ 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(
@@ -200,6 +201,7 @@ 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(
@@ -223,7 +225,6 @@ 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(
@@ -231,15 +232,12 @@ 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(
key="link_noise_margin_sent",
translation_key="link_noise_margin_sent",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=_retrieve_link_noise_margin_sent_state,
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
),
@@ -247,8 +245,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
key="link_noise_margin_received",
translation_key="link_noise_margin_received",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=_retrieve_link_noise_margin_received_state,
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
),
@@ -256,8 +252,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
key="link_attenuation_sent",
translation_key="link_attenuation_sent",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=_retrieve_link_attenuation_sent_state,
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
),
@@ -265,8 +259,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
key="link_attenuation_received",
translation_key="link_attenuation_received",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=_retrieve_link_attenuation_received_state,
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
),

View File

@@ -6,7 +6,6 @@ from typing import Any
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
ClimateEntity,
@@ -39,7 +38,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_BOOST]
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
SUPPORTED_FEATURES = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
@@ -195,8 +194,6 @@ 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:
@@ -214,8 +211,6 @@ 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:

View File

@@ -182,10 +182,10 @@
"state": {
"startup": "Startup",
"running": "Running",
"standby": "[%key:common::state::standby%]",
"standby": "Standby",
"bootloading": "Bootloading",
"error": "Error",
"idle": "[%key:common::state::idle%]",
"idle": "Idle",
"ready": "Ready",
"sleeping": "Sleeping"
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250401.0"]
"requirements": ["home-assistant-frontend==20250328.0"]
}

View File

@@ -2,7 +2,7 @@
"common": {
"data_description_password": "The Remote Admin password from the Fully Kiosk Browser app settings.",
"data_description_ssl": "Is the Fully Kiosk app configured to require SSL for the connection?",
"data_description_verify_ssl": "Should SSL certificates be verified? This should be off for self-signed certificates."
"data_description_verify_ssl": "Should SSL certificartes be verified? This should be off for self-signed certificates."
},
"config": {
"step": {

View File

@@ -1 +0,0 @@
"""Gaggenau virtual integration."""

View File

@@ -1,6 +0,0 @@
{
"domain": "gaggenau",
"name": "Gaggenau",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@@ -539,14 +539,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
return
assert self._cur_temp is not None and self._target_temp is not None
min_temp = self._target_temp - self._cold_tolerance
max_temp = self._target_temp + self._hot_tolerance
too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance
too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance
if self._is_device_active:
if (self.ac_mode and self._cur_temp <= min_temp) or (
not self.ac_mode and self._cur_temp >= max_temp
):
if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot):
_LOGGER.debug("Turning off heater %s", self.heater_entity_id)
await self._async_heater_turn_off()
elif time is not None:
@@ -556,9 +552,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
self.heater_entity_id,
)
await self._async_heater_turn_on()
elif (self.ac_mode and self._cur_temp > max_temp) or (
not self.ac_mode and self._cur_temp < min_temp
):
elif (self.ac_mode and too_hot) or (not self.ac_mode and too_cold):
_LOGGER.debug("Turning on heater %s", self.heater_entity_id)
await self._async_heater_turn_on()
elif time is not None:

View File

@@ -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 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 equals or 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": {
"home_temp": "[%key:common::state::home%]",
"away_temp": "[%key:common::state::not_home%]",
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
"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": {
"home_temp": "[%key:common::state::home%]",
"away_temp": "[%key:common::state::not_home%]",
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
"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%]"
}

View File

@@ -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.3"]
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.1"]
}

View File

@@ -8,7 +8,7 @@
"integration_type": "service",
"iot_class": "cloud_push",
"requirements": [
"google-cloud-texttospeech==2.25.1",
"google-cloud-speech==2.31.1"
"google-cloud-texttospeech==2.17.2",
"google-cloud-speech==2.27.0"
]
}

View File

@@ -7,7 +7,7 @@ import logging
from types import MappingProxyType
from typing import Any
from google import genai
from google import genai # type: ignore[attr-defined]
from google.genai.errors import APIError, ClientError
from requests.exceptions import Timeout
import voluptuous as vol

View File

@@ -356,15 +356,6 @@ 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(

View File

@@ -24,8 +24,8 @@
"fix_menu": {
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
"menu_options": {
"addon_execute_start": "[%key:common::action::start%]",
"addon_disable_boot": "[%key:common::action::disable%]"
"addon_execute_start": "Start",
"addon_disable_boot": "Disable"
}
}
},

View File

@@ -2,13 +2,11 @@
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"

View File

@@ -3,9 +3,6 @@
"get_queue": {
"service": "mdi:playlist-music"
},
"remove_from_queue": {
"service": "mdi:playlist-remove"
},
"group_volume_set": {
"service": "mdi:volume-medium"
},

View File

@@ -24,10 +24,12 @@ 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,
@@ -41,16 +43,30 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_source import BrowseMediaSource
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.core import (
HomeAssistant,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
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 . import services
from .const import DOMAIN as HEOS_DOMAIN
from .const import (
DOMAIN as HEOS_DOMAIN,
SERVICE_GET_QUEUE,
SERVICE_GROUP_VOLUME_DOWN,
SERVICE_GROUP_VOLUME_SET,
SERVICE_GROUP_VOLUME_UP,
)
from .coordinator import HeosConfigEntry, HeosCoordinator
PARALLEL_UPDATES = 0
@@ -121,7 +137,25 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add media players for a config entry."""
services.register_media_player_services()
# 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"
)
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
"""Add entities for each player."""
@@ -353,15 +387,6 @@ 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")
@@ -475,10 +500,6 @@ 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."""

View File

@@ -1,33 +1,19 @@
"""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, SupportsResponse
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers.typing import VolDictType, VolSchemaType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
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,
)
@@ -58,62 +44,6 @@ 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(

View File

@@ -4,19 +4,6 @@ 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:

View File

@@ -90,16 +90,6 @@
"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."

View File

@@ -5,7 +5,6 @@ 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
@@ -120,11 +119,8 @@ class HomeConnectCoordinator(
self.__dict__.pop("context_listeners", None)
def remove_listener_and_invalidate_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)
remove_listener()
self.__dict__.pop("context_listeners", None)
return remove_listener_and_invalidate_context_listeners

View File

@@ -1,10 +1,7 @@
"""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
@@ -17,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util, slugify
@@ -45,6 +42,7 @@ class HomeConnectSensorEntityDescription(
):
"""Entity Description class for sensors."""
default_value: str | None = None
appliance_types: tuple[str, ...] | None = None
fetch_unit: bool = False
@@ -200,6 +198,7 @@ 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"),
),
@@ -207,6 +206,7 @@ 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,6 +222,7 @@ 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"),
),
@@ -229,6 +230,7 @@ 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"),
),
@@ -236,6 +238,7 @@ 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",),
),
@@ -243,6 +246,7 @@ 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",),
),
@@ -250,6 +254,7 @@ 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",),
),
@@ -257,6 +262,7 @@ 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",),
),
@@ -264,6 +270,7 @@ 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",),
),
@@ -271,6 +278,7 @@ 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",),
),
@@ -278,6 +286,7 @@ 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",),
),
@@ -285,6 +294,7 @@ 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",),
),
@@ -292,6 +302,7 @@ 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",),
),
@@ -299,6 +310,7 @@ 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",),
),
@@ -306,6 +318,7 @@ 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",),
),
@@ -313,6 +326,7 @@ 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",),
),
@@ -320,6 +334,7 @@ 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",),
),
@@ -327,6 +342,7 @@ 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",),
),
@@ -334,6 +350,7 @@ 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",),
),
@@ -341,6 +358,7 @@ 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",),
),
@@ -348,6 +366,7 @@ 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",),
),
@@ -355,6 +374,7 @@ 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",),
),
@@ -362,6 +382,7 @@ 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",),
),
@@ -369,6 +390,7 @@ 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",),
),
@@ -376,6 +398,7 @@ 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",),
),
@@ -383,6 +406,7 @@ 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",),
),
@@ -390,6 +414,7 @@ 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",),
),
@@ -397,6 +422,7 @@ 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",),
),
@@ -404,6 +430,7 @@ 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"),
),
@@ -411,6 +438,7 @@ 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"),
),
@@ -418,6 +446,7 @@ 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"),
),
@@ -425,6 +454,7 @@ 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",),
),
@@ -432,6 +462,7 @@ 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",),
),
@@ -439,6 +470,7 @@ 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",),
),
@@ -446,6 +478,7 @@ 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"),
),
@@ -453,6 +486,7 @@ 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"),
),
@@ -460,6 +494,7 @@ 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",),
),
@@ -467,6 +502,7 @@ 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",),
),
@@ -479,6 +515,12 @@ 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
@@ -492,72 +534,6 @@ 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,
@@ -570,32 +546,6 @@ 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."""
@@ -700,5 +650,8 @@ class HomeConnectEventSensor(HomeConnectSensor):
def update_native_value(self) -> None:
"""Update the sensor's status."""
event = self.appliance.events[cast(EventKey, self.bsh_key)]
self._update_native_value(event.value)
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

View File

@@ -511,7 +511,7 @@
},
"spin_speed": {
"options": {
"laundry_care_washer_enum_type_spin_speed_off": "[%key:common::state::off%]",
"laundry_care_washer_enum_type_spin_speed_off": "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": "[%key:common::state::off%]",
"laundry_care_washer_enum_type_spin_speed_ul_off": "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": "[%key:common::state::off%]",
"laundry_care_common_enum_type_vario_perfect_off": "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:common::state::off%]",
"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_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:common::state::off%]",
"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_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:common::state::off%]",
"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_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%]"
}

View File

@@ -33,7 +33,6 @@ from .util import (
OwningIntegration,
get_otbr_addon_manager,
get_zigbee_flasher_addon_manager,
guess_firmware_info,
guess_hardware_owners,
probe_silabs_firmware_info,
)
@@ -512,16 +511,6 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm a discovery."""
assert self._device is not None
fw_info = await guess_firmware_info(self.hass, self._device)
# If our guess for the firmware type is actually running, we can save the user
# an unnecessary confirmation and silently confirm the flow
for owner in fw_info.owners:
if await owner.is_running(self.hass):
self._probed_firmware_info = fw_info
return self._async_flow_finished()
return await self.async_step_pick_firmware()

View File

@@ -95,7 +95,8 @@ class BaseFirmwareUpdateEntity(
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
_attr_has_entity_name = True
# Until this entity can be associated with a device, we must manually name it
_attr_has_entity_name = False
def __init__(
self,
@@ -194,6 +195,10 @@ class BaseFirmwareUpdateEntity(
def _update_attributes(self) -> None:
"""Recompute the attributes of the entity."""
# 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"
if (

View File

@@ -3,81 +3,19 @@
from __future__ import annotations
import logging
import os.path
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
from homeassistant.components.usb import (
USBDevice,
async_register_port_event_callback,
scan_serial_ports,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.core import HomeAssistant
from .const import (
DESCRIPTION,
DEVICE,
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
MANUFACTURER,
PID,
PRODUCT,
SERIAL_NUMBER,
VID,
)
from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ZBT-1 integration."""
@callback
def async_port_event_callback(
added: set[USBDevice], removed: set[USBDevice]
) -> None:
"""Handle USB port events."""
current_entries_by_path = {
entry.data[DEVICE]: entry
for entry in hass.config_entries.async_entries(DOMAIN)
}
for device in added | removed:
path = device.device
entry = current_entries_by_path.get(path)
if entry is not None:
_LOGGER.debug(
"Device %r has changed state, reloading config entry %s",
path,
entry,
)
hass.config_entries.async_schedule_reload(entry.entry_id)
async_register_port_event_callback(hass, async_port_event_callback)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Home Assistant SkyConnect config entry."""
# Postpone loading the config entry if the device is missing
device_path = entry.data[DEVICE]
if not await hass.async_add_executor_job(os.path.exists, device_path):
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_disconnected",
)
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
return True
@@ -91,7 +29,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
"Migrating from version %s:%s", config_entry.version, config_entry.minor_version
)
if config_entry.version == 1:
@@ -126,43 +64,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
minor_version=3,
)
if config_entry.minor_version == 3:
# Old SkyConnect config entries were missing keys
if any(
key not in config_entry.data
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
):
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
serial_ports_info = {port.device: port for port in serial_ports}
device = config_entry.data[DEVICE]
if not (usb_info := serial_ports_info.get(device)):
raise HomeAssistantError(
f"USB device {device} is missing, cannot migrate"
)
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
VID: usb_info.vid,
PID: usb_info.pid,
MANUFACTURER: usb_info.manufacturer,
PRODUCT: usb_info.description,
DESCRIPTION: usb_info.description,
SERIAL_NUMBER: usb_info.serial_number,
},
version=1,
minor_version=4,
)
else:
# Existing entries are migrated by just incrementing the version
hass.config_entries.async_update_entry(
config_entry,
version=1,
minor_version=4,
)
_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,

View File

@@ -81,7 +81,7 @@ class HomeAssistantSkyConnectConfigFlow(
"""Handle a config flow for Home Assistant SkyConnect."""
VERSION = 1
MINOR_VERSION = 4
MINOR_VERSION = 3
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the config flow."""

View File

@@ -195,10 +195,5 @@
"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%]"
}
},
"exceptions": {
"device_disconnected": {
"message": "The device is not plugged in"
}
}
}

View File

@@ -168,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""SkyConnect firmware update entity."""
bootloader_reset_type = None
_attr_has_entity_name = True
def __init__(
self,

View File

@@ -152,7 +152,7 @@
},
"entity": {
"update": {
"radio_firmware": {
"firmware": {
"name": "Radio firmware"
}
}

View File

@@ -44,7 +44,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
] = {
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
key="radio_firmware",
translation_key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -56,7 +55,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
),
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
key="radio_firmware",
translation_key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -67,8 +65,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
firmware_name="OpenThread RCP",
),
ApplicationType.CPC: FirmwareUpdateEntityDescription(
key="radio_firmware",
translation_key="radio_firmware",
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -79,8 +76,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
firmware_name="Multiprotocol",
),
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
key="radio_firmware",
translation_key="radio_firmware",
key="firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -92,7 +88,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
),
None: FirmwareUpdateEntityDescription(
key="radio_firmware",
translation_key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@@ -173,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,

View File

@@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,

View File

@@ -1,200 +0,0 @@
"""The Homee climate platform."""
from typing import Any
from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeNode
from homeassistant.components.climate import (
ATTR_TEMPERATURE,
PRESET_BOOST,
PRESET_ECO,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .const import CLIMATE_PROFILES, DOMAIN, HOMEE_UNIT_TO_HA_UNIT, PRESET_MANUAL
from .entity import HomeeNodeEntity
PARALLEL_UPDATES = 0
ROOM_THERMOSTATS = {
NodeProfile.ROOM_THERMOSTAT,
NodeProfile.ROOM_THERMOSTAT_WITH_HUMIDITY_SENSOR,
NodeProfile.WIFI_ROOM_THERMOSTAT,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the climate component."""
async_add_devices(
HomeeClimate(node, config_entry)
for node in config_entry.runtime_data.nodes
if node.profile in CLIMATE_PROFILES
)
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
"""Representation of a Homee climate entity."""
_attr_name = None
_attr_translation_key = DOMAIN
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
"""Initialize a Homee climate entity."""
super().__init__(node, entry)
(
self._attr_supported_features,
self._attr_hvac_modes,
self._attr_preset_modes,
) = get_climate_features(self._node)
self._target_temp = self._node.get_attribute_by_type(
AttributeType.TARGET_TEMPERATURE
)
assert self._target_temp is not None
self._attr_temperature_unit = str(HOMEE_UNIT_TO_HA_UNIT[self._target_temp.unit])
self._attr_target_temperature_step = self._target_temp.step_value
self._attr_unique_id = f"{self._attr_unique_id}-{self._target_temp.id}"
self._heating_mode = self._node.get_attribute_by_type(
AttributeType.HEATING_MODE
)
self._temperature = self._node.get_attribute_by_type(AttributeType.TEMPERATURE)
self._valve_position = self._node.get_attribute_by_type(
AttributeType.CURRENT_VALVE_POSITION
)
@property
def hvac_mode(self) -> HVACMode:
"""Return the hvac operation mode."""
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
self._heating_mode is not None
):
if self._heating_mode.current_value == 0:
return HVACMode.OFF
return HVACMode.HEAT
@property
def hvac_action(self) -> HVACAction:
"""Return the hvac action."""
if self._heating_mode is not None and self._heating_mode.current_value == 0:
return HVACAction.OFF
if (
self._valve_position is not None and self._valve_position.current_value == 0
) or (
self._temperature is not None
and self._temperature.current_value >= self.target_temperature
):
return HVACAction.IDLE
return HVACAction.HEATING
@property
def preset_mode(self) -> str:
"""Return the present preset mode."""
if (
ClimateEntityFeature.PRESET_MODE in self.supported_features
and self._heating_mode is not None
and self._heating_mode.current_value > 0
):
assert self._attr_preset_modes is not None
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
return PRESET_NONE
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self._temperature is not None:
return self._temperature.current_value
return None
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
assert self._target_temp is not None
return self._target_temp.current_value
@property
def min_temp(self) -> float:
"""Return the lowest settable target temperature."""
assert self._target_temp is not None
return self._target_temp.minimum
@property
def max_temp(self) -> float:
"""Return the lowest settable target temperature."""
assert self._target_temp is not None
return self._target_temp.maximum
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
# Currently only HEAT and OFF are supported.
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
assert self._heating_mode is not None and self._attr_preset_modes is not None
await self.async_set_homee_value(
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
assert self._target_temp is not None
if ATTR_TEMPERATURE in kwargs:
await self.async_set_homee_value(
self._target_temp, kwargs[ATTR_TEMPERATURE]
)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 1)
async def async_turn_off(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 0)
def get_climate_features(
node: HomeeNode,
) -> tuple[ClimateEntityFeature, list[HVACMode], list[str] | None]:
"""Determine supported climate features of a node based on the available attributes."""
features = ClimateEntityFeature.TARGET_TEMPERATURE
hvac_modes = [HVACMode.HEAT]
preset_modes: list[str] = []
if (
attribute := node.get_attribute_by_type(AttributeType.HEATING_MODE)
) is not None:
features |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
hvac_modes.append(HVACMode.OFF)
if attribute.maximum > 1:
# Node supports more modes than off and heating.
features |= ClimateEntityFeature.PRESET_MODE
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
if len(preset_modes) > 0:
preset_modes.insert(0, PRESET_NONE)
return (features, hvac_modes, preset_modes if len(preset_modes) > 0 else None)

View File

@@ -95,6 +95,3 @@ LIGHT_PROFILES = [
NodeProfile.WIFI_DIMMABLE_LIGHT,
NodeProfile.WIFI_ON_OFF_DIMMABLE_METERING_SWITCH,
]
# Climate Presets
PRESET_MANUAL = "manual"

View File

@@ -1,16 +1,5 @@
{
"entity": {
"climate": {
"homee": {
"state_attributes": {
"preset_mode": {
"state": {
"manual": "mdi:hand-back-left"
}
}
}
}
},
"sensor": {
"brightness": {
"default": "mdi:brightness-5"

View File

@@ -131,17 +131,6 @@
"name": "Ventilate"
}
},
"climate": {
"homee": {
"state_attributes": {
"preset_mode": {
"state": {
"manual": "Manual"
}
}
}
}
},
"light": {
"light_instance": {
"name": "Light {instance}"
@@ -308,8 +297,8 @@
"open": "[%key:common::state::open%]",
"closed": "[%key:common::state::closed%]",
"partial": "Partially open",
"opening": "[%key:common::state::opening%]",
"closing": "[%key:common::state::closing%]"
"opening": "Opening",
"closing": "Closing"
}
},
"uv": {

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.13"],
"requirements": ["aiohomekit==3.2.8"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -141,7 +141,7 @@
"air_purifier_state_current": {
"state": {
"inactive": "Inactive",
"idle": "[%key:common::state::idle%]",
"idle": "Idle",
"purifying": "Purifying"
}
}

View File

@@ -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": {

View File

@@ -55,7 +55,7 @@
"preset_mode": {
"state": {
"hold": "Hold",
"away": "[%key:common::state::not_home%]",
"away": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
"none": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::none%]"
}
}

View File

@@ -3,34 +3,25 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Final
from aiohttp import hdrs
from aiohttp.web import Application, Request, StreamResponse, middleware
from aiohttp.web_exceptions import HTTPException
from multidict import CIMultiDict, istr
from homeassistant.core import callback
REFERRER_POLICY: Final[istr] = istr("Referrer-Policy")
X_CONTENT_TYPE_OPTIONS: Final[istr] = istr("X-Content-Type-Options")
X_FRAME_OPTIONS: Final[istr] = istr("X-Frame-Options")
@callback
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
"""Create headers middleware for the app."""
added_headers = CIMultiDict(
{
REFERRER_POLICY: "no-referrer",
X_CONTENT_TYPE_OPTIONS: "nosniff",
hdrs.SERVER: "", # Empty server header, to prevent aiohttp of setting one.
}
)
added_headers = {
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
"Server": "", # Empty server header, to prevent aiohttp of setting one.
}
if use_x_frame_options:
added_headers[X_FRAME_OPTIONS] = "SAMEORIGIN"
added_headers["X-Frame-Options"] = "SAMEORIGIN"
@middleware
async def headers_middleware(

View File

@@ -197,11 +197,5 @@
}
}
}
},
"issues": {
"deprecated_effect_none": {
"title": "Light turned on with deprecated effect",
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
}
}
}

View File

@@ -29,7 +29,6 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util import color as color_util
from ..bridge import HueBridge
@@ -45,9 +44,6 @@ FALLBACK_MIN_KELVIN = 6500
FALLBACK_MAX_KELVIN = 2000
FALLBACK_KELVIN = 5800 # halfway
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
DEPRECATED_EFFECT_NONE = "None"
async def async_setup_entry(
hass: HomeAssistant,
@@ -237,23 +233,6 @@ class HueLight(HueBaseEntity, LightEntity):
self._color_temp_active = color_temp is not None
flash = kwargs.get(ATTR_FLASH)
effect = effect_str = kwargs.get(ATTR_EFFECT)
if effect_str == DEPRECATED_EFFECT_NONE:
# deprecated effect "None" is now "off"
effect_str = EFFECT_OFF
async_create_issue(
self.hass,
DOMAIN,
"deprecated_effect_none",
breaks_in_ha_version="2025.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_effect_none",
)
self.logger.warning(
"Detected deprecated effect 'None' in %s, use 'off' instead. "
"This will stop working in HA 2025.10",
self.entity_id,
)
if effect_str == EFFECT_OFF:
# ignore effect if set to "off" and we have no effect active
# the special effect "off" is only used to stop an active effect

View File

@@ -63,14 +63,14 @@
"name": "Mode",
"state": {
"normal": "Normal",
"home": "[%key:common::state::home%]",
"away": "[%key:common::state::not_home%]",
"auto": "Auto",
"baby": "Baby",
"eco": "Eco",
"away": "Away",
"boost": "Boost",
"comfort": "Comfort",
"eco": "Eco",
"sleep": "Sleep"
"home": "[%key:common::state::home%]",
"sleep": "Sleep",
"auto": "Auto",
"baby": "Baby"
}
}
}

Some files were not shown because too many files have changed in this diff Show More