Compare commits

...

112 Commits

Author SHA1 Message Date
Franck Nijhof
f7794ea6b5 2025.4.2 (#142755) 2025-04-12 11:43:50 +02:00
Sanjay Govind
7a1bea7ff5 Add jaraco.itertools license exception as the classifier was removed but no SPDX expression was added (#142439) 2025-04-11 21:35:35 +00:00
Thomas55555
c7c645776d Bump ical to 9.1.0 (#142197) 2025-04-11 21:35:28 +00:00
Franck Nijhof
667cb772e9 Bump version to 2025.4.2 2025-04-11 16:12:45 +00:00
Jeff Rescignano
933d008e52 Upgrade sharkiq depedency to 1.1.0 (#142746) 2025-04-11 16:12:06 +00:00
Allen Porter
d868f39aea Fix Anthropic bug parsing a streaming response with no json (#142745) 2025-04-11 15:42:08 +00:00
Joost Lekkerkerker
28d776a0b0 Fix SmartThings gas meter (#142741) 2025-04-11 15:42:04 +00:00
Joost Lekkerkerker
b5d541b596 Bump pySmartThings to 3.0.4 (#142739) 2025-04-11 15:41:59 +00:00
Bram Kragten
4948499889 Update frontend to 20250411.0 (#142736) 2025-04-11 15:41:55 +00:00
starkillerOG
7696b101f6 Reolink migrate unique ID debugging (#142723)
* Filter out unexpected unique_ids

* correct

* Add test

* fix styling
2025-04-11 15:41:51 +00:00
starkillerOG
fd2987a9fd Bump reolink-aio 0.13.1 (#142719) 2025-04-11 15:41:47 +00:00
Christopher Fenner
4c1d32020a Bump PyViCare to 2.44.0 (#142701)
bump vicare to v2.44.0
2025-04-11 15:41:42 +00:00
Jan Bouwhuis
b40bdab0ae Fix EC certificate key not allowed in MQTT client setup (#142698) 2025-04-11 15:41:38 +00:00
Simone Chemelli
d192aecd3b Comelit config flow timeout error (#142667) 2025-04-11 15:41:34 +00:00
Thomas55555
d1781f5766 Bump livisi to 0.0.25 (#142638) 2025-04-11 15:41:29 +00:00
henryptung
2c4461457a Bump led_ble to 1.1.7 (#142629)
changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v1.1.6...v1.1.7
2025-04-11 15:40:35 +00:00
J. Nick Koston
82959081de Pin multidict to >= 6.4.2 to resolve memory leaks (#142614)
* Pin multidict to >= 6.4.1 to resolve memory leaks

https://github.com/aio-libs/multidict/issues/1134
https://github.com/aio-libs/multidict/issues/1131
https://github.com/aio-libs/multidict/releases/tag/v6.4.1
https://github.com/aio-libs/multidict/releases/tag/v6.4.0

* Apply suggestions from code review
2025-04-11 15:39:38 +00:00
Thimo Seitz
acdac6d5e8 Update growatt server dependency to 1.6.0 (#142606)
* Update GrowattServer Dependency

* Update requirements_test_all.txt
2025-04-11 15:39:34 +00:00
Fredrik Erlandsson
d3d7889883 Fix ssl_cert load from config_flow (#142570)
fix ssl_cert load from config_flow
2025-04-11 15:39:29 +00:00
puddly
60ece3e1c9 Fix Core deadlock by ensuring only one ZHA log queue handler thread is running at a time (#142568)
Ensure only one log queue handler is running at a time
2025-04-11 15:39:25 +00:00
Christopher Fenner
a9f8529460 Fix Quickmode handling in ViCare integration (#142561)
* only check quickmode if supported

* update snapshot

* revert
2025-04-11 15:39:20 +00:00
Andrew Sayre
ec53b61f9e Bump pyheos to v1.0.5 (#142554)
Update pyheos
2025-04-11 15:39:16 +00:00
Thomas55555
e9f02edd8b Fix adding devices in Husqvarna Automower (#142549) 2025-04-11 15:39:12 +00:00
Marcel van der Veldt
d1b7898219 Fix small typo in Music Assistant integration causing unavailable players (#142535)
Fix small typo in Music Assistant integration causing issues with adding players
2025-04-11 15:39:08 +00:00
Jan Bouwhuis
8dc21ef619 Allow max to be equal with min for mqtt number config validation (#142522) 2025-04-11 15:39:03 +00:00
tronikos
d9f91598a5 Fix range of Google Generative AI temperature (#142513) 2025-04-11 15:34:29 +00:00
Ivan Lopez Hernandez
c540acf2bd Handle None on the response candidates in Google Generative AI (#142497)
* Added type checking on the candidates list

* Made error message a constant
2025-04-11 15:34:24 +00:00
Maciej Bieniek
f702f3efcd Fix Shelly initialization if device runs large script (#142487)
* Don't check the whole script to see if it generates events

* Fix tests

---------

Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-04-11 15:34:20 +00:00
Wilfred Ketelaar
9410061405 Fixed Renault charge state icon (#142478)
Fixed charge state icon (duplicate mdi prefix)
2025-04-11 15:34:15 +00:00
Maciej Bieniek
485b28d9ea Bump aioshelly to version 13.4.1 (#142477)
* Bymp aioshelly to 13.4.1

* Catch InvalidHostError

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-04-11 15:34:10 +00:00
epenet
d59200a9f5 Fix kelvin parameter in light action specifications (#142456) 2025-04-11 15:34:05 +00:00
starkillerOG
44a92ca81c Fix Reolink smart AI sensors (#142454) 2025-04-11 15:34:01 +00:00
J. Nick Koston
d39fa39a03 Fix HKC showing hvac_action as idle when fan is active and heat cool target is off (#142443)
* Fix HKC showing hvac_action as idle when fan is active and heat cool target is off

fixes #142442

* comment relocation
2025-04-11 15:33:56 +00:00
Michael
36ec857523 Fix reload of AVM FRITZ!Tools when new connected device is detected (#142430) 2025-04-11 15:33:51 +00:00
Simone Chemelli
fcb8cdc146 Add missing strings to Fritz (#142413)
* Add missing strings to Fritz

* update quality scale

* add common section

this avoids later re-structuring and re-translating

* fix strings

* fix strings

* apply review comment

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2025-04-11 15:33:46 +00:00
Simone Chemelli
2322b0b65f Add exceptions translation to SamsungTV (#142406)
* Add exceptions translation to SmasungTV

* Update strings.json

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-04-11 15:33:42 +00:00
tronikos
87baaf4255 Bump opower to 0.11.1 (#142395)
* Bump opower to 0.10.1

* opower==0.11.0

* opower==0.11.1

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-04-11 15:33:37 +00:00
J. Nick Koston
b7f0e877f0 Bump aioesphomeapi to 29.9.0 (#142393)
changelog: https://github.com/esphome/aioesphomeapi/compare/v29.8.0...v29.9.0

fixes #142381
2025-04-11 15:33:33 +00:00
Jan-Philipp Benecke
5d92a04732 Only load files ending .metadata.json in WebDAV (#142388) 2025-04-11 15:33:29 +00:00
Álvaro Fernández Rojas
8ff879df22 Update aioairzone to v1.0.0 (#142385)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2025-04-11 15:33:24 +00:00
J. Nick Koston
9fb7ee676e Bump flux_led to 1.2.0 (#142362)
changelog: https://github.com/lightinglibs/flux_led/compare/1.1.3...1.2.0
2025-04-11 15:33:20 +00:00
Jan Bouwhuis
2c855a3986 Limit mqtt info logging for discovery of new components (#142344)
* Limit mqtt info logging for discovery of new component

* Keep in bail out, when debug logging is not enabled
2025-04-11 15:33:14 +00:00
Luke Lashley
cdd4894e30 Check that the current roboorck map exists before updating it. (#142341)
* Check that the current map exists

* Add a few extra checks

* Update coordinator.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* fixlint

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-04-11 15:33:09 +00:00
tronikos
5f26226712 Add a description for the enable_google_search_tool option in Google AI (#142322)
* Add a description for the enable_google_search_tool option in Google AI

* Use quotes
2025-04-11 15:33:05 +00:00
tronikos
8baf61031d Bump opower to 0.10.0 (#142321) 2025-04-11 15:33:00 +00:00
Andre Lengwenus
e90ba40553 Add SensorDeviceClass and unit for LCN CO2 sensor. (#142320)
Add SesnorDeviceClass and unit for LCN CO2 sensor.
2025-04-11 15:32:54 +00:00
Luke Lashley
b38016425f Update Roborock map more consistently on state change (#142228)
* update map more consistently on state change

* Makecoordinator keep track of last_updated_state
2025-04-11 15:32:49 +00:00
Thomas55555
ee5e3f7691 Add error details in remote calendar flow (#141753)
* Add error details in remote calendar flow

* no args

* adjust

* json

* Apply suggestions

* remove description placeholder
2025-04-11 15:32:41 +00:00
Franck Nijhof
7af6a4f493 2025.4.1 (#142299)
* Fix blocking event loop - daikin (#141442)

* fix blocking event loop

* create ssl_context directly

* update manifest

* update manifest.json

* Made Google Search enable dependent on Assist availability (#141712)

* Made Google Search enable dependent on Assist availability

* Show error instead of rendering again

* Cleanup test code

* Fix humidifier platform for Comelit (#141854)

* Fix humidifier platform for Comelit

* apply review comment

* Bump evohome-async to 1.0.5 (#141871)

bump client to 1.0.5

* Replace "to log into" with "to log in to" in `incomfort` (#142060)

* Replace "to log into" with "to log in to" in `incomfort`

Also fix one missing sentence-casing of "gateway".

* Replace duplicate "data_description" strings with references

* Avoid unnecessary reload in apple_tv reauth flow (#142079)

* Add translation for hassio update entity name (#142090)

* Bump pyenphase to 1.25.5 (#142107)

* Hide broken ZBT-1 config entries on the hardware page (#142110)

* Hide bad ZBT-1 config entries on the hardware page

* Set up the bad config entry in the unit test

* Roll into a list comprehension

* Remove constant changes

* Fix condition in unit test

* Bump pysmhi to 1.0.1 (#142111)

* Avoid logging a warning when replacing an ignored config entry (#142114)

Replacing an ignored config entry with one from the user
flow should not generate a warning. We should only warn
if we are replacing a usable config entry.

Followup to adjust the warning added in #130567
cc @epenet

* Slow down polling in Tesla Fleet (#142130)

* Slow down polling

* Fix tests

* Bump tesla-fleet-api to v1.0.17 (#142131)

bump

* Tado bump to 0.18.11 (#142175)

* Bump to version 0.18.11

* Adding hassfest files

* Add preset mode to SmartThings climate (#142180)

* Add preset mode to SmartThings climate

* Add preset mode to SmartThings climate

* Do not create a HA mediaplayer for the builtin Music Assistant player (#142192)

Do not create a HA mediaplayer for the builtin Music player

* Do not fetch disconnected Home Connect appliances (#142200)

* Do not fetch disconnected Home Connect appliances

* Apply suggestions

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

* Update docstring

---------

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

* Fix fibaro setup (#142201)

* Fix circular mean by always storing and using the weighted one (#142208)

* Fix circular mean by always storing and using the weighted one

* fix

* Fix test

* Bump pySmartThings to 3.0.2 (#142257)

Co-authored-by: Robert Resch <robert@resch.dev>

* Update frontend to 20250404.0 (#142274)

* Bump forecast-solar lib to v4.1.0 (#142280)

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>

* Bump version to 2025.4.1

* Fix skyconnect tests (#142262)

fix tests

* Fix empty actions (#142292)

* Apply fix

* Add tests for alarm button cover lock

* update light

* add number tests

* test select

* add switch tests

* test vacuum

* update lock test

---------

Co-authored-by: Fredrik Erlandsson <fredrik.e@gmail.com>
Co-authored-by: Ivan Lopez Hernandez <ivan.lh.94@outlook.com>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
Co-authored-by: David Bonnes <zxdavb@bonnes.me>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: rappenze <rappenze@yahoo.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Klaas Schoute <klaas_schoute@hotmail.com>
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Petro31 <35082313+Petro31@users.noreply.github.com>
2025-04-04 22:59:10 +02:00
Petro31
c25f26a290 Fix empty actions (#142292)
* Apply fix

* Add tests for alarm button cover lock

* update light

* add number tests

* test select

* add switch tests

* test vacuum

* update lock test
2025-04-04 20:18:31 +00:00
Josef Zweck
8d62cb60a6 Fix skyconnect tests (#142262)
fix tests
2025-04-04 20:18:27 +00:00
Franck Nijhof
4f799069ea Bump version to 2025.4.1 2025-04-04 19:24:45 +00:00
Klaas Schoute
af708b78e0 Bump forecast-solar lib to v4.1.0 (#142280)
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2025-04-04 19:24:30 +00:00
Bram Kragten
f46e659740 Update frontend to 20250404.0 (#142274) 2025-04-04 19:24:27 +00:00
Joost Lekkerkerker
7bd517e6ff Bump pySmartThings to 3.0.2 (#142257)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-04-04 19:24:23 +00:00
Robert Resch
e9abdab1f5 Fix circular mean by always storing and using the weighted one (#142208)
* Fix circular mean by always storing and using the weighted one

* fix

* Fix test
2025-04-04 19:24:20 +00:00
rappenze
86eee4f041 Fix fibaro setup (#142201) 2025-04-04 19:24:17 +00:00
J. Diego Rodríguez Royo
9db60c830c Do not fetch disconnected Home Connect appliances (#142200)
* Do not fetch disconnected Home Connect appliances

* Apply suggestions

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

* Update docstring

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-04 19:24:13 +00:00
Marcel van der Veldt
c43a4682b9 Do not create a HA mediaplayer for the builtin Music Assistant player (#142192)
Do not create a HA mediaplayer for the builtin Music player
2025-04-04 19:24:10 +00:00
Joost Lekkerkerker
2a4996055a Add preset mode to SmartThings climate (#142180)
* Add preset mode to SmartThings climate

* Add preset mode to SmartThings climate
2025-04-04 19:24:07 +00:00
Erwin Douna
4643fc2c14 Tado bump to 0.18.11 (#142175)
* Bump to version 0.18.11

* Adding hassfest files
2025-04-04 19:24:04 +00:00
Brett Adams
6410b90d82 Bump tesla-fleet-api to v1.0.17 (#142131)
bump
2025-04-04 19:24:00 +00:00
Brett Adams
e5c00eceae Slow down polling in Tesla Fleet (#142130)
* Slow down polling

* Fix tests
2025-04-04 19:23:55 +00:00
J. Nick Koston
fe65579df8 Avoid logging a warning when replacing an ignored config entry (#142114)
Replacing an ignored config entry with one from the user
flow should not generate a warning. We should only warn
if we are replacing a usable config entry.

Followup to adjust the warning added in #130567
cc @epenet
2025-04-04 19:23:52 +00:00
G Johansson
281beecb05 Bump pysmhi to 1.0.1 (#142111) 2025-04-04 19:23:48 +00:00
puddly
7546b5d269 Hide broken ZBT-1 config entries on the hardware page (#142110)
* Hide bad ZBT-1 config entries on the hardware page

* Set up the bad config entry in the unit test

* Roll into a list comprehension

* Remove constant changes

* Fix condition in unit test
2025-04-04 19:23:45 +00:00
Arie Catsman
490e3201b9 Bump pyenphase to 1.25.5 (#142107) 2025-04-04 19:23:42 +00:00
Paul Bottein
04be575139 Add translation for hassio update entity name (#142090) 2025-04-04 19:23:39 +00:00
Erik Montnemery
854cae7f12 Avoid unnecessary reload in apple_tv reauth flow (#142079) 2025-04-04 19:23:35 +00:00
Norbert Rittel
109d20978f Replace "to log into" with "to log in to" in incomfort (#142060)
* Replace "to log into" with "to log in to" in `incomfort`

Also fix one missing sentence-casing of "gateway".

* Replace duplicate "data_description" strings with references
2025-04-04 19:23:32 +00:00
David Bonnes
f8d284ec4b Bump evohome-async to 1.0.5 (#141871)
bump client to 1.0.5
2025-04-04 19:23:28 +00:00
Simone Chemelli
06ebe0810f Fix humidifier platform for Comelit (#141854)
* Fix humidifier platform for Comelit

* apply review comment
2025-04-04 19:23:25 +00:00
Ivan Lopez Hernandez
802ad2ff51 Made Google Search enable dependent on Assist availability (#141712)
* Made Google Search enable dependent on Assist availability

* Show error instead of rendering again

* Cleanup test code
2025-04-04 19:23:22 +00:00
Fredrik Erlandsson
9070a8d579 Fix blocking event loop - daikin (#141442)
* fix blocking event loop

* create ssl_context directly

* update manifest

* update manifest.json
2025-04-04 19:23:18 +00:00
Franck Nijhof
e8b2a3de8b 2025.4.0 (#141505) 2025-04-02 18:47:40 +02:00
Joost Lekkerkerker
39549d5dd4 Fix switch name Unknown in SmartThings (#142081)
Fix switch name Unknown
2025-04-02 15:16:50 +00:00
Franck Nijhof
0c19e47bd4 Bump version to 2025.4.0 2025-04-02 15:02:28 +00:00
Michael
05507d77e3 Fix state class for battery sensors in AVM Fritz!SmartHome (#142078)
* set proper state class for battery sensor

* fix tests
2025-04-02 15:02:04 +00:00
Franck Nijhof
94558e2d40 Bump version to 2025.4.0b15 2025-04-02 14:19:49 +00:00
puddly
4f22fe8f7f Translation key for ZBT-1 integration failing due to disconnection (#142077)
Translation key for device disconnected
2025-04-02 14:19:41 +00:00
Marcel van der Veldt
9e7dfbb857 Deprecate None effect instead of breaking it for Hue (#142073)
* Deprecate effect none instead of breaking it for Hue

* add guard for unknown effect value

* revert guard

* Fix

* Add test

* Add test

* Add test

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-04-02 14:19:38 +00:00
Joost Lekkerkerker
02d182239a Improve SmartThings switch deprecation (#142072) 2025-04-02 14:19:35 +00:00
Joost Lekkerkerker
4e0f581747 Improve SmartThings sensor deprecation (#142070)
* Improve SmartThings sensor deprecation

* Improve SmartThings sensor deprecation

* Improve SmartThings sensor deprecation
2025-04-02 14:19:32 +00:00
Joost Lekkerkerker
42d97d348c Add Eve brand (#142067) 2025-04-02 14:19:29 +00:00
Robert Resch
69380c85ca Bump deebot-client to 12.5.0 (#142046) 2025-04-02 14:19:25 +00:00
Abílio Costa
b38c647830 Allow excluding modules from noisy logs check (#142020)
* Allow excluding modules from noisy logs check

* Cache non-excluded modules; hardcode self module name; optimize call

* Address review comments
2025-04-02 14:19:22 +00:00
Petro31
2396fd1090 Fix weather templates using new style configuration (#136677) 2025-04-02 14:19:19 +00:00
Franck Nijhof
aa4eb89eee Bump version to 2025.4.0b14 2025-04-02 09:44:23 +00:00
J. Nick Koston
1b1bc6af95 Bump bluetooth-data-tools to 1.26.5 (#142045)
changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.26.1...v1.26.5
2025-04-02 09:36:51 +00:00
J. Nick Koston
f17003a79c Bump aiohttp to 3.11.16 (#142034)
changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.15...v3.11.16
2025-04-02 09:34:14 +00:00
TheJulianJES
ec70e8b0cd Bump ZHA to 0.0.55 (#142031) 2025-04-02 08:29:26 +00:00
puddly
d888c70ff0 Fix entity names for HA hardware firmware update entities (#142029)
* Fix entity names for HA hardware firmware update entities

* Fix unit tests
2025-04-02 08:29:23 +00:00
puddly
f29444002e Skip firmware config flow confirmation if the hardware is in use (#142017)
* Auto-confirm the discovery if we detect that the device is already in use

* Add a unit test
2025-04-02 08:29:20 +00:00
Tomek Wasilczyk
fc66997a36 Fix warning about unfinished oauth tasks on shutdown (#141969)
* Don't wait for OAuth token task on shutdown

To reproduce the warning:
1. Start authentication with integration using OAuth (e.g. SmartThings)
2. When redirected to external login site, just close the page
3. Settings -> Restart Home Assistant

* Clarify comment

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-02 08:29:16 +00:00
Erik Montnemery
35513ae072 Remove unused mypy ignore from google_generative_ai_conversation (#141549) 2025-04-02 08:29:13 +00:00
Franck Nijhof
cd363d48c3 Bump version to 2025.4.0b13 2025-04-01 19:12:16 +00:00
G Johansson
d47ef835d7 Fix train to for multiple stations in Trafikverket Train (#142016) 2025-04-01 19:11:51 +00:00
Bram Kragten
00177c699e Update frontend to 20250401.0 (#142010) 2025-04-01 19:11:48 +00:00
Joost Lekkerkerker
11b0086a01 Add LG ThinQ event bus listener to lifecycle hooks (#142006) 2025-04-01 19:11:44 +00:00
J. Nick Koston
ceb177f80e Bump aiohttp to 3.11.15 (#141967)
changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.14...v3.11.15

fixes #141855
fixes #141146
2025-04-01 19:10:28 +00:00
Jan Bouwhuis
fa3832fbd7 Improve error handling and logging on MQTT update entity state updates when template rederings fails (#141960) 2025-04-01 19:07:10 +00:00
puddly
2b9c903429 Fix data in old SkyConnect integration config entries or delete them (#141959)
* Delete old SkyConnect integration config entries

* Try migrating, if possible

* Do not delete config entries, log a failure
2025-04-01 19:07:07 +00:00
puddly
a7c43f9b49 Reload the ZBT-1 integration on USB state changes (#141287)
* Reload the config entry when the ZBT-1 is unplugged

* Register the USB event handler globally to react better to re-plugs

* Fix existing unit tests

* Add an empty `CONFIG_SCHEMA`

* Add a unit test

* Fix unit tests

* Fix unit tests for Linux

* Address most review comments

* Address remaining review comments
2025-04-01 19:07:03 +00:00
Joost Lekkerkerker
b428196149 Improve SmartThings deprecation (#141939)
* Improve SmartThings deprecation

* Improve SmartThings deprecation
2025-04-01 19:01:43 +00:00
Erik Montnemery
e23da1a90f Fix import issues related to onboarding views (#141919)
* Fix import issues related to onboarding views

* Add ha-intents and numpy to pyproject.toml

* Add more requirements to pyproject.toml

* Add more requirements to pyproject.toml
2025-04-01 19:00:24 +00:00
Ben Jones
3951c2ea66 Handle empty or missing state values for MQTT light entities using 'template' schema (#141177)
* check for empty or missing values when processing state messages for MQTT light entities using 'template' schema

* normalise warning logs

* add tests (one is still failing and I can't work out why)

* fix test

* improve test coverage after PR review

* improve test coverage after PR review
2025-04-01 18:32:50 +00:00
Louis Christ
fee152654d Use saved volume when selecting preset in bluesound integration (#141079)
* Use load_preset to select preset as source

* Add tests

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-04-01 18:32:47 +00:00
Mikko Koo
51073c948c Fix nordpool Not to return Unknown if price is exactly 0 (#140647)
* now the price will return even if it is exactly 0

* now the price will return even if it is exactly 0

* now the price will return even if it is exactly 0

* clean code

* clean code

* update testing code coverage

* change zero testing to SE4

* remove row duplicate

* fix date comments

* improve testing

* simplify if-return-0

* remove unnecessary tests

* order testing rows

* restore test_sensor_no_next_price

* remove_average_price_test

* fix test name
2025-04-01 18:32:44 +00:00
aaronburt
91438088a0 Correct unit conversion for OneDrive quota display (#140337)
* Correct unit conversion for OneDrive quota display

* Convert OneDrive quota values from bytes to GiB in coordinator and update strings
2025-04-01 18:32:39 +00:00
Franck Nijhof
427e1abdae Bump version to 2025.4.0b12 2025-03-31 20:12:58 +00:00
Steven Looman
6e7ac45ac0 Bump async-upnp-client to 0.44.0 (#141946) 2025-03-31 20:12:48 +00:00
Bram Kragten
4b3b9ebc29 Update frontend to 20250331.0 (#141943) 2025-03-31 20:12:43 +00:00
202 changed files with 10693 additions and 2342 deletions

View File

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

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.9"]
"requirements": ["aioairzone==1.0.0"]
}

View File

@@ -266,7 +266,7 @@ async def _transform_stream(
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
tool_block = cast(ToolUseBlockParam, current_block)
tool_args = json.loads(current_tool_args)
tool_args = json.loads(current_tool_args) if current_tool_args else {}
tool_block["input"] = tool_args
yield {
"tool_calls": [

View File

@@ -20,6 +20,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_ZEROCONF,
ConfigEntry,
ConfigFlow,
@@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IDENTIFIERS: list(combined_identifiers),
},
)
if entry.source != SOURCE_IGNORE:
# Don't reload ignored entries or in the middle of reauth,
# e.g. if the user is entering a new PIN
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
if not allow_exist:
raise DeviceAlreadyConfigured

View File

@@ -501,18 +501,16 @@ 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:
url = input_.url
await self._player.play_url(input_.url)
return
for preset in self._presets:
if preset.name == source:
url = preset.url
await self._player.load_preset(preset.id)
return
if url is None:
raise ServiceValidationError(f"Source {source} not found")
await self._player.play_url(url)
raise ServiceValidationError(f"Source {source} not found")
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.1",
"bluetooth-data-tools==1.26.5",
"dbus-fast==2.43.0",
"habluetooth==3.37.0"
]

View File

@@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
flow_id=flow_id, user_input=tokens
)
self.hass.async_create_task(await_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"
)
return authorize_url

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from asyncio.exceptions import TimeoutError
from collections.abc import Mapping
from typing import Any
@@ -53,10 +54,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
try:
await api.login()
except aiocomelit_exceptions.CannotConnect as err:
raise CannotConnect from err
except (aiocomelit_exceptions.CannotConnect, TimeoutError) as err:
raise CannotConnect(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"error": repr(err)},
) from err
except aiocomelit_exceptions.CannotAuthenticate as err:
raise InvalidAuth from err
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
await api.close()

View File

@@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
if self.mode == HumidifierComelitMode.OFF:
if not self._attr_is_on:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="humidity_while_off",
@@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
await self.coordinator.api.set_humidity_status(
self._device.index, self._set_command
)
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off."""
await self.coordinator.api.set_humidity_status(
self._device.index, HumidifierComelitCommand.OFF
)
self._attr_is_on = False
self.async_write_ha_state()

View File

@@ -52,7 +52,9 @@
"rest": "Rest",
"sabotated": "Sabotated"
}
},
}
},
"humidifier": {
"humidifier": {
"name": "Humidifier"
},
@@ -67,6 +69,12 @@
},
"invalid_clima_data": {
"message": "Invalid 'clima' data"
},
"cannot_connect": {
"message": "Error connecting: {error}"
},
"cannot_authenticate": {
"message": "Error authenticating: {error}"
}
}
}

View File

@@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util.ssl import client_context_no_verify
from .const import KEY_MAC, TIMEOUT
from .coordinator import DaikinConfigEntry, DaikinCoordinator
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
key=entry.data.get(CONF_API_KEY),
uuid=entry.data.get(CONF_UUID),
password=entry.data.get(CONF_PASSWORD),
ssl_context=client_context_no_verify(),
)
_LOGGER.debug("Connection to %s successful", host)
except TimeoutError as err:

View File

@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.util.ssl import client_context_no_verify
from .const import DOMAIN, KEY_MAC, TIMEOUT
@@ -90,6 +91,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
key=key,
uuid=uuid,
password=password,
ssl_context=client_context_no_verify(),
)
except (TimeoutError, ClientError):
self.host = None

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.14.1"],
"requirements": ["pydaikin==2.15.0"],
"zeroconf": ["_dkapi._tcp.local."]
}

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.43.0", "getmac==0.9.5"],
"requirements": ["async-upnp-client==0.44.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.43.0"],
"requirements": ["async-upnp-client==0.44.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

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.4.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.25.1"],
"requirements": ["pyenphase==1.25.5"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

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

View File

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

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.4"]
"requirements": ["evohome-async==1.0.5"]
}

View File

@@ -301,6 +301,7 @@ class FibaroController:
device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
)
platform = None
if device.enabled and (not device.is_plugin or self._import_plugins):
platform = self._map_device_to_platform(device)
if platform is None:

View File

@@ -53,5 +53,5 @@
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"iot_class": "local_push",
"loggers": ["flux_led"],
"requirements": ["flux-led==1.1.3"]
"requirements": ["flux-led==1.2.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["forecast-solar==4.0.0"]
"requirements": ["forecast-solar==4.1.0"]
}

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, DOMAIN, MeshRoles
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles
from .coordinator import (
FRITZ_DATA_KEY,
AvmWrapper,
@@ -175,16 +175,6 @@ class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity):
self._name = f"{self.hostname} Wake on LAN"
self._attr_unique_id = f"{self._mac}_wake_on_lan"
self._is_available = True
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, self._mac)},
default_manufacturer="AVM",
default_model="FRITZ!Box Tracked device",
default_name=device.hostname,
via_device=(
DOMAIN,
avm_wrapper.unique_id,
),
)
async def async_press(self) -> None:
"""Press the button."""

View File

@@ -526,7 +526,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
def manage_device_info(
self, dev_info: Device, dev_mac: str, consider_home: bool
) -> bool:
"""Update device lists."""
"""Update device lists and return if device is new."""
_LOGGER.debug("Client dev_info: %s", dev_info)
if dev_mac in self._devices:
@@ -536,6 +536,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
device = FritzDevice(dev_mac, dev_info.name)
device.update(dev_info, consider_home)
self._devices[dev_mac] = device
# manually register device entry for new connected device
dr.async_get(self.hass).async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, dev_mac)},
default_manufacturer="AVM",
default_model="FRITZ!Box Tracked device",
default_name=device.hostname,
via_device=(DOMAIN, self.unique_id),
)
return True
async def async_send_signal_device_update(self, new_device: bool) -> None:

View File

@@ -26,6 +26,9 @@ class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):
self._avm_wrapper = avm_wrapper
self._mac: str = device.mac_address
self._name: str = device.hostname or DEFAULT_DEVICE_NAME
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)}
)
@property
def name(self) -> str:

View File

@@ -7,9 +7,7 @@ rules:
config-flow-test-coverage:
status: todo
comment: one coverage miss in line 110
config-flow:
status: todo
comment: data_description are missing
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done

View File

@@ -1,4 +1,11 @@
{
"common": {
"data_description_host": "The hostname or IP address of your FRITZ!Box router.",
"data_description_port": "Leave empty to use the default port.",
"data_description_username": "Username for the FRITZ!Box.",
"data_description_password": "Password for the FRITZ!Box.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box."
},
"config": {
"flow_title": "{name}",
"step": {
@@ -9,6 +16,11 @@
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]"
},
"data_description": {
"username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
}
},
"reauth_confirm": {
@@ -17,6 +29,10 @@
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]"
}
},
"reconfigure": {
@@ -28,8 +44,9 @@
"ssl": "[%key:common::config_flow::data::ssl%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router.",
"port": "Leave it empty to use the default port."
"host": "[%key:component::fritz::common::data_description_host%]",
"port": "[%key:component::fritz::common::data_description_port%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
}
},
"user": {
@@ -43,8 +60,11 @@
"ssl": "[%key:common::config_flow::data::ssl%]"
},
"data_description": {
"host": "The hostname or IP address of your FRITZ!Box router.",
"port": "Leave it empty to use the default port."
"host": "[%key:component::fritz::common::data_description_host%]",
"port": "[%key:component::fritz::common::data_description_port%]",
"username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
}
}
},
@@ -70,6 +90,10 @@
"data": {
"consider_home": "Seconds to consider a device at 'home'",
"old_discovery": "Enable old discovery method"
},
"data_description": {
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
"old_discovery": "Enable old discovery method. This is needed for some scenarios."
}
}
}
@@ -169,8 +193,12 @@
"config_entry_not_found": {
"message": "Failed to perform action \"{service}\". Config entry for target not found"
},
"service_parameter_unknown": { "message": "Action or parameter unknown" },
"service_not_supported": { "message": "Action not supported" },
"service_parameter_unknown": {
"message": "Action or parameter unknown"
},
"service_not_supported": {
"message": "Action not supported"
},
"error_refresh_hosts_info": {
"message": "Error refreshing hosts info"
},

View File

@@ -511,16 +511,6 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
self._name = f"{device.hostname} Internet Access"
self._attr_unique_id = f"{self._mac}_internet_access"
self._attr_entity_category = EntityCategory.CONFIG
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, self._mac)},
default_manufacturer="AVM",
default_model="FRITZ!Box Tracked device",
default_name=device.hostname,
via_device=(
DOMAIN,
avm_wrapper.unique_id,
),
)
@property
def is_on(self) -> bool | None:

View File

@@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
key="battery",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suitable=lambda device: device.battery_level is not None,
native_value=lambda device: device.battery_level,

View File

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

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.1.0"]
}

View File

@@ -7,7 +7,7 @@ import logging
from types import MappingProxyType
from typing import Any
from google import genai # type: ignore[attr-defined]
from google import genai
from google.genai.errors import APIError, ClientError
from requests.exceptions import Timeout
import voluptuous as vol
@@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
errors: dict[str, str] = {}
if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API)
return self.async_create_entry(title="", data=user_input)
if not (
user_input.get(CONF_LLM_HASS_API, "none") != "none"
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
):
# Don't allow to save options that enable the Google Seearch tool with an Assist API
return self.async_create_entry(title="", data=user_input)
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
# Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
}
options = user_input
schema = await google_generative_ai_config_option_schema(
self.hass, options, self._genai_client
)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(schema),
step_id="init", data_schema=vol.Schema(schema), errors=errors
)
@@ -301,7 +303,7 @@ async def google_generative_ai_config_option_schema(
CONF_TEMPERATURE,
description={"suggested_value": options.get(CONF_TEMPERATURE)},
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
vol.Optional(
CONF_TOP_P,
description={"suggested_value": options.get(CONF_TOP_P)},

View File

@@ -55,6 +55,10 @@ from .const import (
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
ERROR_GETTING_RESPONSE = (
"Sorry, I had a problem getting a response from Google Generative AI."
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity(
raise HomeAssistantError(
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
)
if not chat_response.candidates:
LOGGER.error(
"No candidates found in the response: %s",
chat_response,
)
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
except (
APIError,
@@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity(
response_parts = chat_response.candidates[0].content.parts
if not response_parts:
raise HomeAssistantError(
"Sorry, I had a problem getting a response from Google Generative AI."
)
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
content = " ".join(
[part.text.strip() for part in response_parts if part.text]
)

View File

@@ -40,9 +40,13 @@
"enable_google_search_tool": "Enable Google Search tool"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template."
"prompt": "Instruct how the LLM should respond. This can be a template.",
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
}
}
},
"error": {
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
}
},
"services": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
"iot_class": "cloud_polling",
"loggers": ["growattServer"],
"requirements": ["growattServer==1.5.0"]
"requirements": ["growattServer==1.6.0"]
}

View File

@@ -265,6 +265,11 @@
"version_latest": {
"name": "Newest version"
}
},
"update": {
"update": {
"name": "[%key:component::update::title%]"
}
}
},
"services": {

View File

@@ -39,7 +39,7 @@ from .entity import (
from .update_helper import update_addon, update_core
ENTITY_DESCRIPTION = UpdateEntityDescription(
name="Update",
translation_key="update",
key=ATTR_VERSION_LATEST,
)

View File

@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["pyheos"],
"quality_scale": "platinum",
"requirements": ["pyheos==1.0.4"],
"requirements": ["pyheos==1.0.5"],
"ssdp": [
{
"st": "urn:schemas-denon-com:device:ACT-Denon:1"

View File

@@ -87,6 +87,7 @@ BASE_SUPPORTED_FEATURES = (
PLAY_STATE_TO_STATE = {
None: MediaPlayerState.IDLE,
PlayState.UNKNOWN: MediaPlayerState.IDLE,
PlayState.PLAY: MediaPlayerState.PLAYING,
PlayState.STOP: MediaPlayerState.IDLE,
PlayState.PAUSE: MediaPlayerState.PAUSED,

View File

@@ -73,6 +73,19 @@ class HomeConnectApplianceData:
self.settings.update(other.settings)
self.status.update(other.status)
@classmethod
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
"""Return empty data."""
return cls(
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
class HomeConnectCoordinator(
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
@@ -358,15 +371,7 @@ class HomeConnectCoordinator(
model=appliance.vib,
)
if appliance.ha_id not in self.data:
self.data[appliance.ha_id] = HomeConnectApplianceData(
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
else:
self.data[appliance.ha_id].info.connected = appliance.connected
old_appliances.remove(appliance.ha_id)
@@ -402,6 +407,15 @@ class HomeConnectCoordinator(
name=appliance.name,
model=appliance.vib,
)
if not appliance.connected:
_LOGGER.debug(
"Appliance %s is not connected, skipping data fetch",
appliance.ha_id,
)
if appliance_data_to_update:
appliance_data_to_update.info.connected = False
return appliance_data_to_update
return HomeConnectApplianceData.empty(appliance)
try:
settings = {
setting.key: setting

View File

@@ -33,6 +33,7 @@ from .util import (
OwningIntegration,
get_otbr_addon_manager,
get_zigbee_flasher_addon_manager,
guess_firmware_info,
guess_hardware_owners,
probe_silabs_firmware_info,
)
@@ -511,6 +512,16 @@ 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,8 +95,7 @@ class BaseFirmwareUpdateEntity(
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
# Until this entity can be associated with a device, we must manually name it
_attr_has_entity_name = False
_attr_has_entity_name = True
def __init__(
self,
@@ -195,10 +194,6 @@ 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,19 +3,81 @@
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
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 .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
from .const import (
DESCRIPTION,
DEVICE,
DOMAIN,
FIRMWARE,
FIRMWARE_VERSION,
MANUFACTURER,
PID,
PRODUCT,
SERIAL_NUMBER,
VID,
)
_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
@@ -29,7 +91,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:
@@ -64,6 +126,43 @@ 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 = 3
MINOR_VERSION = 4
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the config flow."""

View File

@@ -5,17 +5,21 @@ from __future__ import annotations
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
from homeassistant.core import HomeAssistant, callback
from .config_flow import HomeAssistantSkyConnectConfigFlow
from .const import DOMAIN
from .util import get_hardware_variant
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
EXPECTED_ENTRY_VERSION = (
HomeAssistantSkyConnectConfigFlow.VERSION,
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
)
@callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info."""
entries = hass.config_entries.async_entries(DOMAIN)
return [
HardwareInfo(
board=None,
@@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
url=DOCUMENTATION_URL,
)
for entry in entries
# Ignore unmigrated config entries in the hardware page
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
]

View File

@@ -195,5 +195,10 @@
"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,7 +168,6 @@ 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": {
"firmware": {
"radio_firmware": {
"name": "Radio firmware"
}
}

View File

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

@@ -659,13 +659,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
# Can be 0 - 2 (Off, Heat, Cool)
# If the HVAC is switched off, it must be idle
# This works around a bug in some devices (like Eve radiator valves) that
# return they are heating when they are not.
target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if target == HeatingCoolingTargetValues.OFF:
return HVACAction.IDLE
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
@@ -679,6 +673,12 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
):
return HVACAction.FAN
# If the HVAC is switched off, it must be idle
# This works around a bug in some devices (like Eve radiator valves) that
# return they are heating when they are not.
if target == HeatingCoolingTargetValues.OFF:
return HVACAction.IDLE
return current_hass_value
@property

View File

@@ -197,5 +197,11 @@
}
}
}
},
"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,6 +29,7 @@ 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
@@ -44,6 +45,9 @@ 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,
@@ -233,6 +237,23 @@ 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

@@ -136,6 +136,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
# Process new device
new_devices = current_devices - self._devices_last_update
if new_devices:
self.data = data
_LOGGER.debug("New devices found: %s", ", ".join(map(str, new_devices)))
self._add_new_devices(new_devices)

View File

@@ -10,8 +10,8 @@
},
"data_description": {
"host": "Hostname or IP-address of the Intergas gateway.",
"username": "The username to log into the gateway. This is `admin` in most cases.",
"password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
"username": "The username to log in to the gateway. This is `admin` in most cases.",
"password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
}
},
"dhcp_auth": {
@@ -22,8 +22,8 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"username": "The username to log into the gateway. This is `admin` in most cases.",
"password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices."
"username": "[%key:component::incomfort::config::step::user::data_description::username%]",
"password": "[%key:component::incomfort::config::step::user::data_description::password%]"
}
},
"dhcp_confirm": {

View File

@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_DOMAIN,
CONF_ENTITIES,
CONF_SOURCE,
@@ -49,6 +50,7 @@ DEVICE_CLASS_MAPPING = {
pypck.lcn_defs.VarUnit.METERPERSECOND: SensorDeviceClass.SPEED,
pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE,
pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT,
pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2,
}
UNIT_OF_MEASUREMENT_MAPPING = {
@@ -60,6 +62,7 @@ UNIT_OF_MEASUREMENT_MAPPING = {
pypck.lcn_defs.VarUnit.METERPERSECOND: UnitOfSpeed.METERS_PER_SECOND,
pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT,
pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE,
pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION,
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.26.1", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.26.5", "ld2410-ble==0.1.1"]
}

View File

@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.26.1", "led-ble==1.1.6"]
"requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.7"]
}

View File

@@ -63,10 +63,12 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Add a callback to handle core config update.
self.unit_system: str | None = None
self.hass.bus.async_listen(
event_type=EVENT_CORE_CONFIG_UPDATE,
listener=self._handle_update_config,
event_filter=self.async_config_update_filter,
self.config_entry.async_on_unload(
self.hass.bus.async_listen(
event_type=EVENT_CORE_CONFIG_UPDATE,
listener=self._handle_update_config,
event_filter=self.async_config_update_filter,
)
)
async def _handle_update_config(self, _: Event) -> None:

View File

@@ -199,7 +199,7 @@ turn_on:
example: "[255, 100, 100]"
selector:
color_rgb:
kelvin: &kelvin
color_temp_kelvin: &color_temp_kelvin
filter: *color_temp_support
selector:
color_temp:
@@ -317,7 +317,7 @@ toggle:
fields:
transition: *transition
rgb_color: *rgb_color
kelvin: *kelvin
color_temp_kelvin: *color_temp_kelvin
brightness_pct: *brightness_pct
effect: *effect
advanced_fields:

View File

@@ -19,8 +19,8 @@
"field_flash_name": "Flash",
"field_hs_color_description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100.",
"field_hs_color_name": "Hue/Sat color",
"field_kelvin_description": "Color temperature in Kelvin.",
"field_kelvin_name": "Color temperature",
"field_color_temp_kelvin_description": "Color temperature in Kelvin.",
"field_color_temp_kelvin_name": "Color temperature",
"field_profile_description": "Name of a light profile to use.",
"field_profile_name": "Profile",
"field_rgb_color_description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue.",
@@ -322,9 +322,9 @@
"name": "[%key:component::light::common::field_color_temp_name%]",
"description": "[%key:component::light::common::field_color_temp_description%]"
},
"kelvin": {
"name": "[%key:component::light::common::field_kelvin_name%]",
"description": "[%key:component::light::common::field_kelvin_description%]"
"color_temp_kelvin": {
"name": "[%key:component::light::common::field_color_temp_kelvin_name%]",
"description": "[%key:component::light::common::field_color_temp_kelvin_description%]"
},
"brightness": {
"name": "[%key:component::light::common::field_brightness_name%]",
@@ -420,9 +420,9 @@
"name": "[%key:component::light::common::field_color_temp_name%]",
"description": "[%key:component::light::common::field_color_temp_description%]"
},
"kelvin": {
"name": "[%key:component::light::common::field_kelvin_name%]",
"description": "[%key:component::light::common::field_kelvin_description%]"
"color_temp_kelvin": {
"name": "[%key:component::light::common::field_color_temp_kelvin_name%]",
"description": "[%key:component::light::common::field_color_temp_kelvin_description%]"
},
"brightness": {
"name": "[%key:component::light::common::field_brightness_name%]",

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/livisi",
"iot_class": "local_polling",
"requirements": ["livisi==0.0.24"]
"requirements": ["livisi==0.0.25"]
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==9.0.3"]
"requirements": ["ical==9.1.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==9.0.3"]
"requirements": ["ical==9.1.0"]
}

View File

@@ -1611,6 +1611,7 @@ def async_is_pem_data(data: bytes) -> bool:
return (
b"-----BEGIN CERTIFICATE-----" in data
or b"-----BEGIN PRIVATE KEY-----" in data
or b"-----BEGIN EC PRIVATE KEY-----" in data
or b"-----BEGIN RSA PRIVATE KEY-----" in data
or b"-----BEGIN ENCRYPTED PRIVATE KEY-----" in data
)

View File

@@ -154,18 +154,14 @@ def get_origin_support_url(discovery_payload: MQTTDiscoveryPayload) -> str | Non
@callback
def async_log_discovery_origin_info(
message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO
message: str, discovery_payload: MQTTDiscoveryPayload
) -> None:
"""Log information about the discovery and origin."""
# We only log origin info once per device discovery
if not _LOGGER.isEnabledFor(level):
# bail out early if logging is disabled
if not _LOGGER.isEnabledFor(logging.DEBUG):
# bail out early if debug logging is disabled
return
_LOGGER.log(
level,
"%s%s",
message,
get_origin_log_string(discovery_payload, include_url=True),
_LOGGER.debug(
"%s%s", message, get_origin_log_string(discovery_payload, include_url=True)
)
@@ -562,7 +558,7 @@ async def async_start( # noqa: C901
elif already_discovered:
# Dispatch update
message = f"Component has already been discovered: {component} {discovery_id}, sending update"
async_log_discovery_origin_info(message, payload, logging.DEBUG)
async_log_discovery_origin_info(message, payload)
async_dispatcher_send(
hass, MQTT_DISCOVERY_UPDATED.format(*discovery_hash), payload
)

View File

@@ -62,6 +62,7 @@ from ..entity import MqttEntity
from ..models import (
MqttCommandTemplate,
MqttValueTemplate,
PayloadSentinel,
PublishPayloadType,
ReceiveMessage,
)
@@ -126,7 +127,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
_command_templates: dict[
str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType]
]
_value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
_value_templates: dict[
str, Callable[[ReceivePayloadType, ReceivePayloadType], ReceivePayloadType]
]
_fixed_color_mode: ColorMode | str | None
_topics: dict[str, str | None]
@@ -203,73 +206,133 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
@callback
def _state_received(self, msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
state = self._value_templates[CONF_STATE_TEMPLATE](msg.payload)
if state == STATE_ON:
state_value = self._value_templates[CONF_STATE_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
if not state_value:
_LOGGER.debug(
"Ignoring message from '%s' with empty state value", msg.topic
)
elif state_value == STATE_ON:
self._attr_is_on = True
elif state == STATE_OFF:
elif state_value == STATE_OFF:
self._attr_is_on = False
elif state == PAYLOAD_NONE:
elif state_value == PAYLOAD_NONE:
self._attr_is_on = None
else:
_LOGGER.warning("Invalid state value received")
_LOGGER.warning(
"Invalid state value '%s' received from %s",
state_value,
msg.topic,
)
if CONF_BRIGHTNESS_TEMPLATE in self._config:
try:
if brightness := int(
self._value_templates[CONF_BRIGHTNESS_TEMPLATE](msg.payload)
):
self._attr_brightness = brightness
else:
_LOGGER.debug(
"Ignoring zero brightness value for entity %s",
self.entity_id,
brightness_value = self._value_templates[CONF_BRIGHTNESS_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
if not brightness_value:
_LOGGER.debug(
"Ignoring message from '%s' with empty brightness value",
msg.topic,
)
else:
try:
if brightness := int(brightness_value):
self._attr_brightness = brightness
else:
_LOGGER.debug(
"Ignoring zero brightness value for entity %s",
self.entity_id,
)
except ValueError:
_LOGGER.warning(
"Invalid brightness value '%s' received from %s",
brightness_value,
msg.topic,
)
except ValueError:
_LOGGER.warning("Invalid brightness value received from %s", msg.topic)
if CONF_COLOR_TEMP_TEMPLATE in self._config:
try:
color_temp = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
msg.payload
color_temp_value = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
if not color_temp_value:
_LOGGER.debug(
"Ignoring message from '%s' with empty color temperature value",
msg.topic,
)
self._attr_color_temp_kelvin = (
int(color_temp)
if self._color_temp_kelvin
else color_util.color_temperature_mired_to_kelvin(int(color_temp))
if color_temp != "None"
else None
)
except ValueError:
_LOGGER.warning("Invalid color temperature value received")
else:
try:
self._attr_color_temp_kelvin = (
int(color_temp_value)
if self._color_temp_kelvin
else color_util.color_temperature_mired_to_kelvin(
int(color_temp_value)
)
if color_temp_value != "None"
else None
)
except ValueError:
_LOGGER.warning(
"Invalid color temperature value '%s' received from %s",
color_temp_value,
msg.topic,
)
if (
CONF_RED_TEMPLATE in self._config
and CONF_GREEN_TEMPLATE in self._config
and CONF_BLUE_TEMPLATE in self._config
):
try:
red = self._value_templates[CONF_RED_TEMPLATE](msg.payload)
green = self._value_templates[CONF_GREEN_TEMPLATE](msg.payload)
blue = self._value_templates[CONF_BLUE_TEMPLATE](msg.payload)
if red == "None" and green == "None" and blue == "None":
self._attr_hs_color = None
else:
self._attr_hs_color = color_util.color_RGB_to_hs(
int(red), int(green), int(blue)
)
red_value = self._value_templates[CONF_RED_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
green_value = self._value_templates[CONF_GREEN_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
blue_value = self._value_templates[CONF_BLUE_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
if not red_value or not green_value or not blue_value:
_LOGGER.debug(
"Ignoring message from '%s' with empty color value", msg.topic
)
elif red_value == "None" and green_value == "None" and blue_value == "None":
self._attr_hs_color = None
self._update_color_mode()
except ValueError:
_LOGGER.warning("Invalid color value received")
else:
try:
self._attr_hs_color = color_util.color_RGB_to_hs(
int(red_value), int(green_value), int(blue_value)
)
self._update_color_mode()
except ValueError:
_LOGGER.warning("Invalid color value received from %s", msg.topic)
if CONF_EFFECT_TEMPLATE in self._config:
effect = str(self._value_templates[CONF_EFFECT_TEMPLATE](msg.payload))
if (
effect_list := self._config[CONF_EFFECT_LIST]
) and effect in effect_list:
self._attr_effect = effect
effect_value = self._value_templates[CONF_EFFECT_TEMPLATE](
msg.payload,
PayloadSentinel.NONE,
)
if not effect_value:
_LOGGER.debug(
"Ignoring message from '%s' with empty effect value", msg.topic
)
elif (effect_list := self._config[CONF_EFFECT_LIST]) and str(
effect_value
) in effect_list:
self._attr_effect = str(effect_value)
else:
_LOGGER.warning("Unsupported effect value received")
_LOGGER.warning(
"Unsupported effect value '%s' received from %s",
effect_value,
msg.topic,
)
@callback
def _prepare_subscribe_topics(self) -> None:

View File

@@ -70,8 +70,8 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
def validate_config(config: ConfigType) -> ConfigType:
"""Validate that the configuration is valid, throws if it isn't."""
if config[CONF_MIN] >= config[CONF_MAX]:
raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'")
if config[CONF_MIN] > config[CONF_MAX]:
raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}")
return config

View File

@@ -26,7 +26,7 @@ from . import subscription
from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import MqttValueTemplate, ReceiveMessage
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic, valid_subscribe_topic
@@ -136,7 +136,18 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
@callback
def _handle_state_message_received(self, msg: ReceiveMessage) -> None:
"""Handle receiving state message via MQTT."""
payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
payload = self._templates[CONF_VALUE_TEMPLATE](
msg.payload, PayloadSentinel.DEFAULT
)
if payload is PayloadSentinel.DEFAULT:
_LOGGER.warning(
"Unable to process payload '%s' for topic %s, with value template '%s'",
msg.payload,
msg.topic,
self._config.get(CONF_VALUE_TEMPLATE),
)
return
if not payload or payload == PAYLOAD_EMPTY_JSON:
_LOGGER.debug(

View File

@@ -151,6 +151,11 @@ async def async_setup_entry(
assert event.object_id is not None
if event.object_id in added_ids:
return
player = mass.players.get(event.object_id)
if TYPE_CHECKING:
assert player is not None
if not player.expose_to_ha:
return
added_ids.add(event.object_id)
async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
@@ -159,6 +164,8 @@ async def async_setup_entry(
mass_players = []
# add all current players
for player in mass.players:
if not player.expose_to_ha:
continue
added_ids.add(player.player_id)
mass_players.append(MusicAssistantPlayer(mass, player.player_id))

View File

@@ -34,7 +34,7 @@ def validate_prices(
index: int,
) -> float | None:
"""Validate and return."""
if result := func(entity)[area][index]:
if (result := func(entity)[area][index]) is not None:
return result / 1000
return None

View File

@@ -31,7 +31,7 @@ from homeassistant.helpers import area_registry as ar
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.translation import async_get_translations
from homeassistant.setup import async_setup_component
from homeassistant.setup import SetupPhases, async_pause_setup, async_setup_component
if TYPE_CHECKING:
from . import OnboardingData, OnboardingStorage, OnboardingStoreData
@@ -60,7 +60,7 @@ async def async_setup(
hass.http.register_view(BackupInfoView(data))
hass.http.register_view(RestoreBackupView(data))
hass.http.register_view(UploadBackupView(data))
setup_cloud_views(hass, data)
await setup_cloud_views(hass, data)
class OnboardingView(HomeAssistantView):
@@ -430,9 +430,19 @@ class UploadBackupView(BackupOnboardingView, backup_http.UploadBackupView):
return await self._post(request)
def setup_cloud_views(hass: HomeAssistant, data: OnboardingStoreData) -> None:
async def setup_cloud_views(hass: HomeAssistant, data: OnboardingStoreData) -> None:
"""Set up the cloud views."""
with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES):
# Import the cloud integration in an executor to avoid blocking the
# event loop.
def import_cloud() -> None:
"""Import the cloud integration."""
# pylint: disable-next=import-outside-toplevel
from homeassistant.components.cloud import http_api # noqa: F401
await hass.async_add_import_executor_job(import_cloud)
# The cloud integration is imported locally to avoid cloud being imported by
# bootstrap.py and to avoid circular imports.

View File

@@ -88,8 +88,8 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]):
),
translation_key=key,
translation_placeholders={
"total": str(drive.quota.total),
"used": str(drive.quota.used),
"total": f"{drive.quota.total / (1024**3):.2f}",
"used": f"{drive.quota.used / (1024**3):.2f}",
},
)
return drive

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.9.0"]
"requirements": ["opower==0.11.1"]
}

View File

@@ -6,5 +6,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/private_ble_device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.26.1"]
"requirements": ["bluetooth-data-tools==1.26.5"]
}

View File

@@ -139,14 +139,13 @@ def query_circular_mean(table: type[StatisticsBase]) -> tuple[Label, Label]:
# in Python.
# https://en.wikipedia.org/wiki/Circular_mean
radians = func.radians(table.mean)
weighted_sum_sin = func.sum(func.sin(radians) * table.mean_weight)
weighted_sum_cos = func.sum(func.cos(radians) * table.mean_weight)
weight = func.sqrt(
func.power(func.sum(func.sin(radians) * table.mean_weight), 2)
+ func.power(func.sum(func.cos(radians) * table.mean_weight), 2)
func.power(weighted_sum_sin, 2) + func.power(weighted_sum_cos, 2)
)
return (
func.degrees(
func.atan2(func.sum(func.sin(radians)), func.sum(func.cos(radians)))
).label("mean"),
func.degrees(func.atan2(weighted_sum_sin, weighted_sum_cos)).label("mean"),
weight.label("mean_weight"),
)
@@ -240,18 +239,20 @@ DEG_TO_RAD = math.pi / 180
RAD_TO_DEG = 180 / math.pi
def weighted_circular_mean(values: Iterable[tuple[float, float]]) -> float:
"""Return the weighted circular mean of the values."""
sin_sum = sum(math.sin(x * DEG_TO_RAD) * weight for x, weight in values)
cos_sum = sum(math.cos(x * DEG_TO_RAD) * weight for x, weight in values)
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
def weighted_circular_mean(
values: Iterable[tuple[float, float]],
) -> tuple[float, float]:
"""Return the weighted circular mean and the weight of the values."""
weighted_sin_sum, weighted_cos_sum = 0.0, 0.0
for x, weight in values:
rad_x = x * DEG_TO_RAD
weighted_sin_sum += math.sin(rad_x) * weight
weighted_cos_sum += math.cos(rad_x) * weight
def circular_mean(values: list[float]) -> float:
"""Return the circular mean of the values."""
sin_sum = sum(math.sin(x * DEG_TO_RAD) for x in values)
cos_sum = sum(math.cos(x * DEG_TO_RAD) for x in values)
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
return (
(RAD_TO_DEG * math.atan2(weighted_sin_sum, weighted_cos_sum)) % 360,
math.sqrt(weighted_sin_sum**2 + weighted_cos_sum**2),
)
_LOGGER = logging.getLogger(__name__)
@@ -300,6 +301,7 @@ class StatisticsRow(BaseStatisticsRow, total=False):
min: float | None
max: float | None
mean: float | None
mean_weight: float | None
change: float | None
@@ -1023,7 +1025,7 @@ def _reduce_statistics(
_want_sum = "sum" in types
for statistic_id, stat_list in stats.items():
max_values: list[float] = []
mean_values: list[float] = []
mean_values: list[tuple[float, float]] = []
min_values: list[float] = []
prev_stat: StatisticsRow = stat_list[0]
fake_entry: StatisticsRow = {"start": stat_list[-1]["start"] + period_seconds}
@@ -1039,12 +1041,15 @@ def _reduce_statistics(
}
if _want_mean:
row["mean"] = None
row["mean_weight"] = None
if mean_values:
match metadata[statistic_id][1]["mean_type"]:
case StatisticMeanType.ARITHMETIC:
row["mean"] = mean(mean_values)
row["mean"] = mean([x[0] for x in mean_values])
case StatisticMeanType.CIRCULAR:
row["mean"] = circular_mean(mean_values)
row["mean"], row["mean_weight"] = (
weighted_circular_mean(mean_values)
)
mean_values.clear()
if _want_min:
row["min"] = min(min_values) if min_values else None
@@ -1063,7 +1068,8 @@ def _reduce_statistics(
max_values.append(_max)
if _want_mean:
if (_mean := statistic.get("mean")) is not None:
mean_values.append(_mean)
_mean_weight = statistic.get("mean_weight") or 0.0
mean_values.append((_mean, _mean_weight))
if _want_min and (_min := statistic.get("min")) is not None:
min_values.append(_min)
prev_stat = statistic
@@ -1385,7 +1391,7 @@ def _get_max_mean_min_statistic(
match metadata[1]["mean_type"]:
case StatisticMeanType.CIRCULAR:
if circular_means := max_mean_min["circular_means"]:
mean_value = weighted_circular_mean(circular_means)
mean_value = weighted_circular_mean(circular_means)[0]
case StatisticMeanType.ARITHMETIC:
if (mean_value := max_mean_min.get("mean_acc")) is not None and (
duration := max_mean_min.get("duration")
@@ -1739,12 +1745,12 @@ def statistic_during_period(
_type_column_mapping = {
"last_reset": "last_reset_ts",
"max": "max",
"mean": "mean",
"min": "min",
"state": "state",
"sum": "sum",
"last_reset": ("last_reset_ts",),
"max": ("max",),
"mean": ("mean", "mean_weight"),
"min": ("min",),
"state": ("state",),
"sum": ("sum",),
}
@@ -1756,12 +1762,13 @@ def _generate_select_columns_for_types_stmt(
track_on: list[str | None] = [
table.__tablename__, # type: ignore[attr-defined]
]
for key, column in _type_column_mapping.items():
if key in types:
columns = columns.add_columns(getattr(table, column))
track_on.append(column)
else:
track_on.append(None)
for key, type_columns in _type_column_mapping.items():
for column in type_columns:
if key in types:
columns = columns.add_columns(getattr(table, column))
track_on.append(column)
else:
track_on.append(None)
return lambda_stmt(lambda: columns, track_on=track_on)
@@ -1944,6 +1951,12 @@ def _statistics_during_period_with_session(
hass, session, start_time, units, _types, table, metadata, result
)
# filter out mean_weight as it is only needed to reduce statistics
# and not needed in the result
for stats_rows in result.values():
for row in stats_rows:
row.pop("mean_weight", None)
# Return statistics combined with metadata
return result
@@ -2391,7 +2404,12 @@ def _sorted_statistics_to_dict(
field_map["last_reset"] = field_map.pop("last_reset_ts")
sum_idx = field_map["sum"] if "sum" in types else None
sum_only = len(types) == 1 and sum_idx is not None
row_mapping = tuple((key, field_map[key]) for key in types if key in field_map)
row_mapping = tuple(
(column, field_map[column])
for key in types
for column in ({key, *_type_column_mapping.get(key, ())})
if column in field_map
)
# Append all statistic entries, and optionally do unit conversion
table_duration_seconds = table.duration.total_seconds()
for meta_id, db_rows in stats_by_meta_id.items():

View File

@@ -69,7 +69,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
)
except CalendarParseError as err:
errors["base"] = "invalid_ics_file"
_LOGGER.debug("Invalid .ics file: %s", err)
_LOGGER.error("Error reading the calendar information: %s", err.message)
_LOGGER.debug(
"Additional calendar error detail: %s", str(err.detailed_error)
)
else:
return self.async_create_entry(
title=user_input[CONF_CALENDAR_NAME], data=user_input

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==9.0.3"]
"requirements": ["ical==9.1.0"]
}

View File

@@ -20,7 +20,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"forbidden": "The server understood the request but refuses to authorize it.",
"invalid_ics_file": "[%key:component::local_calendar::config::error::invalid_ics_file%]"
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
}
},
"exceptions": {

View File

@@ -35,7 +35,7 @@
},
"sensor": {
"charge_state": {
"default": "mdi:mdi:flash-off",
"default": "mdi:flash-off",
"state": {
"charge_in_progress": "mdi:flash"
}

View File

@@ -420,6 +420,14 @@ def migrate_entity_ids(
if entity.device_id in ch_device_ids:
ch = ch_device_ids[entity.device_id]
id_parts = entity.unique_id.split("_", 2)
if len(id_parts) < 3:
_LOGGER.warning(
"Reolink channel %s entity has unexpected unique_id format %s, with device id %s",
ch,
entity.unique_id,
entity.device_id,
)
continue
if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch):
new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}"
existing_entity = entity_reg.async_get_entity_id(

View File

@@ -301,7 +301,7 @@ async def async_setup_entry(
)
for entity_description in BINARY_SMART_AI_SENSORS
for location in api.baichuan.smart_location_list(
channel, entity_description.key
channel, entity_description.smart_type
)
if entity_description.supported(api, channel, location)
)

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.0"]
"requirements": ["reolink-aio==0.13.1"]
}

View File

@@ -153,6 +153,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
ImageConfig(scale=MAP_SCALE),
[],
)
self.last_update_state: str | None = None
@cached_property
def dock_device_info(self) -> DeviceInfo:
@@ -225,7 +226,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
"""Update the currently selected map."""
# The current map was set in the props update, so these can be done without
# worry of applying them to the wrong map.
if self.current_map is None:
if self.current_map is None or self.current_map not in self.maps:
# This exists as a safeguard/ to keep mypy happy.
return
try:
@@ -291,7 +292,6 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
async def _async_update_data(self) -> DeviceProp:
"""Update data via library."""
previous_state = self.roborock_device_info.props.status.state_name
try:
# Update device props and standard api information
await self._update_device_prop()
@@ -302,13 +302,17 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
# If the vacuum is currently cleaning and it has been IMAGE_CACHE_INTERVAL
# since the last map update, you can update the map.
new_status = self.roborock_device_info.props.status
if self.current_map is not None and (
(
new_status.in_cleaning
and (dt_util.utcnow() - self.maps[self.current_map].last_updated)
> IMAGE_CACHE_INTERVAL
if (
self.current_map is not None
and (current_map := self.maps.get(self.current_map))
and (
(
new_status.in_cleaning
and (dt_util.utcnow() - current_map.last_updated)
> IMAGE_CACHE_INTERVAL
)
or self.last_update_state != new_status.state_name
)
or previous_state != new_status.state_name
):
try:
await self.update_map()
@@ -330,6 +334,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
self.update_interval = V1_CLOUD_NOT_CLEANING_INTERVAL
else:
self.update_interval = V1_LOCAL_NOT_CLEANING_INTERVAL
self.last_update_state = self.roborock_device_info.props.status.state_name
return self.roborock_device_info.props
def _set_current_map(self) -> None:

View File

@@ -381,7 +381,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity):
@property
def options(self) -> list[str]:
"""Return the currently valid rooms."""
if self.coordinator.current_map is not None:
if (
self.coordinator.current_map is not None
and self.coordinator.current_map in self.coordinator.maps
):
return list(
self.coordinator.maps[self.coordinator.current_map].rooms.values()
)
@@ -390,7 +393,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity):
@property
def native_value(self) -> str | None:
"""Return the value reported by the sensor."""
if self.coordinator.current_map is not None:
if (
self.coordinator.current_map is not None
and self.coordinator.current_map in self.coordinator.maps
):
return self.coordinator.maps[self.coordinator.current_map].current_room
return None

View File

@@ -15,6 +15,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import trigger
from .const import DOMAIN
from .helpers import (
async_get_client_by_device_entry,
async_get_device_entry_by_device_id,
@@ -75,4 +76,8 @@ async def async_attach_trigger(
hass, trigger_config, action, trigger_info
)
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unhandled_trigger_type",
translation_placeholders={"trigger_type": trigger_type},
)

View File

@@ -106,5 +106,7 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity)
self.entity_id,
)
raise HomeAssistantError(
f"Entity {self.entity_id} does not support this service."
translation_domain=DOMAIN,
translation_key="service_unsupported",
translation_placeholders={"entity": self.entity_id},
)

View File

@@ -39,7 +39,7 @@
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==2.1.0",
"async-upnp-client==0.43.0"
"async-upnp-client==0.44.0"
],
"ssdp": [
{

View File

@@ -47,5 +47,13 @@
"trigger_type": {
"samsungtv.turn_on": "Device is requested to turn on"
}
},
"exceptions": {
"unhandled_trigger_type": {
"message": "Unhandled trigger type {trigger_type}."
},
"service_unsupported": {
"message": "Entity {entity} does not support this action."
}
}
}

View File

@@ -160,7 +160,7 @@ def _time_weighted_arithmetic_mean(
def _time_weighted_circular_mean(
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
) -> float:
) -> tuple[float, float]:
"""Calculate a time weighted circular mean.
The circular mean is calculated by weighting the states by duration in seconds between
@@ -623,7 +623,7 @@ def compile_statistics( # noqa: C901
valid_float_states, start, end
)
case StatisticMeanType.CIRCULAR:
stat["mean"] = _time_weighted_circular_mean(
stat["mean"], stat["mean_weight"] = _time_weighted_circular_mean(
valid_float_states, start, end
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
"iot_class": "cloud_polling",
"loggers": ["sharkiq"],
"requirements": ["sharkiq==1.0.2"]
"requirements": ["sharkiq==1.1.0"]
}

View File

@@ -12,6 +12,7 @@ from aioshelly.exceptions import (
CustomPortNotSupported,
DeviceConnectionError,
InvalidAuthError,
InvalidHostError,
MacAddressMismatchError,
)
from aioshelly.rpc_device import RpcDevice
@@ -157,6 +158,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
self.info = await self._async_get_info(host, port)
except DeviceConnectionError:
errors["base"] = "cannot_connect"
except InvalidHostError:
errors["base"] = "invalid_host"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

View File

@@ -277,3 +277,7 @@ ROLE_TO_DEVICE_CLASS_MAP = {
"current_humidity": SensorDeviceClass.HUMIDITY,
"current_temperature": SensorDeviceClass.TEMPERATURE,
}
# We want to check only the first 5 KB of the script if it contains emitEvent()
# so that the integration startup remains fast.
MAX_SCRIPT_SIZE = 5120

View File

@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioshelly"],
"requirements": ["aioshelly==13.4.0"],
"requirements": ["aioshelly==13.4.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -51,6 +51,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support",
"custom_port_not_supported": "Gen1 device does not support custom port.",

View File

@@ -58,6 +58,7 @@ from .const import (
GEN2_BETA_RELEASE_URL,
GEN2_RELEASE_URL,
LOGGER,
MAX_SCRIPT_SIZE,
RPC_INPUTS_EVENTS_TYPES,
SHAIR_MAX_WORK_HOURS,
SHBTN_INPUTS_EVENTS_TYPES,
@@ -642,7 +643,7 @@ def get_rpc_ws_url(hass: HomeAssistant) -> str | None:
async def get_rpc_script_event_types(device: RpcDevice, id: int) -> list[str]:
"""Return a list of event types for a specific script."""
code_response = await device.script_getcode(id)
code_response = await device.script_getcode(id, bytes_to_read=MAX_SCRIPT_SIZE)
matches = SHELLY_EMIT_EVENT_PATTERN.finditer(code_response["data"])
return sorted([*{str(event_type.group(1)) for event_type in matches}])

View File

@@ -7,26 +7,21 @@ from dataclasses import dataclass
from pysmartthings import Attribute, Capability, Category, SmartThings, Status
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import FullDevice, SmartThingsConfigEntry
from .const import DOMAIN, MAIN
from .const import INVALID_SWITCH_CATEGORIES, MAIN
from .entity import SmartThingsEntity
from .util import deprecate_entity
@dataclass(frozen=True, kw_only=True)
@@ -132,14 +127,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.SWITCH,
device_class=BinarySensorDeviceClass.POWER,
is_on_key="on",
category={
Category.CLOTHING_CARE_MACHINE,
Category.COOKTOP,
Category.DISHWASHER,
Category.DRYER,
Category.MICROWAVE,
Category.WASHER,
},
category=INVALID_SWITCH_CATEGORIES,
)
},
Capability.TAMPER_ALERT: {
@@ -192,24 +180,64 @@ async def async_setup_entry(
) -> None:
"""Add binary sensors for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsBinarySensor(
entry_data.client, device, description, capability, attribute, component
)
for device in entry_data.devices.values()
for capability, attribute_map in CAPABILITY_TO_SENSORS.items()
for attribute, description in attribute_map.items()
for component in device.status
if capability in device.status[component]
and (
component == MAIN
or (description.exists_fn is not None and description.exists_fn(component))
)
and (
not description.category
or get_main_component_category(device) in description.category
)
)
entities = []
entity_registry = er.async_get(hass)
for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks
for capability, attribute_map in CAPABILITY_TO_SENSORS.items():
for attribute, description in attribute_map.items():
for component in device.status:
if (
capability in device.status[component]
and (
component == MAIN
or (
description.exists_fn is not None
and description.exists_fn(component)
)
)
and (
not description.category
or get_main_component_category(device)
in description.category
)
):
if (
component == MAIN
and (issue := description.deprecated_fn(device.status))
is not None
):
if deprecate_entity(
hass,
entity_registry,
BINARY_SENSOR_DOMAIN,
f"{device.device.device_id}_{component}_{capability}_{attribute}_{attribute}",
f"deprecated_binary_{issue}",
):
entities.append(
SmartThingsBinarySensor(
entry_data.client,
device,
description,
capability,
attribute,
component,
)
)
continue
entities.append(
SmartThingsBinarySensor(
entry_data.client,
device,
description,
capability,
attribute,
component,
)
)
async_add_entities(entities)
class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity):
@@ -257,57 +285,3 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity):
self.get_attribute_value(self.capability, self._attribute)
== self.entity_description.is_on_key
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
if (issue := self.entity_description.deprecated_fn(self.device.status)) is None:
return
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
if not items:
return
entity_reg: er.EntityRegistry = er.async_get(self.hass)
entity_automations = [
automation_entity
for automation_id in automations
if (automation_entity := entity_reg.async_get(automation_id))
]
entity_scripts = [
script_entity
for script_id in scripts
if (script_entity := entity_reg.async_get(script_id))
]
items_list = [
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
for item in entity_automations
] + [
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
for item in entity_scripts
]
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_binary_{issue}_{self.entity_id}",
breaks_in_ha_version="2025.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=f"deprecated_binary_{issue}",
translation_placeholders={
"entity": self.entity_id,
"items": "\n".join(items_list),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if (issue := self.entity_description.deprecated_fn(self.device.status)) is None:
return
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_{issue}_{self.entity_id}"
)

View File

@@ -333,7 +333,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
"""Define a SmartThings Air Conditioner."""
_attr_name = None
_attr_preset_mode = None
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Init the class."""
@@ -545,6 +544,18 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
SWING_OFF,
)
@property
def preset_mode(self) -> str | None:
"""Return the preset mode."""
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):
mode = self.get_attribute_value(
Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE,
Attribute.AC_OPTIONAL_MODE,
)
if mode == WINDFREE:
return WINDFREE
return None
def _determine_preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes."""
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):

View File

@@ -1,6 +1,6 @@
"""Constants used by the SmartThings component and platforms."""
from pysmartthings import Attribute, Capability
from pysmartthings import Attribute, Capability, Category
DOMAIN = "smartthings"
@@ -109,3 +109,12 @@ SENSOR_ATTRIBUTES_TO_CAPABILITIES: dict[str, str] = {
Attribute.WASHER_MODE: Capability.WASHER_MODE,
Attribute.WASHER_JOB_STATE: Capability.WASHER_OPERATING_STATE,
}
INVALID_SWITCH_CATEGORIES = {
Category.CLOTHING_CARE_MACHINE,
Category.COOKTOP,
Category.DRYER,
Category.WASHER,
Category.MICROWAVE,
Category.DISHWASHER,
}

View File

@@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.0.1"]
"requirements": ["pysmartthings==3.0.4"]
}

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