Compare commits

...

197 Commits

Author SHA1 Message Date
Franck Nijhof dc4627f413
Bump version to 2025.6.0b9 2025-06-11 18:07:37 +00:00
Joost Lekkerkerker 02524b8b9b
Make issue creation check architecture instead of uname (#146537) 2025-06-11 18:06:36 +00:00
andreimoraru 60b8230ecc
Bump yt-dlp to 2025.06.09 (#146553)
* Bumped yt-dlp to 2025.06.09

* fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-11 18:00:53 +00:00
Paul Bottein 75e6f23a82
Update frontend to 20250531.2 (#146551) 2025-06-11 18:00:52 +00:00
Petar Petrov 1f221712a2
Remove the Delete button on the ZwaveJS device page (#146544) 2025-06-11 18:00:51 +00:00
Paul Bottein 43797c03cc
Update frontend to 20250531.1 (#146542) 2025-06-11 18:00:49 +00:00
Erik Montnemery 89637a618e
Handle changes to source entities in generic_thermostat helper (#146541) 2025-06-11 18:00:48 +00:00
Erik Montnemery fd605e0abe
Handle changes to source entities in generic_hygrostat helper (#146538) 2025-06-11 17:58:52 +00:00
Franck Nijhof e73bcc73b5
Bump version to 2025.6.0b8 2025-06-11 17:26:20 +00:00
Erik Montnemery c02707a90f
Handle changes to source entity in statistics helper (#146523) 2025-06-11 17:25:57 +00:00
Erik Montnemery 232f853d68
Simplify helper_integration.async_handle_source_entity_changes (#146516) 2025-06-11 17:22:28 +00:00
Tsvi Mostovicz 91e296a0c8
Bump hdate to 1.1.1 (#146536) 2025-06-11 16:58:43 +00:00
Simon Lamon bcedb06862
Bump linkplay to v0.2.11 (#146530) 2025-06-11 16:56:36 +00:00
Erik Montnemery 2ab32220ed
Handle changes to source entity in utility_meter (#146526) 2025-06-11 16:56:35 +00:00
Erik Montnemery 273ccb3929
Handle changes to source entity in trend helper (#146525) 2025-06-11 16:56:33 +00:00
Erik Montnemery caaa4d5f35
Handle changes to source entity in threshold helper (#146524) 2025-06-11 16:56:32 +00:00
Erik Montnemery 0cf1fd1d41
Handle changes to source entity in integration helper (#146522) 2025-06-11 16:54:26 +00:00
Erik Montnemery 5ee39df330
Handle changes to source entity in history_stats helper (#146521) 2025-06-11 16:54:25 +00:00
Martin Hjelmare cc972d20f6
Remove Z-Wave useless reconfigure options (#146520)
* Remove emulate hardware option

* Remove log level option
2025-06-11 16:54:24 +00:00
Erik Montnemery e0f32cfd54
Allow removing entity registry items twice (#146519) 2025-06-11 16:54:22 +00:00
Jesse Hills 6384c800c3
Fix solax state class of `Today's Generated Energy` (#146492) 2025-06-11 16:50:15 +00:00
tronikos 82de2ed8e1
Rename Amazon Devices to Alexa Devices (#146362)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-11 16:50:14 +00:00
Aidan Timson af72d1854f
Add guide for Honeywell Lyric application credentials setup (#146281)
* Add guide for Honeywell Lyric application credentials setup

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-11 16:50:13 +00:00
Shay Levy 0cff7cbccd
Remove stale Shelly BLU TRV devices (#145994)
* Remove stale Shelly BLU TRV devices

* Add test

* Remove config entry from device
2025-06-11 16:50:11 +00:00
Kevin Stillhammer 6f4e16eed1
Fix stale options in here_travel_time (#145911) 2025-06-11 16:50:10 +00:00
Petro31 66be2f9240
Fix `delay_on` and `delay_off` restarting when a new trigger occurs during the delay (#145050) 2025-06-11 16:50:09 +00:00
Franck Nijhof b6c8718ae4
Bump version to 2025.6.0b7 2025-06-11 10:17:18 +00:00
Åke Strandberg c8b70cc0fb
Graceful handling of missing datapoint in myuplink (#146517) 2025-06-11 10:17:09 +00:00
Robert Resch 6d1f621e55
Bump deebot-client to 13.3.0 (#146507) 2025-06-11 10:17:08 +00:00
Erik Montnemery 671a33b31c
Do not remove derivative config entry when input sensor is removed (#146506)
* Do not remove derivative config entry when input sensor is removed

* Add comments

* Update homeassistant/helpers/helper_integration.py

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

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-11 10:17:06 +00:00
Michael Hansen 7afc469306
Bump intents to 2025.6.10 (#146491) 2025-06-11 10:17:05 +00:00
Felix Schneider 8fd52248b7
Bump `apsystems` to `2.7.0` (#146485) 2025-06-11 10:17:04 +00:00
Joost Lekkerkerker 69ba2aab11
Remove DHCP discovery from Amazon Devices (#146476) 2025-06-11 10:17:02 +00:00
Tsvi Mostovicz f1df6dcda5
Fix Jewish calendar not updating (#146465) 2025-06-11 10:17:01 +00:00
Joost Lekkerkerker 43e16bb913
Split deprecated system issue in 2 places (#146453) 2025-06-11 10:15:11 +00:00
Petro31 4147211f94
Add color_temp_kelvin to set_temperature action variables (#146448) 2025-06-11 10:10:40 +00:00
Joost Lekkerkerker 63e49c5d3c
Explain Nest setup (#146217) 2025-06-11 10:10:39 +00:00
hahn-th 35580c0849
Bump homematicip to 2.0.4 (#144096)
* Bump to 2.0.2 with all necessary changes

* bump to prerelease

* add addiional tests

* Bump to homematicip 2.0.3

* do not delete device

* Setup BRAND_SWITCH_MEASURING as light

* bump to 2.0.4

* refactor test_remove_obsolete_entities

* move test

* use const from homematicip lib
2025-06-11 10:10:38 +00:00
Franck Nijhof 8949a595fe
Bump version to 2025.6.0b6 2025-06-10 17:45:26 +00:00
Simone Chemelli bf8ef0a767
Fix EntityCategory for binary_sensor platform in Amazon Devices (#146472)
* Fix EntityCategory for  binary_sensor platform in Amazon Devices

* update snapshots
2025-06-10 17:41:02 +00:00
Simone Chemelli 39962a3f48
Avoid closing shared aiohttp session in Vodafone Station (#146471) 2025-06-10 17:41:00 +00:00
G Johansson 4964621014
Fix incorrect categories handling in holiday (#146470) 2025-06-10 17:40:59 +00:00
Joost Lekkerkerker 18e1a26da1
Catch exception before retrying in AirGradient (#146460) 2025-06-10 17:40:58 +00:00
Joost Lekkerkerker 1d91ca5716
Bump pySmartThings to 3.2.4 (#146459) 2025-06-10 17:40:57 +00:00
Luca Schröder 1040646610
Update caldav to 1.6.0 (#146456)
Fixes #140798
2025-06-10 17:40:56 +00:00
Robert Resch fcd71931e7
Update wording deprecated system package integration repair (#146450)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-10 17:40:55 +00:00
Joost Lekkerkerker bdbb74aff1
Return expected state in SmartThings water heater (#146449) 2025-06-10 17:40:53 +00:00
Marc Mueller 6f4029983a
Update requests to 2.32.4 (#146445) 2025-06-10 17:40:52 +00:00
J. Diego Rodríguez Royo b2d25b1883
Improvements for Home Connect application credentials string (#146443) 2025-06-10 17:40:51 +00:00
J. Diego Rodríguez Royo ba19d4f043
Fix typo at application credentials string at Home Connect integration (#146442)
Fix typos
2025-06-10 17:40:50 +00:00
tronikos b222fe5afa
Handle grpc errors in Google Assistant SDK (#146438) 2025-06-10 17:40:49 +00:00
Franck Nijhof f945defa2b
Reformat Dockerfile to reduce merge conflicts (#146435) 2025-06-10 17:38:37 +00:00
J. Nick Koston 4f0e4bc1ca
Bump aiohttp to 3.12.12 (#146426) 2025-06-10 17:28:13 +00:00
J. Nick Koston 41abc8404d
Bump yarl to 1.20.1 (#146424) 2025-06-10 17:26:03 +00:00
Jamin 2b08c4c344
Check hangup error in voip (#146423)
Check hangup error

Prevent an error where the call end future may have already been set
when a hangup is detected.
2025-06-10 17:26:02 +00:00
J. Nick Koston 97d91ddddb
Bump propcache to 0.3.2 (#146418) 2025-06-10 17:26:01 +00:00
Whitney Young ec30b12fd1
Fix initial state of UV protection window (#146408)
The `binary_sensor` is created when the config entry is loaded after the
`async_config_entry_first_refresh` has completed (during the forward of
setup to platforms). Therefore, the update coordinator will already have
data and will not trigger the invocation of
`_handle_coordinator_update`.

Fixing this just means performing the same update at initialization.
2025-06-10 17:25:59 +00:00
Erik Montnemery 9997fc11b1
Handle changes to source entity in derivative helper (#146407)
* Handle changes to source entity in derivative helper

* Rename helper function, improve docstring

* Add tests

* Improve derivative tests

* Deduplicate tests

* Rename helpers/helper_entity.py to helpers/helper_integration.py

* Rename tests
2025-06-10 17:25:59 +00:00
wittypluck c6ff0e6492
Fix CO concentration unit in OpenWeatherMap (#146403) 2025-06-10 17:25:58 +00:00
G Johansson a3220ecae6
Bump pynordpool to 0.3.0 (#146396) 2025-06-10 17:25:57 +00:00
Erik Montnemery 218864d08c
Update switch_as_x to handle wrapped switch moved to another device (#146387)
* Update switch_as_x to handle wrapped switch moved to another device

* Reload switch_as_x config entry after updating device

* Make sure the switch_as_x entity is not removed
2025-06-10 17:25:56 +00:00
Erik Montnemery 3d0d70ece6
Fix switch_as_x entity_id tracking (#146386) 2025-06-10 17:25:55 +00:00
Simone Chemelli f629731930
Bump aioamazondevices to 3.0.6 (#146385) 2025-06-10 17:25:53 +00:00
J. Nick Koston e7a7b2417b
Bump aioesphomeapi to 32.2.1 (#146375) 2025-06-10 17:03:20 +00:00
Michael Davie 0b24a9abc3
Bump env-canada to v0.11.2 (#146371) 2025-06-10 17:03:19 +00:00
David Knowles ca77b5210f
Bump pydrawise to 2025.6.0 (#146369) 2025-06-10 17:03:18 +00:00
Simon Lamon 0874f1c350
Bump python-linkplay to v0.2.10 (#146359) 2025-06-10 17:03:17 +00:00
Jan-Philipp Benecke d89b99f42b
Improve error logging in trend binary sensor (#146358) 2025-06-10 17:03:16 +00:00
J. Diego Rodríguez Royo 7bd6ec68a8
Explain Home Connect setup (#146356)
* Explain Home Connect setup

* Avoid using "we"

* Fix login spelling

* Fix signup spelling
2025-06-10 17:03:15 +00:00
J. Nick Koston bfe2eeb833
Shift ESPHome log parsing to the library (#146349) 2025-06-10 17:03:14 +00:00
Klaas Schoute e97ab1fe3c
Change interval for Powerfox integration (#146348) 2025-06-10 17:03:13 +00:00
J. Nick Koston b3ee2a8885
Bump aioesphomeapi to 32.2.0 (#146344) 2025-06-10 17:03:12 +00:00
Michael 80b09e3212
Bump py-synologydsm-api to 2.7.3 (#146338)
bump py-synologydsm-api to 2.7.3
2025-06-10 17:03:11 +00:00
tronikos 0eb3714abc
Allow different manufacturer than Amazon in Amazon Devices (#146333) 2025-06-10 17:03:10 +00:00
Sanjay Govind 7991977443
Fix bosch alarm areas not correctly subscribing to alarms (#146322)
* Fix bosch alarm areas not correctly subscribing to alarms

* add test
2025-06-10 17:03:09 +00:00
J. Nick Koston 5e5431c9f9
Use entity unique id for ESPHome media player formats (#146318) 2025-06-10 17:03:08 +00:00
Simon Lamon 1fc05d1a30
Do not probe linkplay device if another config entry already contains the host (#146305)
* Do not probe if config entry already contains the host

* Add unit test

* Use common fixture
2025-06-10 17:03:07 +00:00
J. Nick Koston 21833e7c31
Bump aiohttp to 3.12.11 (#146298) 2025-06-10 16:59:07 +00:00
G Johansson 79daeb23a9
Bump holidays to 0.74 (#146290) 2025-06-10 16:55:33 +00:00
J. Nick Koston 761c2578fb
Bump aiohttp-fast-zlib to 0.3.0 (#146285)
changelog: https://github.com/Bluetooth-Devices/aiohttp-fast-zlib/compare/v0.2.3...v0.3.0

proper aiohttp 3.12 support
2025-06-10 16:48:33 +00:00
Brett Adams 4d3145e559
Add missing write state to Teslemetry (#146267) 2025-06-10 16:47:33 +00:00
Michael 91e29a3bf1
Bump aioimmich to 0.9.1 (#146222)
bump aioimmich to 0.9.1
2025-06-10 16:47:32 +00:00
Joost Lekkerkerker f6a4486c65
Explain Withings setup (#146216) 2025-06-10 16:47:31 +00:00
Joost Lekkerkerker fc8b512931
Remove zeroconf discovery from Spotify (#146213) 2025-06-10 16:47:30 +00:00
Brett Adams e5dd15da82
Fix Export Rule Select Entity in Tessie (#146203)
Fix TessieExportRuleSelectEntity
2025-06-10 16:47:29 +00:00
Brett Adams e4140d71ab
Prevent energy history returning zero in Teslemetry (#146202) 2025-06-10 16:47:28 +00:00
J. Nick Koston 8312780c47
Bump aiohttp to 3.12.9 (#146178) 2025-06-10 16:44:14 +00:00
Raphael Hehl 5accc3dec2
Bump uiprotect to 7.11.0 (#146171)
Bump uiprotect to version 7.11.0
2025-06-10 16:42:40 +00:00
Iskra kranj d875989866
Bump pyiskra to 0.1.21 (#146156) 2025-06-10 16:42:39 +00:00
Michael 38c92a2338
Bump aioimmich to 0.9.0 (#146154)
bump aioimmich to 0.9.0
2025-06-10 16:42:38 +00:00
J. Nick Koston ce76b5db16
Bump aiohttp to 3.12.8 (#146153) 2025-06-10 16:39:58 +00:00
Ian dfc4889d45
Throttle Nextbus if we are reaching the rate limit (#146064)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-06-10 16:32:59 +00:00
Andrea Turri 41431282ee
Add evaporate water program id for Miele oven (#145996) 2025-06-10 16:32:58 +00:00
Arie Catsman 5821b2f03c
fix possible mac collision in enphase_envoy (#145549)
* fix possible mac collision in enphase_envoy

* remove redundant device registry async_get
2025-06-10 16:32:57 +00:00
starkillerOG 78d2bf736c
Reolink conserve battery (#145452) 2025-06-10 16:32:56 +00:00
Franck Nijhof 6c098c3e0a
Bump version to 2025.6.0b5 2025-06-04 09:02:53 +00:00
J. Nick Koston bfb140d2e9
Bump aioesphomeapi to 32.0.0 (#146135) 2025-06-04 09:00:59 +00:00
J. Nick Koston f71a1a7a89
Bump protobuf to 6.31.1 (#146128)
changelog: https://github.com/protocolbuffers/protobuf/compare/v30.2...v31.1
2025-06-04 09:00:57 +00:00
Erwin Douna e8aab39620
SMA fix strings (#146112)
* Fix

* Feedback
2025-06-04 09:00:55 +00:00
J. Nick Koston 1d578d8563
Bump habluetooth to 3.49.0 (#146111)
* Bump habluetooth to 3.49.0

changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.48.2...v3.49.0

* update diag

* diag
2025-06-04 09:00:53 +00:00
J. Nick Koston abfd443541
Bump bleak-esphome to 2.16.0 (#146110) 2025-06-04 09:00:51 +00:00
Brett Adams 81cbb6e5cf
Fix BMS and Charge states in Teslemetry (#146091)
Fix BMS and Charge states
2025-06-04 09:00:49 +00:00
Retha Runolfsson 010c5cab87
Fix nightlatch option for all switchbot locks (#146090) 2025-06-04 09:00:47 +00:00
SNoof85 415858119a
Add state class measurement to Freebox temperature sensors (#146074) 2025-06-04 09:00:44 +00:00
Simone Chemelli 1838a731d6
Bump aioamazondevices to 3.0.5 (#146073) 2025-06-04 09:00:42 +00:00
Shay Levy 1e304fad65
Fix Shelly BLU TRV calibrate button (#146066) 2025-06-04 09:00:40 +00:00
Michael 999c9b3dc5
Don't use multi-line conditionals in immich (#146062) 2025-06-04 09:00:37 +00:00
epenet e15edbd54b
Adjust SamsungTV on/off logging (#146045)
* Adjust SamsungTV on/off logging

* Update coordinator.py
2025-06-04 09:00:35 +00:00
epenet e5cb77d168
Adjust ConnectionFailure logging in SamsungTV (#146044) 2025-06-04 09:00:32 +00:00
starkillerOG cf521d4c7c
Improve debug logging Reolink (#146033)
Add debug logging
2025-06-04 09:00:25 +00:00
J. Nick Koston 6f09474193
Bump grpcio to 1.72.1 (#146029) 2025-06-04 09:00:21 +00:00
Robert Resch 7626933352
Bump go2rtc-client to 0.2.1 (#146019)
* Bump go2rtc-client to 0.2.0

* Bump go2rtc-client to 0.2.1

* Clean up hassfest exception

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-04 08:56:07 +00:00
starkillerOG 9e1d8c2fc6
Bump reolink-aio to 0.13.5 (#145974)
* Add debug logging

* Bump reolink-aio to 0.13.5

* Revert "Add debug logging"

This reverts commit f96030a6c8.
2025-06-04 08:43:30 +00:00
Simone Chemelli 6defed2915
Bump aioamazondevices to 3.0.4 (#145971) 2025-06-04 08:43:28 +00:00
Simone Chemelli d729eed7c2
Add diagnostics to Amazon devices (#145964) 2025-06-04 08:43:26 +00:00
Noah Groß f280032dcf
Bump python-picnic-api2 to 1.3.1 (#145962) 2025-06-04 08:43:21 +00:00
Allen Porter 7e85137012
Bump ical to 10.0.0 (#145954) 2025-06-04 08:43:18 +00:00
TimL 88f2c3abd3
Bump pysmlight to v0.2.5 (#145949) 2025-06-04 08:43:15 +00:00
Michael 1a21e01f85
Bump aioimmich to 0.8.0 (#145908) 2025-06-04 08:43:13 +00:00
Ian d302e817c8
NextBus: Bump py_nextbusnext to 2.2.0 (#145904) 2025-06-04 08:43:09 +00:00
Martin Hjelmare 1e1b0424d7
Fix removal of devices during Z-Wave migration (#145867) 2025-06-04 08:43:07 +00:00
Robert Resch 03f028b7e2
Deprecate hddtemp (#145850) 2025-06-04 08:43:05 +00:00
Robert Resch b1d35de8e4
Deprecate eddystone temperature integration (#145833) 2025-06-04 08:43:02 +00:00
Erwin Douna ea6b9e5260
SMA add missing strings for DHCP (#145782) 2025-06-04 08:42:59 +00:00
Bram Kragten 06d869aaa5 Bump version to 2025.6.0b4 2025-05-31 21:25:06 +02:00
Josef Zweck 907cebdd6d Increase update intervals in lamarzocco (#145939) 2025-05-31 21:25:02 +02:00
Josef Zweck 745902bc7e Bump pylamarzocco to 2.0.8 (#145938) 2025-05-31 21:25:01 +02:00
Bram Kragten ef0b3c9f9c Update frontend to 20250531.0 (#145933) 2025-05-31 21:25:00 +02:00
J. Nick Koston 532c077ddf Bump aiohttp to 3.12.6 (#145919)
* Bump aiohttp to 3.12.5

changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.4...v3.12.5

* .6

* fix mock
2025-05-31 21:24:59 +02:00
tronikos cd905a6593 Bump opower to 0.12.3 (#145918) 2025-05-31 21:24:59 +02:00
Josef Zweck d0bf9d9bfb Move server device creation to init in jellyfin (#145910)
* Move server device creation to init in jellyfin

* move device creation to after coordinator refresh
2025-05-31 21:24:58 +02:00
Jordan Harvey ddc79a631d Bump pyprobeplus to 1.0.1 (#145897) 2025-05-31 21:24:57 +02:00
Simon Lamon 6015f60db4 Bump python-linkplay to v0.2.9 (#145892) 2025-05-31 21:24:57 +02:00
Iskra kranj a6608bd7ea Bump pyiskra to 0.1.19 (#145889) 2025-05-31 21:24:56 +02:00
Brett Adams fb2d8c6406 Add streaming to charge cable connected in Teslemetry (#145880) 2025-05-31 21:24:55 +02:00
Brett Adams c84ffb54d2 Bump tesla-fleet-api to 1.1.1. (#145869)
bump
2025-05-31 21:24:54 +02:00
Samuel Xiao 306bbdc697 Bump switchbot-api to 2.4.0 (#145786)
* update switchbot-api version to 2.4.0

* debug for test code
2025-05-31 21:24:54 +02:00
Robert Resch 9879ecad85 Deprecate snips integration (#145784) 2025-05-31 21:24:53 +02:00
Joost Lekkerkerker f0fcef5744 Add more Amazon Devices DHCP matches (#145776) 2025-05-31 21:24:52 +02:00
Bram Kragten aa8a6058b5 Bump version to 2025.6.0b3 2025-05-30 12:56:51 +02:00
J. Diego Rodríguez Royo 48103bd244 Bump aiohomeconnect to 0.17.1 (#145873) 2025-05-30 12:56:45 +02:00
Robert Resch 600ac17a5f Deprecate sms integration (#145847) 2025-05-30 12:56:44 +02:00
Michael d46f28792c Bump aioimmich to 0.7.0 (#145845) 2025-05-30 12:56:43 +02:00
starkillerOG 0f7379c941 Reolink fallback to download command for playback (#145842) 2025-05-30 12:56:43 +02:00
J. Nick Koston 4317fad798 Bump aiohttp to 3.12.4 (#145838) 2025-05-30 12:56:42 +02:00
J. Nick Koston 5cfccb7e1d Bump aiohttp to 3.12.3 (#145837) 2025-05-30 12:56:41 +02:00
Matthew FitzGerald-Chamberlain 097eecd78a Bump pyaprilaire to 0.9.1 (#145836) 2025-05-30 12:56:40 +02:00
Brett Adams 64b4642c49 Fix Tessie volume max and step (#145835)
* Use fixed volume max and step

* Update snapshot
2025-05-30 12:56:39 +02:00
Michael 0e87d14ca8 Use mime type provided by Immich (#145830)
use mime type from immich instead of guessing it
2025-05-30 12:56:38 +02:00
Josef Zweck 4d22b35a9f Bump aiotedee to 0.2.23 (#145822)
* Bump aiotedee to 0.2.23

* update snapshot
2025-05-30 12:56:37 +02:00
G Johansson 26586b4514 Fix language selections in workday (#145813) 2025-05-30 12:56:36 +02:00
Robert Resch 95fb2a7d7f Deprecate decora integration (#145807) 2025-05-30 12:56:35 +02:00
Robert Resch fa66ea31d3 Deprecate tensorflow (#145806)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-30 12:56:34 +02:00
André Lersveen e0d3b819e5 Set correct nobo_hub max temperature (#145751)
Max temperature 30°C is implemented upstream in pynobo and the Nobø Energy Hub app also stops at 30°C.
2025-05-30 12:56:34 +02:00
Bram Kragten 17a0b4f3d0 Bump version to 2025.6.0b2 2025-05-28 23:18:38 +02:00
Bram Kragten d0d228d9f4 Update frontend to 20250528.0 (#145828)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-05-28 23:18:33 +02:00
Michael 309acb961b Fix Immich media source browsing with multiple config entries (#145823)
fix media source browsing with multiple config entries
2025-05-28 23:18:32 +02:00
Michael Hansen 12f8ebb3ea Bump intents to 2025.5.28 (#145816) 2025-05-28 23:18:32 +02:00
David Bonnes 612861061c Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809)
* initial commit

* a better approach

* Add comment
2025-05-28 23:18:31 +02:00
Robert Resch 83af5ec36b Deprecate keyboard integration (#145805) 2025-05-28 23:18:30 +02:00
starkillerOG 74102d0319 Bump reolink-aio to 0.13.4 (#145799) 2025-05-28 23:18:29 +02:00
Robert Resch fbd05a0fcf Deprecate lirc integration (#145797) 2025-05-28 23:18:29 +02:00
Robert Resch a53c786fe0 Deprecate pandora integration (#145785) 2025-05-28 23:18:28 +02:00
Josef Zweck eb2728e5b9 Fix uom for prebrew numbers in lamarzocco (#145772) 2025-05-28 23:18:27 +02:00
J. Diego Rodríguez Royo 3f17223387 Add more information about possible hostnames at Home Connect (#145770) 2025-05-28 23:18:26 +02:00
Robert Resch 74104cf107 Deprecate GStreamer integration (#145768) 2025-05-28 23:18:25 +02:00
Robert Resch 13b4879723 Deprecate dlib image processing integrations (#145767) 2025-05-28 23:18:25 +02:00
Erik Montnemery f1ec0b2c59 Handle late abort when creating subentry (#145765)
* Handle late abort when creating subentry

* Move error handling to the base class

* Narrow down expected error in test
2025-05-28 23:18:24 +02:00
Josef Zweck 6d44daf599 Bump pylamarzocco to 2.0.7 (#145763) 2025-05-28 23:18:23 +02:00
Joost Lekkerkerker 644a6f5569 Add more Amazon Devices DHCP matches (#145754) 2025-05-28 23:18:22 +02:00
Abílio Costa fb83396522 Add Shelly zwave virtual integration (#145749) 2025-05-28 23:18:22 +02:00
Raphael Hehl e825bd0bdb Bump uiprotect to version 7.10.1 (#145737)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-28 23:18:21 +02:00
G Johansson 61823ec7e2 Fix dns resolver error in dnsip config flow validation (#145735)
Fix dns resolver error in dnsip
2025-05-28 23:18:20 +02:00
Michael cd133cbbe3 Add level of collections in Immich media source tree (#145734)
* add layer for collections in media source tree

* re-arange tests, add test for collection layer

* fix
2025-05-28 23:18:19 +02:00
Erik Montnemery 0e7a1bb76c Make async_remove_stale_devices_links_keep_entity_device move entities (#145719)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-28 23:18:18 +02:00
Josef Zweck f86bf69ebc Update otp description for amazon_devices (#145701)
* Update otp description from amazon_devices

* separate

* Update strings.json
2025-05-28 23:18:18 +02:00
Jan Bouwhuis adddf330fd Ensure mqtt sensor unit of measurement validation for state class `measurement_angle` (#145648) 2025-05-28 23:18:17 +02:00
Bram Kragten 10adb57b83 Bump version to 2025.6.0b1 2025-05-27 22:16:13 +02:00
Bram Kragten 3160fe9abc Update frontend to 20250527.0 (#145741) 2025-05-27 22:14:02 +02:00
Erwin Douna 6adb27d173 Tado update mobile devices interval (#145738)
Update the mobile devices interval to five minutes
2025-05-27 22:14:01 +02:00
Joost Lekkerkerker 6e6aae2ea3 Fix unbound local variable in Acmeda config flow (#145729) 2025-05-27 22:14:00 +02:00
Kevin Stillhammer 41a140d16c Debug log the update response in google_travel_time (#145725)
Debug log the update response
2025-05-27 22:14:00 +02:00
Kevin Stillhammer 8880ab6498 Catch PermissionDenied(Route API disabled) in google_travel_time (#145722)
Catch PermissionDenied(Route API disabled)
2025-05-27 22:13:59 +02:00
Martin Hjelmare 389becc4f6 Disable advanced window cover position Matter sensor by default (#145713)
* Disable advanced window cover position Matter sensor by default

* Enanble disabled sensors in snapshot test
2025-05-27 22:13:58 +02:00
Martin Hjelmare 923530972a Remove static pin code length Matter sensors (#145711)
* Remove static Matter sensors

* Clean up translation strings
2025-05-27 22:13:57 +02:00
Martin Hjelmare b84850df9f Fix error stack trace for HomeAssistantError in websocket service call (#145699)
* Add test

* Fix error stack trace for HomeAssistantError in websocket service call
2025-05-27 22:13:56 +02:00
Joost Lekkerkerker 9e7dc1d11d Use string type for amazon devices OTP code (#145698) 2025-05-27 22:13:56 +02:00
Petar Petrov 2830ed6147 Change description on recommended/custom Z-Wave install step (#145688)
Change description on recommended/custom Z-WaveJS step
2025-05-27 22:13:55 +02:00
Petar Petrov bfa919d078 Remove confirm screen after Z-Wave usb discovery (#145682)
* Remove confirm screen after Z-Wave usb discovery

* Simplify async_step_usb
2025-05-27 22:13:54 +02:00
Jan Bouwhuis f09c28e61f Fix justnimbus CI test (#145681) 2025-05-27 22:13:54 +02:00
J. Nick Koston bfdba7713e Bump aiohttp to 3.12.2 (#145671) 2025-05-27 22:13:53 +02:00
Kevin Stillhammer d6cadc1e3f Support addresses with comma in google_travel_time (#145663)
Support addresses with comma
2025-05-27 22:13:52 +02:00
Joost Lekkerkerker 20a6a3f195 Handle Google Nest DHCP flows (#145658)
* Handle Google Nest DHCP flows

* Handle Google Nest DHCP flows
2025-05-27 22:13:51 +02:00
Joost Lekkerkerker f60de45b52 Fix Amazon devices offline handling (#145656) 2025-05-27 22:13:50 +02:00
Joost Lekkerkerker 77031d1ae4 Fix Aquacell snapshot (#145651) 2025-05-27 22:13:49 +02:00
Jan Bouwhuis 9483a88ee1 Fix translation for sensor measurement angle state class (#145649) 2025-05-27 22:13:48 +02:00
Franck Nijhof 3438a4f063
Bump version to 2025.6.0b0 2025-05-26 20:31:18 +00:00
317 changed files with 8621 additions and 1945 deletions

View File

@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.* homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.* homeassistant.components.alert.*
homeassistant.components.alexa.* homeassistant.components.alexa.*
homeassistant.components.alexa_devices.*
homeassistant.components.alpha_vantage.* homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_devices.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.* homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.* homeassistant.components.ambient_network.*

4
CODEOWNERS generated
View File

@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
/tests/components/alert/ @home-assistant/core @frenck /tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amazon_devices/ @chemelli74 /homeassistant/components/alexa_devices/ @chemelli74
/tests/components/amazon_devices/ @chemelli74 /tests/components/alexa_devices/ @chemelli74
/homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot /homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot

View File

@ -3,7 +3,7 @@
"name": "Amazon", "name": "Amazon",
"integrations": [ "integrations": [
"alexa", "alexa",
"amazon_devices", "alexa_devices",
"amazon_polly", "amazon_polly",
"aws", "aws",
"aws_s3", "aws_s3",

View File

@ -0,0 +1,6 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries() entry.unique_id for entry in self._async_current_entries()
} }
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError): with suppress(TimeoutError):
async with timeout(5): async with timeout(5):
hubs: list[aiopulse.Hub] = [ hubs = [
hub hub
async for hub in aiopulse.Hub.discover() async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured if hub.id not in already_configured

View File

@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
async def _async_setup(self) -> None: async def _async_setup(self) -> None:
"""Set up the coordinator.""" """Set up the coordinator."""
self._current_version = ( try:
await self.client.get_current_measures() self._current_version = (
).firmware_version await self.client.get_current_measures()
).firmware_version
except AirGradientError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": str(error)},
) from error
async def _async_update_data(self) -> AirGradientData: async def _async_update_data(self) -> AirGradientData:
try: try:

View File

@ -1,4 +1,4 @@
"""Amazon Devices integration.""" """Alexa Devices integration."""
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -13,7 +13,7 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Amazon Devices platform.""" """Set up Alexa Devices platform."""
coordinator = AmazonDevicesCoordinator(hass, entry) coordinator = AmazonDevicesCoordinator(hass, entry)

View File

@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -25,7 +26,7 @@ PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription): class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Amazon Devices binary sensor entity description.""" """Alexa Devices binary sensor entity description."""
is_on_fn: Callable[[AmazonDevice], bool] is_on_fn: Callable[[AmazonDevice], bool]
@ -34,10 +35,12 @@ BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription( AmazonBinarySensorEntityDescription(
key="online", key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda _device: _device.online, is_on_fn=lambda _device: _device.online,
), ),
AmazonBinarySensorEntityDescription( AmazonBinarySensorEntityDescription(
key="bluetooth", key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth", translation_key="bluetooth",
is_on_fn=lambda _device: _device.bluetooth_state, is_on_fn=lambda _device: _device.bluetooth_state,
), ),
@ -49,7 +52,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry, entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Amazon Devices binary sensors based on a config entry.""" """Set up Alexa Devices binary sensors based on a config entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data

View File

@ -1,4 +1,4 @@
"""Config flow for Amazon Devices integration.""" """Config flow for Alexa Devices integration."""
from __future__ import annotations from __future__ import annotations
@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Amazon Devices.""" """Handle a config flow for Alexa Devices."""
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(), ): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.positive_int, vol.Required(CONF_CODE): cv.string,
} }
), ),
) )

View File

@ -1,8 +1,8 @@
"""Amazon Devices constants.""" """Alexa Devices constants."""
import logging import logging
_LOGGER = logging.getLogger(__package__) _LOGGER = logging.getLogger(__package__)
DOMAIN = "amazon_devices" DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data" CONF_LOGIN_DATA = "login_data"

View File

@ -1,4 +1,4 @@
"""Support for Amazon Devices.""" """Support for Alexa Devices."""
from datetime import timedelta from datetime import timedelta
@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Base coordinator for Amazon Devices.""" """Base coordinator for Alexa Devices."""
config_entry: AmazonConfigEntry config_entry: AmazonConfigEntry

View File

@ -0,0 +1,66 @@
"""Diagnostics support for Alexa Devices integration."""
from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonDevice
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import AmazonConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
devices: list[dict[str, dict[str, Any]]] = [
build_device_data(device) for device in coordinator.data.values()
]
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"last_update success": coordinator.last_update_success,
"last_exception": repr(coordinator.last_exception),
"devices": devices,
},
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = entry.runtime_data
assert device_entry.serial_number
return build_device_data(coordinator.data[device_entry.serial_number])
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
"""Build device data for diagnostics."""
return {
"account name": device.account_name,
"capabilities": device.capabilities,
"device family": device.device_family,
"device type": device.device_type,
"device cluster members": device.device_cluster_members,
"online": device.online,
"serial number": device.serial_number,
"software version": device.software_version,
"do not disturb": device.do_not_disturb,
"response style": device.response_style,
"bluetooth state": device.bluetooth_state,
}

View File

@ -1,4 +1,4 @@
"""Defines a base Amazon Devices entity.""" """Defines a base Alexa Devices entity."""
from aioamazondevices.api import AmazonDevice from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SPEAKER_GROUP_MODEL from aioamazondevices.const import SPEAKER_GROUP_MODEL
@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines a base Amazon Devices entity.""" """Defines a base Alexa Devices entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._serial_num = serial_num self._serial_num = serial_num
model_details = coordinator.api.get_model_details(self.device) model_details = coordinator.api.get_model_details(self.device) or {}
model = model_details["model"] if model_details else None model = model_details.get("model")
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)}, identifiers={(DOMAIN, serial_num)},
name=self.device.account_name, name=self.device.account_name,
model=model, model=model,
model_id=self.device.device_type, model_id=self.device.device_type,
manufacturer="Amazon", manufacturer=model_details.get("manufacturer", "Amazon"),
hw_version=model_details["hw_version"] if model_details else None, hw_version=model_details.get("hw_version"),
sw_version=( sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None self.device.software_version if model != SPEAKER_GROUP_MODEL else None
), ),
@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return super().available and self._serial_num in self.coordinator.data return (
super().available
and self._serial_num in self.coordinator.data
and self.device.online
)

View File

@ -0,0 +1,12 @@
{
"domain": "alexa_devices",
"name": "Alexa Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.0.6"]
}

View File

@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class AmazonNotifyEntityDescription(NotifyEntityDescription): class AmazonNotifyEntityDescription(NotifyEntityDescription):
"""Amazon Devices notify entity description.""" """Alexa Devices notify entity description."""
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]] method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
subkey: str subkey: str
@ -49,7 +49,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry, entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Amazon Devices notification entity based on a config entry.""" """Set up Alexa Devices notification entity based on a config entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data

View File

@ -45,7 +45,9 @@ rules:
discovery-update-info: discovery-update-info:
status: exempt status: exempt
comment: Network information not relevant comment: Network information not relevant
discovery: done discovery:
status: exempt
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
docs-data-update: todo docs-data-update: todo
docs-examples: todo docs-examples: todo
docs-known-limitations: todo docs-known-limitations: todo

View File

@ -5,23 +5,23 @@
"data_description_country": "The country of your Amazon account.", "data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.", "data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.", "data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password sent to your email address." "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
}, },
"config": { "config": {
"flow_title": "{username}", "flow_title": "{username}",
"step": { "step": {
"user": { "user": {
"data": { "data": {
"country": "[%key:component::amazon_devices::common::data_country%]", "country": "[%key:component::alexa_devices::common::data_country%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]" "code": "[%key:component::alexa_devices::common::data_description_code%]"
}, },
"data_description": { "data_description": {
"country": "[%key:component::amazon_devices::common::data_description_country%]", "country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::amazon_devices::common::data_description_username%]", "username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::amazon_devices::common::data_description_password%]", "password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]" "code": "[%key:component::alexa_devices::common::data_description_code%]"
} }
} }
}, },

View File

@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class AmazonSwitchEntityDescription(SwitchEntityDescription): class AmazonSwitchEntityDescription(SwitchEntityDescription):
"""Amazon Devices switch entity description.""" """Alexa Devices switch entity description."""
is_on_fn: Callable[[AmazonDevice], bool] is_on_fn: Callable[[AmazonDevice], bool]
subkey: str subkey: str
@ -43,7 +43,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry, entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Amazon Devices switches based on a config entry.""" """Set up Alexa Devices switches based on a config entry."""
coordinator = entry.runtime_data coordinator = entry.runtime_data

View File

@ -1,33 +0,0 @@
{
"domain": "amazon_devices",
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "08A6BC*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "440049*" },
{ "macaddress": "443D54*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "74D637*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "901195*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==2.1.1"]
}

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyaprilaire"], "loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.0"] "requirements": ["pyaprilaire==0.9.1"]
} }

View File

@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["APsystemsEZ1"], "loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.6.0"] "requirements": ["apsystems-ez1==2.7.0"]
} }

View File

@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.2", "bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.1", "bluetooth-data-tools==1.28.1",
"dbus-fast==2.43.0", "dbus-fast==2.43.0",
"habluetooth==3.48.2" "habluetooth==3.49.0"
] ]
} }

View File

@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None: def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
"""Initialise a Bosch Alarm control panel entity.""" """Initialise a Bosch Alarm control panel entity."""
super().__init__(panel, area_id, unique_id, False, False, True) super().__init__(panel, area_id, unique_id, True, False, True)
self._attr_unique_id = self._area_unique_id self._attr_unique_id = self._area_unique_id
@property @property

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav", "documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"], "loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"] "requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"] "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
} }

View File

@ -1 +1,3 @@
"""The decora component.""" """The decora component."""
DOMAIN = "decora"

View File

@ -21,7 +21,11 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
) )
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from . import DOMAIN
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -90,6 +94,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up an Decora switch.""" """Set up an Decora switch."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Leviton Decora",
},
)
lights = [] lights = []
for address, device_config in config[CONF_DEVICES].items(): for address, device_config in config[CONF_DEVICES].items():
device = {} device = {}

View File

@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import ( from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device, async_remove_stale_devices_links_keep_entity_device,
) )
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -17,6 +19,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry.entry_id, entry.options[CONF_SOURCE] hass, entry.entry_id, entry.options[CONF_SOURCE]
) )
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SOURCE: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_SOURCE]
),
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True return True

View File

@ -1 +1,3 @@
"""The dlib_face_detect component.""" """The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
@ -25,6 +32,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Detect",
},
)
source: list[dict[str, str]] = config[CONF_SOURCE] source: list[dict[str, str]] = config[CONF_SOURCE]
add_entities( add_entities(
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))

View File

@ -1 +1,4 @@
"""The dlib_face_identify component.""" """The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"

View File

@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity, ImageProcessingFaceEntity,
) )
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_FACES, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FACES = "faces"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{ {
@ -39,6 +45,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Dlib Face detection platform.""" """Set up the Dlib Face detection platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Dlib Face Identify",
},
)
confidence: float = config[CONF_CONFIDENCE] confidence: float = config[CONF_CONFIDENCE]
faces: dict[str, str] = config[CONF_FACES] faces: dict[str, str] = config[CONF_FACES]
source: list[dict[str, str]] = config[CONF_SOURCE] source: list[dict[str, str]] = config[CONF_SOURCE]

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
from typing import Any from typing import Any, Literal
import aiodns import aiodns
from aiodns.error import DNSError from aiodns.error import DNSError
@ -62,16 +62,16 @@ async def async_validate_hostname(
"""Validate hostname.""" """Validate hostname."""
async def async_check( async def async_check(
hostname: str, resolver: str, qtype: str, port: int = 53 hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
) -> bool: ) -> bool:
"""Return if able to resolve hostname.""" """Return if able to resolve hostname."""
result = False result: bool = False
with contextlib.suppress(DNSError): with contextlib.suppress(DNSError):
result = bool( _resolver = aiodns.DNSResolver(
await aiodns.DNSResolver( # type: ignore[call-overload] nameservers=[resolver], udp_port=port, tcp_port=port
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
) )
result = bool(await _resolver.query(hostname, qtype))
return result return result
result: dict[str, bool] = {} result: dict[str, bool] = {}

View File

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

View File

@ -1 +1,6 @@
"""The eddystone_temperature component.""" """The eddystone_temperature component."""
DOMAIN = "eddystone_temperature"
CONF_BEACONS = "beacons"
CONF_INSTANCE = "instance"
CONF_NAMESPACE = "namespace"

View File

@ -23,17 +23,18 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import Event, HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_BEACONS = "beacons"
CONF_BT_DEVICE_ID = "bt_device_id" CONF_BT_DEVICE_ID = "bt_device_id"
CONF_INSTANCE = "instance"
CONF_NAMESPACE = "namespace"
BEACON_SCHEMA = vol.Schema( BEACON_SCHEMA = vol.Schema(
{ {
@ -58,6 +59,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Validate configuration, create devices and start monitoring thread.""" """Validate configuration, create devices and start monitoring thread."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Eddystone",
},
)
bt_device_id: int = config[CONF_BT_DEVICE_ID] bt_device_id: int = config[CONF_BT_DEVICE_ID]
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS] beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]

View File

@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) )
return return
device_registry.async_update_device( device_registry.async_get_or_create(
device_id=envoy_device.id, config_entry_id=self.config_entry.entry_id,
new_connections={connection}, identifiers={
(
DOMAIN,
self.envoy_serial_number,
)
},
connections={connection},
) )
_LOGGER.debug("added connection: %s to %s", connection, self.name) _LOGGER.debug("added connection: %s to %s", connection, self.name)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["env_canada"], "loggers": ["env_canada"],
"requirements": ["env-canada==0.10.2"] "requirements": ["env-canada==0.11.2"]
} }

View File

@ -22,5 +22,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["eq3btsmart"], "loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"] "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
} }

View File

@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
_static_info: _InfoT _static_info: _InfoT
_state: _StateT _state: _StateT
_has_state: bool _has_state: bool
unique_id: str
def __init__( def __init__(
self, self,

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio import asyncio
from functools import partial from functools import partial
import logging import logging
import re
from typing import TYPE_CHECKING, Any, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import ( from aioesphomeapi import (
@ -23,6 +22,7 @@ from aioesphomeapi import (
RequiresEncryptionAPIError, RequiresEncryptionAPIError,
UserService, UserService,
UserServiceArgType, UserServiceArgType,
parse_log_message,
) )
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import voluptuous as vol import voluptuous as vol
@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = {
logging.ERROR: LogLevel.LOG_LEVEL_ERROR, logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR, logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
} }
# 7-bit and 8-bit C1 ANSI sequences
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
ANSI_ESCAPE_78BIT = re.compile(
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
)
@callback @callback
@ -387,13 +382,15 @@ class ESPHomeManager:
def _async_on_log(self, msg: SubscribeLogsResponse) -> None: def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
"""Handle a log message from the API.""" """Handle a log message from the API."""
log: bytes = msg.message for line in parse_log_message(
_LOGGER.log( msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG), ):
"%s: %s", _LOGGER.log(
self.entry.title, LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"), "%s: %s",
) self.entry.title,
line,
)
@callback @callback
def _async_get_equivalent_log_level(self) -> LogLevel: def _async_get_equivalent_log_level(self) -> LogLevel:

View File

@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"aioesphomeapi==31.1.0", "aioesphomeapi==32.2.1",
"esphome-dashboard-api==1.3.0", "esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1" "bleak-esphome==2.16.0"
], ],
"zeroconf": ["_esphomelib._tcp.local."] "zeroconf": ["_esphomelib._tcp.local."]
} }

View File

@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
if self._static_info.supports_pause: if self._static_info.supports_pause:
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
self._attr_supported_features = flags self._attr_supported_features = flags
self._entry_data.media_player_formats[static_info.unique_id] = cast( self._entry_data.media_player_formats[self.unique_id] = cast(
MediaPlayerInfo, static_info MediaPlayerInfo, static_info
).supported_formats ).supported_formats
@ -114,9 +114,8 @@ class EsphomeMediaPlayer(
media_id = async_process_play_media_url(self.hass, media_id) media_id = async_process_play_media_url(self.hass, media_id)
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE) announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY) bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
supported_formats: list[MediaPlayerSupportedFormat] | None = ( supported_formats: list[MediaPlayerSupportedFormat] | None = (
self._entry_data.media_player_formats.get(self._static_info.unique_id) self._entry_data.media_player_formats.get(self.unique_id)
) )
if ( if (
@ -139,7 +138,7 @@ class EsphomeMediaPlayer(
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Handle entity being removed.""" """Handle entity being removed."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
self._entry_data.media_player_formats.pop(self.entity_id, None) self._entry_data.media_player_formats.pop(self.unique_id, None)
def _get_proxy_url( def _get_proxy_url(
self, self,

View File

@ -84,6 +84,7 @@ async def async_setup_entry(
name=f"Freebox {sensor_name}", name=f"Freebox {sensor_name}",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
), ),
) )
for sensor_name in router.sensors_temperature for sensor_name in router.sensors_temperature

View File

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

View File

@ -5,11 +5,18 @@ import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.helpers.device import ( from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device, async_remove_stale_devices_links_keep_entity_device,
) )
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
DOMAIN = "generic_hygrostat" DOMAIN = "generic_hygrostat"
@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.options[CONF_HUMIDIFIER], entry.options[CONF_HUMIDIFIER],
) )
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the humidifer,
# but not the humidity sensor because the generic_hygrostat adds itself to the
# humidifier's device.
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HUMIDIFIER]
),
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
source_entity_removed=source_entity_removed,
)
)
async def async_sensor_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] != "update":
return
if "entity_id" not in data["changes"]:
return
# Entity_id changed, update the config entry
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SENSOR: data["entity_id"]},
)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, entry.options[CONF_SENSOR], async_sensor_updated
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True return True

View File

@ -1,12 +1,16 @@
"""The generic_thermostat component.""" """The generic_thermostat component."""
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device import ( from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device, async_remove_stale_devices_links_keep_entity_device,
) )
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from .const import CONF_HEATER, PLATFORMS from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.entry_id, entry.entry_id,
entry.options[CONF_HEATER], entry.options[CONF_HEATER],
) )
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_HEATER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the heater, but
# not the temperature sensor because the generic_hygrostat adds itself to the
# heater's device.
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HEATER]
),
source_entity_id_or_uuid=entry.options[CONF_HEATER],
source_entity_removed=source_entity_removed,
)
)
async def async_sensor_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] != "update":
return
if "entity_id" not in data["changes"]:
return
# Entity_id changed, update the config entry
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SENSOR: data["entity_id"]},
)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, entry.options[CONF_SENSOR], async_sensor_updated
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True return True

View File

@ -8,6 +8,6 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["go2rtc-client==0.1.3b0"], "requirements": ["go2rtc-client==0.2.1"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google", "documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"] "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
} }

View File

@ -12,6 +12,7 @@ import aiohttp
from aiohttp import web from aiohttp import web
from gassist_text import TextAssistant from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from grpc import RpcError
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -25,6 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
@ -83,7 +85,17 @@ async def async_send_text_commands(
) as assistant: ) as assistant:
command_response_list = [] command_response_list = []
for command in commands: for command in commands:
resp = await hass.async_add_executor_job(assistant.assist, command) try:
resp = await hass.async_add_executor_job(assistant.assist, command)
except RpcError as err:
_LOGGER.error(
"Failed to send command '%s' to Google Assistant: %s",
command,
err,
)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="grpc_error"
) from err
text_response = resp[0] text_response = resp[0]
_LOGGER.debug("command: %s\nresponse: %s", command, text_response) _LOGGER.debug("command: %s\nresponse: %s", command, text_response)
audio_response = resp[2] audio_response = resp[2]

View File

@ -57,5 +57,10 @@
} }
} }
} }
},
"exceptions": {
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
}
} }
} }

View File

@ -50,7 +50,12 @@ from .const import (
UNITS_IMPERIAL, UNITS_IMPERIAL,
UNITS_METRIC, UNITS_METRIC,
) )
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
RECONFIGURE_SCHEMA = vol.Schema( RECONFIGURE_SCHEMA = vol.Schema(
{ {
@ -188,6 +193,8 @@ async def validate_input(
user_input[CONF_ORIGIN], user_input[CONF_ORIGIN],
user_input[CONF_DESTINATION], user_input[CONF_DESTINATION],
) )
except PermissionDeniedException:
return {"base": "permission_denied"}
except InvalidApiKeyException: except InvalidApiKeyException:
return {"base": "invalid_auth"} return {"base": "invalid_auth"}
except TimeoutError: except TimeoutError:

View File

@ -7,6 +7,7 @@ from google.api_core.exceptions import (
Forbidden, Forbidden,
GatewayTimeout, GatewayTimeout,
GoogleAPIError, GoogleAPIError,
PermissionDenied,
Unauthorized, Unauthorized,
) )
from google.maps.routing_v2 import ( from google.maps.routing_v2 import (
@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
from google.type import latlng_pb2 from google.type import latlng_pb2
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.location import find_coordinates
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
try: try:
formatted_coordinates = coordinates.split(",") formatted_coordinates = coordinates.split(",")
vol.Schema(cv.gps(formatted_coordinates)) vol.Schema(cv.gps(formatted_coordinates))
except (AttributeError, vol.ExactSequenceInvalid): except (AttributeError, vol.Invalid):
return Waypoint(address=location) return Waypoint(address=location)
return Waypoint( return Waypoint(
location=Location( location=Location(
@ -67,6 +76,9 @@ async def validate_config_entry(
await client.compute_routes( await client.compute_routes(
request, metadata=[("x-goog-fieldmask", field_mask)] request, metadata=[("x-goog-fieldmask", field_mask)]
) )
except PermissionDenied as permission_error:
_LOGGER.error("Permission denied: %s", permission_error.message)
raise PermissionDeniedException from permission_error
except (Unauthorized, Forbidden) as unauthorized_error: except (Unauthorized, Forbidden) as unauthorized_error:
_LOGGER.error("Request denied: %s", unauthorized_error.message) _LOGGER.error("Request denied: %s", unauthorized_error.message)
raise InvalidApiKeyException from unauthorized_error raise InvalidApiKeyException from unauthorized_error
@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
class UnknownException(Exception): class UnknownException(Exception):
"""Unknown API Error.""" """Unknown API Error."""
class PermissionDeniedException(Exception):
"""Permission Denied Error."""
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Create an issue for the Routes API being disabled."""
async_create_issue(
hass,
DOMAIN,
f"routes_api_disabled_{entry.entry_id}",
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
is_fixable=False,
severity=IssueSeverity.ERROR,
translation_key="routes_api_disabled",
translation_placeholders={
"entry_title": entry.title,
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
},
)
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Delete the issue for the Routes API being disabled."""
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")

View File

@ -7,7 +7,7 @@ import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from google.api_core.client_options import ClientOptions from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import GoogleAPIError from google.api_core.exceptions import GoogleAPIError, PermissionDenied
from google.maps.routing_v2 import ( from google.maps.routing_v2 import (
ComputeRoutesRequest, ComputeRoutesRequest,
Route, Route,
@ -58,7 +58,11 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM, TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM, UNITS_TO_GOOGLE_SDK_ENUM,
) )
from .helpers import convert_to_waypoint from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
response = await self._client.compute_routes( response = await self._client.compute_routes(
request, metadata=[("x-goog-fieldmask", FIELD_MASK)] request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
) )
_LOGGER.debug("Received response: %s", response)
if response is not None and len(response.routes) > 0: if response is not None and len(response.routes) > 0:
self._route = response.routes[0] self._route = response.routes[0]
delete_routes_api_disabled_issue(self.hass, self._config_entry)
except PermissionDenied:
_LOGGER.error("Routes API is disabled for this API key")
create_routes_api_disabled_issue(self.hass, self._config_entry)
self._route = None
except GoogleAPIError as ex: except GoogleAPIError as ex:
_LOGGER.error("Error getting travel time: %s", ex) _LOGGER.error("Error getting travel time: %s", ex)
self._route = None self._route = None

View File

@ -21,6 +21,7 @@
} }
}, },
"error": { "error": {
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
@ -100,5 +101,11 @@
"fewer_transfers": "Fewer transfers" "fewer_transfers": "Fewer transfers"
} }
} }
},
"issues": {
"routes_api_disabled": {
"title": "The Routes API must be enabled",
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
}
} }
} }

View File

@ -1 +1,3 @@
"""The gstreamer component.""" """The gstreamer component."""
DOMAIN = "gstreamer"

View File

@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PIPELINE = "pipeline" CONF_PIPELINE = "pipeline"
DOMAIN = "gstreamer"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
@ -48,6 +50,20 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Gstreamer platform.""" """Set up the Gstreamer platform."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "GStreamer",
},
)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
pipeline = config.get(CONF_PIPELINE) pipeline = config.get(CONF_PIPELINE)

View File

@ -9,8 +9,10 @@ from functools import partial
import logging import logging
import os import os
import re import re
import struct
from typing import Any, NamedTuple from typing import Any, NamedTuple
import aiofiles
from aiohasupervisor import SupervisorError from aiohasupervisor import SupervisorError
import voluptuous as vol import voluptuous as vol
@ -37,6 +39,7 @@ from homeassistant.helpers import (
config_validation as cv, config_validation as cv,
device_registry as dr, device_registry as dr,
discovery_flow, discovery_flow,
issue_registry as ir,
) )
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.deprecation import ( from homeassistant.helpers.deprecation import (
@ -51,6 +54,7 @@ from homeassistant.helpers.hassio import (
get_supervisor_ip as _get_supervisor_ip, get_supervisor_ip as _get_supervisor_ip,
is_hassio as _is_hassio, is_hassio as _is_hassio,
) )
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service_info.hassio import ( from homeassistant.helpers.service_info.hassio import (
HassioServiceInfo as _HassioServiceInfo, HassioServiceInfo as _HassioServiceInfo,
) )
@ -109,7 +113,7 @@ from .coordinator import (
get_core_info, # noqa: F401 get_core_info, # noqa: F401
get_core_stats, # noqa: F401 get_core_stats, # noqa: F401
get_host_info, # noqa: F401 get_host_info, # noqa: F401
get_info, # noqa: F401 get_info,
get_issues_info, # noqa: F401 get_issues_info, # noqa: F401
get_os_info, get_os_info,
get_supervisor_info, # noqa: F401 get_supervisor_info, # noqa: F401
@ -168,6 +172,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$")) VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
DEPRECATION_URL = (
"https://www.home-assistant.io/blog/2025/05/22/"
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
)
def valid_addon(value: Any) -> str: def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug.""" """Validate value is a valid addon slug."""
@ -225,6 +234,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
) )
def _is_32_bit() -> bool:
size = struct.calcsize("P")
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = await arch_file.read()
return {"x86": "i386"}.get(raw_arch, raw_arch)
class APIEndpointSettings(NamedTuple): class APIEndpointSettings(NamedTuple):
"""Settings for API endpoint.""" """Settings for API endpoint."""
@ -546,6 +566,62 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data[ADDONS_COORDINATOR] = coordinator hass.data[ADDONS_COORDINATOR] = coordinator
arch = await _get_arch()
def deprecated_setup_issue() -> None:
os_info = get_os_info(hass)
info = get_info(hass)
if os_info is None or info is None:
return
is_haos = info.get("hassos") is not None
board = os_info.get("board")
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
unsupported_os_on_board = board in {"rpi3", "rpi4"}
if is_haos and (unsupported_board or unsupported_os_on_board):
issue_id = "deprecated_os_"
if unsupported_os_on_board:
issue_id += "aarch64"
elif unsupported_board:
issue_id += "armv7"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_guide": "https://www.home-assistant.io/installation/",
},
)
bit32 = _is_32_bit()
deprecated_architecture = bit32 and not (
unsupported_board or unsupported_os_on_board
)
if not is_haos or deprecated_architecture:
issue_id = "deprecated"
if not is_haos:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": "OS" if is_haos else "Supervised",
"arch": arch,
},
)
listener()
listener = coordinator.async_add_listener(deprecated_setup_issue)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@ -1 +1,3 @@
"""The hddtemp component.""" """The hddtemp component."""
DOMAIN = "hddtemp"

View File

@ -22,11 +22,14 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device" ATTR_DEVICE = "device"
@ -56,6 +59,21 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the HDDTemp sensor.""" """Set up the HDDTemp sensor."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "hddtemp",
},
)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)

View File

@ -5,26 +5,13 @@ from __future__ import annotations
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started from homeassistant.helpers.start import async_at_started
from homeassistant.util import dt as dt_util
from .const import ( from .const import TRAVEL_MODE_PUBLIC
CONF_ARRIVAL_TIME,
CONF_DEPARTURE_TIME,
CONF_DESTINATION_ENTITY_ID,
CONF_DESTINATION_LATITUDE,
CONF_DESTINATION_LONGITUDE,
CONF_ORIGIN_ENTITY_ID,
CONF_ORIGIN_LATITUDE,
CONF_ORIGIN_LONGITUDE,
CONF_ROUTE_MODE,
TRAVEL_MODE_PUBLIC,
)
from .coordinator import ( from .coordinator import (
HereConfigEntry, HereConfigEntry,
HERERoutingDataUpdateCoordinator, HERERoutingDataUpdateCoordinator,
HERETransitDataUpdateCoordinator, HERETransitDataUpdateCoordinator,
) )
from .model import HERETravelTimeConfig
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
@ -33,29 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry)
"""Set up HERE Travel Time from a config entry.""" """Set up HERE Travel Time from a config entry."""
api_key = config_entry.data[CONF_API_KEY] api_key = config_entry.data[CONF_API_KEY]
arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, ""))
departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, ""))
here_travel_time_config = HERETravelTimeConfig(
destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
travel_mode=config_entry.data[CONF_MODE],
route_mode=config_entry.options[CONF_ROUTE_MODE],
arrival=arrival,
departure=departure,
)
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator] cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
cls = HERETransitDataUpdateCoordinator cls = HERETransitDataUpdateCoordinator
else: else:
cls = HERERoutingDataUpdateCoordinator cls = HERERoutingDataUpdateCoordinator
data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config) data_coordinator = cls(hass, config_entry, api_key)
config_entry.runtime_data = data_coordinator config_entry.runtime_data = data_coordinator
async def _async_update_at_start(_: HomeAssistant) -> None: async def _async_update_at_start(_: HomeAssistant) -> None:

View File

@ -26,7 +26,7 @@ from here_transit import (
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfLength from homeassistant.const import CONF_MODE, UnitOfLength
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.location import find_coordinates
@ -34,8 +34,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_conversion import DistanceConverter
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST from .const import (
from .model import HERETravelTimeConfig, HERETravelTimeData CONF_ARRIVAL_TIME,
CONF_DEPARTURE_TIME,
CONF_DESTINATION_ENTITY_ID,
CONF_DESTINATION_LATITUDE,
CONF_DESTINATION_LONGITUDE,
CONF_ORIGIN_ENTITY_ID,
CONF_ORIGIN_LATITUDE,
CONF_ORIGIN_LONGITUDE,
CONF_ROUTE_MODE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
ROUTE_MODE_FASTEST,
)
from .model import HERETravelTimeAPIParams, HERETravelTimeData
BACKOFF_MULTIPLIER = 1.1 BACKOFF_MULTIPLIER = 1.1
@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
"""here_routing DataUpdateCoordinator.""" """HERETravelTime DataUpdateCoordinator for the routing API."""
config_entry: HereConfigEntry config_entry: HereConfigEntry
@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
hass: HomeAssistant, hass: HomeAssistant,
config_entry: HereConfigEntry, config_entry: HereConfigEntry,
api_key: str, api_key: str,
config: HERETravelTimeConfig,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__( super().__init__(
@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
) )
self._api = HERERoutingApi(api_key) self._api = HERERoutingApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData: async def _async_update_data(self) -> HERETravelTimeData:
"""Get the latest data from the HERE Routing API.""" """Get the latest data from the HERE Routing API."""
origin, destination, arrival, departure = prepare_parameters( params = prepare_parameters(self.hass, self.config_entry)
self.hass, self.config
)
route_mode = (
RoutingMode.FAST
if self.config.route_mode == ROUTE_MODE_FASTEST
else RoutingMode.SHORT
)
_LOGGER.debug( _LOGGER.debug(
( (
"Requesting route for origin: %s, destination: %s, route_mode: %s," "Requesting route for origin: %s, destination: %s, route_mode: %s,"
" mode: %s, arrival: %s, departure: %s" " mode: %s, arrival: %s, departure: %s"
), ),
origin, params.origin,
destination, params.destination,
route_mode, params.route_mode,
TransportMode(self.config.travel_mode), TransportMode(params.travel_mode),
arrival, params.arrival,
departure, params.departure,
) )
try: try:
response = await self._api.route( response = await self._api.route(
transport_mode=TransportMode(self.config.travel_mode), transport_mode=TransportMode(params.travel_mode),
origin=here_routing.Place(origin[0], origin[1]), origin=here_routing.Place(params.origin[0], params.origin[1]),
destination=here_routing.Place(destination[0], destination[1]), destination=here_routing.Place(
routing_mode=route_mode, params.destination[0], params.destination[1]
arrival_time=arrival, ),
departure_time=departure, routing_mode=params.route_mode,
arrival_time=params.arrival,
departure_time=params.departure,
return_values=[Return.POLYINE, Return.SUMMARY], return_values=[Return.POLYINE, Return.SUMMARY],
spans=[Spans.NAMES], spans=[Spans.NAMES],
) )
@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
class HERETransitDataUpdateCoordinator( class HERETransitDataUpdateCoordinator(
DataUpdateCoordinator[HERETravelTimeData | None] DataUpdateCoordinator[HERETravelTimeData | None]
): ):
"""HERETravelTime DataUpdateCoordinator.""" """HERETravelTime DataUpdateCoordinator for the transit API."""
config_entry: HereConfigEntry config_entry: HereConfigEntry
@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: HereConfigEntry, config_entry: HereConfigEntry,
api_key: str, api_key: str,
config: HERETravelTimeConfig,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__( super().__init__(
@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator(
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
) )
self._api = HERETransitApi(api_key) self._api = HERETransitApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData | None: async def _async_update_data(self) -> HERETravelTimeData | None:
"""Get the latest data from the HERE Routing API.""" """Get the latest data from the HERE Routing API."""
origin, destination, arrival, departure = prepare_parameters( params = prepare_parameters(self.hass, self.config_entry)
self.hass, self.config
)
_LOGGER.debug( _LOGGER.debug(
( (
"Requesting transit route for origin: %s, destination: %s, arrival: %s," "Requesting transit route for origin: %s, destination: %s, arrival: %s,"
" departure: %s" " departure: %s"
), ),
origin, params.origin,
destination, params.destination,
arrival, params.arrival,
departure, params.departure,
) )
try: try:
response = await self._api.route( response = await self._api.route(
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]), origin=here_transit.Place(
destination=here_transit.Place( latitude=params.origin[0], longitude=params.origin[1]
latitude=destination[0], longitude=destination[1]
), ),
arrival_time=arrival, destination=here_transit.Place(
departure_time=departure, latitude=params.destination[0], longitude=params.destination[1]
),
arrival_time=params.arrival,
departure_time=params.departure,
return_values=[ return_values=[
here_transit.Return.POLYLINE, here_transit.Return.POLYLINE,
here_transit.Return.TRAVEL_SUMMARY, here_transit.Return.TRAVEL_SUMMARY,
@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator(
def prepare_parameters( def prepare_parameters(
hass: HomeAssistant, hass: HomeAssistant,
config: HERETravelTimeConfig, config_entry: HereConfigEntry,
) -> tuple[list[str], list[str], str | None, str | None]: ) -> HERETravelTimeAPIParams:
"""Prepare parameters for the HERE api.""" """Prepare parameters for the HERE api."""
def _from_entity_id(entity_id: str) -> list[str]: def _from_entity_id(entity_id: str) -> list[str]:
@ -305,32 +308,55 @@ def prepare_parameters(
return formatted_coordinates return formatted_coordinates
# Destination # Destination
if config.destination_entity_id is not None: if (
destination = _from_entity_id(config.destination_entity_id) destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
) is not None:
destination = _from_entity_id(str(destination_entity_id))
else: else:
destination = [ destination = [
str(config.destination_latitude), str(config_entry.data[CONF_DESTINATION_LATITUDE]),
str(config.destination_longitude), str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
] ]
# Origin # Origin
if config.origin_entity_id is not None: if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
origin = _from_entity_id(config.origin_entity_id) origin = _from_entity_id(str(origin_entity_id))
else: else:
origin = [ origin = [
str(config.origin_latitude), str(config_entry.data[CONF_ORIGIN_LATITUDE]),
str(config.origin_longitude), str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
] ]
# Arrival/Departure # Arrival/Departure
arrival: str | None = None arrival: datetime | None = None
departure: str | None = None if (
if config.arrival is not None: conf_arrival := dt_util.parse_time(
arrival = next_datetime(config.arrival).isoformat() config_entry.options.get(CONF_ARRIVAL_TIME, "")
if config.departure is not None: )
departure = next_datetime(config.departure).isoformat() ) is not None:
arrival = next_datetime(conf_arrival)
departure: datetime | None = None
if (
conf_departure := dt_util.parse_time(
config_entry.options.get(CONF_DEPARTURE_TIME, "")
)
) is not None:
departure = next_datetime(conf_departure)
return (origin, destination, arrival, departure) route_mode = (
RoutingMode.FAST
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
else RoutingMode.SHORT
)
return HERETravelTimeAPIParams(
destination=destination,
origin=origin,
travel_mode=config_entry.data[CONF_MODE],
route_mode=route_mode,
arrival=arrival,
departure=departure,
)
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None: def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import time from datetime import datetime
from typing import TypedDict from typing import TypedDict
@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict):
@dataclass @dataclass
class HERETravelTimeConfig: class HERETravelTimeAPIParams:
"""Configuration for HereTravelTimeDataUpdateCoordinator.""" """Configuration for polling the HERE API."""
destination_latitude: float | None destination: list[str]
destination_longitude: float | None origin: list[str]
destination_entity_id: str | None
origin_latitude: float | None
origin_longitude: float | None
origin_entity_id: str | None
travel_mode: str travel_mode: str
route_mode: str route_mode: str
arrival: time | None arrival: datetime | None
departure: time | None departure: datetime | None

View File

@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import ( from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device, async_remove_stale_devices_links_keep_entity_device,
) )
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
@ -51,6 +53,30 @@ async def async_setup_entry(
entry.options[CONF_ENTITY_ID], entry.options[CONF_ENTITY_ID],
) )
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we remove the config entry because
# history_stats does not allow replacing the input entity.
await hass.config_entries.async_remove(entry.entry_id)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_ENTITY_ID]
),
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))

View File

@ -107,7 +107,7 @@ OPTIONS_FLOW = {
} }
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for History stats.""" """Handle a config flow for History stats."""
config_flow = CONFIG_FLOW config_flow = CONFIG_FLOW

View File

@ -25,17 +25,12 @@ def _get_obj_holidays_and_language(
selected_categories: list[str] | None, selected_categories: list[str] | None,
) -> tuple[HolidayBase, str]: ) -> tuple[HolidayBase, str]:
"""Get the object for the requested country and year.""" """Get the object for the requested country and year."""
if selected_categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *selected_categories]
obj_holidays = country_holidays( obj_holidays = country_holidays(
country, country,
subdiv=province, subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1}, years={dt_util.now().year, dt_util.now().year + 1},
language=language, language=language,
categories=categories, categories=selected_categories,
) )
if language == "en": if language == "en":
for lang in obj_holidays.supported_languages: for lang in obj_holidays.supported_languages:
@ -45,7 +40,7 @@ def _get_obj_holidays_and_language(
subdiv=province, subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1}, years={dt_util.now().year, dt_util.now().year + 1},
language=lang, language=lang,
categories=categories, categories=selected_categories,
) )
language = lang language = lang
break break
@ -59,7 +54,7 @@ def _get_obj_holidays_and_language(
subdiv=province, subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1}, years={dt_util.now().year, dt_util.now().year + 1},
language=default_language, language=default_language,
categories=categories, categories=selected_categories,
) )
language = default_language language = default_language
@ -77,6 +72,11 @@ async def async_setup_entry(
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES) categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
language = hass.config.language language = hass.config.language
if categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *categories]
obj_holidays, language = await hass.async_add_executor_job( obj_holidays, language = await hass.async_add_executor_job(
_get_obj_holidays_and_language, country, province, language, categories _get_obj_holidays_and_language, country, province, language, categories
) )

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["holidays==0.73", "babel==2.15.0"] "requirements": ["holidays==0.74", "babel==2.15.0"]
} }

View File

@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
authorize_url=OAUTH2_AUTHORIZE, authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN, token_url=OAUTH2_TOKEN,
) )
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
"developer_dashboard_url": "https://developer.home-connect.com/",
"applications_url": "https://developer.home-connect.com/applications",
"register_application_url": "https://developer.home-connect.com/application/add",
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
}

View File

@ -10,17 +10,17 @@
"macaddress": "C8D778*" "macaddress": "C8D778*"
}, },
{ {
"hostname": "(bosch|siemens)-*", "hostname": "(balay|bosch|neff|siemens)-*",
"macaddress": "68A40E*" "macaddress": "68A40E*"
}, },
{ {
"hostname": "siemens-*", "hostname": "(siemens|neff)-*",
"macaddress": "38B4D3*" "macaddress": "38B4D3*"
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/home_connect", "documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aiohomeconnect"], "loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.0"], "requirements": ["aiohomeconnect==0.17.1"],
"zeroconf": ["_homeconnect._tcp.local."] "zeroconf": ["_homeconnect._tcp.local."]
} }

View File

@ -1,4 +1,7 @@
{ {
"application_credentials": {
"description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**."
},
"common": { "common": {
"confirmed": "Confirmed", "confirmed": "Confirmed",
"present": "Present" "present": "Present"
@ -13,7 +16,7 @@
"description": "The Home Connect integration needs to re-authenticate your account" "description": "The Home Connect integration needs to re-authenticate your account"
}, },
"oauth_discovery": { "oauth_discovery": {
"description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect." "description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect."
} }
}, },
"abort": { "abort": {

View File

@ -4,8 +4,10 @@ import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import itertools as it import itertools as it
import logging import logging
from typing import TYPE_CHECKING, Any import struct
from typing import Any
import aiofiles
import voluptuous as vol import voluptuous as vol
from homeassistant import config as conf_util, core_config from homeassistant import config as conf_util, core_config
@ -38,7 +40,6 @@ from homeassistant.helpers import (
restore_state, restore_state,
) )
from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service import ( from homeassistant.helpers.service import (
async_extract_config_entry_ids, async_extract_config_entry_ids,
@ -95,6 +96,17 @@ DEPRECATION_URL = (
) )
def _is_32_bit() -> bool:
size = struct.calcsize("P")
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = (await arch_file.read()).strip()
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
"""Set up general services related to Home Assistant.""" """Set up general services related to Home Assistant."""
@ -402,79 +414,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
info = await async_get_system_info(hass) info = await async_get_system_info(hass)
installation_type = info["installation_type"][15:] installation_type = info["installation_type"][15:]
deprecated_method = installation_type in { if installation_type in {"Core", "Container"}:
"Core", deprecated_method = installation_type == "Core"
"Supervised", bit32 = _is_32_bit()
} arch = info["arch"]
arch = info["arch"] if bit32 and installation_type == "Container":
if arch == "armv7": arch = await _get_arch()
if installation_type == "OS": ir.async_create_issue(
# Local import to avoid circular dependencies hass,
# We use the import helper because hassio DOMAIN,
# may not be loaded yet and we don't want to "deprecated_container",
# do blocking I/O in the event loop to import it. learn_more_url=DEPRECATION_URL,
if TYPE_CHECKING: is_fixable=False,
# pylint: disable-next=import-outside-toplevel severity=IssueSeverity.WARNING,
from homeassistant.components import hassio translation_key="deprecated_container",
else: translation_placeholders={"arch": arch},
hassio = await async_import_module( )
hass, "homeassistant.components.hassio" deprecated_architecture = bit32 and installation_type != "Container"
) if deprecated_method or deprecated_architecture:
os_info = hassio.get_os_info(hass) issue_id = "deprecated"
assert os_info is not None if deprecated_method:
issue_id = "deprecated_os_" issue_id += "_method"
board = os_info.get("board") if deprecated_architecture:
if board in {"rpi3", "rpi4"}: issue_id += "_architecture"
issue_id += "aarch64"
elif board in {"tinker", "odroid-xu4", "rpi2"}:
issue_id += "armv7"
ir.async_create_issue( ir.async_create_issue(
hass, hass,
DOMAIN, DOMAIN,
issue_id, issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL, learn_more_url=DEPRECATION_URL,
is_fixable=False, is_fixable=False,
severity=IssueSeverity.WARNING, severity=IssueSeverity.WARNING,
translation_key=issue_id, translation_key=issue_id,
translation_placeholders={ translation_placeholders={
"installation_guide": "https://www.home-assistant.io/installation/", "installation_type": installation_type,
"arch": arch,
}, },
) )
elif installation_type == "Container":
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_container_armv7",
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_container_armv7",
)
deprecated_architecture = False
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
deprecated_architecture = True
if deprecated_method or deprecated_architecture:
issue_id = "deprecated"
if deprecated_method:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": installation_type,
"arch": arch,
},
)
return True return True

View File

@ -18,9 +18,13 @@
"title": "The {integration_title} YAML configuration is being removed", "title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}, },
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries."
},
"deprecated_system_packages_yaml_integration": { "deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed", "title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." "description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant."
}, },
"historic_currency": { "historic_currency": {
"title": "The configured currency is no longer in use", "title": "The configured currency is no longer in use",
@ -103,9 +107,9 @@
"title": "Deprecation notice: 32-bit architecture", "title": "Deprecation notice: 32-bit architecture",
"description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware." "description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
}, },
"deprecated_container_armv7": { "deprecated_container": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." "description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
}, },
"deprecated_os_aarch64": { "deprecated_os_aarch64": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",

View File

@ -112,6 +112,7 @@ class HomematicipHAP:
self.config_entry = config_entry self.config_entry = config_entry
self._ws_close_requested = False self._ws_close_requested = False
self._ws_connection_closed = asyncio.Event()
self._retry_task: asyncio.Task | None = None self._retry_task: asyncio.Task | None = None
self._tries = 0 self._tries = 0
self._accesspoint_connected = True self._accesspoint_connected = True
@ -218,6 +219,8 @@ class HomematicipHAP:
try: try:
await self.home.get_current_state_async() await self.home.get_current_state_async()
hmip_events = self.home.enable_events() hmip_events = self.home.enable_events()
self.home.set_on_connected_handler(self.ws_connected_handler)
self.home.set_on_disconnected_handler(self.ws_disconnected_handler)
tries = 0 tries = 0
await hmip_events await hmip_events
except HmipConnectionError: except HmipConnectionError:
@ -267,6 +270,18 @@ class HomematicipHAP:
"Reset connection to access point id %s", self.config_entry.unique_id "Reset connection to access point id %s", self.config_entry.unique_id
) )
async def ws_connected_handler(self) -> None:
"""Handle websocket connected."""
_LOGGER.debug("WebSocket connection to HomematicIP established")
if self._ws_connection_closed.is_set():
await self.get_state()
self._ws_connection_closed.clear()
async def ws_disconnected_handler(self) -> None:
"""Handle websocket disconnection."""
_LOGGER.warning("WebSocket connection to HomematicIP closed")
self._ws_connection_closed.set()
async def get_hap( async def get_hap(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
@ -290,6 +305,7 @@ class HomematicipHAP:
raise HmipcConnectionError from err raise HmipcConnectionError from err
home.on_update(self.async_update) home.on_update(self.async_update)
home.on_create(self.async_create_entity) home.on_create(self.async_create_entity)
hass.loop.create_task(self.async_connect()) hass.loop.create_task(self.async_connect())
return home return home

View File

@ -4,16 +4,16 @@ from __future__ import annotations
from typing import Any from typing import Any
from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState
from homematicip.base.functionalChannels import NotificationLightChannel from homematicip.base.functionalChannels import NotificationLightChannel
from homematicip.device import ( from homematicip.device import (
BrandDimmer, BrandDimmer,
BrandSwitchMeasuring,
BrandSwitchNotificationLight, BrandSwitchNotificationLight,
Dimmer, Dimmer,
DinRailDimmer3, DinRailDimmer3,
FullFlushDimmer, FullFlushDimmer,
PluggableDimmer, PluggableDimmer,
SwitchMeasuring,
WiredDimmer3, WiredDimmer3,
) )
from packaging.version import Version from packaging.version import Version
@ -44,9 +44,12 @@ async def async_setup_entry(
hap = config_entry.runtime_data hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = [] entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices: for device in hap.home.devices:
if isinstance(device, BrandSwitchMeasuring): if (
isinstance(device, SwitchMeasuring)
and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING
):
entities.append(HomematicipLightMeasuring(hap, device)) entities.append(HomematicipLightMeasuring(hap, device))
elif isinstance(device, BrandSwitchNotificationLight): if isinstance(device, BrandSwitchNotificationLight):
device_version = Version(device.firmwareVersion) device_version = Version(device.firmwareVersion)
entities.append(HomematicipLight(hap, device)) entities.append(HomematicipLight(hap, device))

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["homematicip"], "loggers": ["homematicip"],
"requirements": ["homematicip==2.0.1.1"] "requirements": ["homematicip==2.0.4"]
} }

View File

@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import (
FunctionalChannel, FunctionalChannel,
) )
from homematicip.device import ( from homematicip.device import (
BrandSwitchMeasuring,
EnergySensorsInterface, EnergySensorsInterface,
FloorTerminalBlock6, FloorTerminalBlock6,
FloorTerminalBlock10, FloorTerminalBlock10,
FloorTerminalBlock12, FloorTerminalBlock12,
FullFlushSwitchMeasuring,
HeatingThermostat, HeatingThermostat,
HeatingThermostatCompact, HeatingThermostatCompact,
HeatingThermostatEvo, HeatingThermostatEvo,
@ -26,9 +24,9 @@ from homematicip.device import (
MotionDetectorOutdoor, MotionDetectorOutdoor,
MotionDetectorPushButton, MotionDetectorPushButton,
PassageDetector, PassageDetector,
PlugableSwitchMeasuring,
PresenceDetectorIndoor, PresenceDetectorIndoor,
RoomControlDeviceAnalog, RoomControlDeviceAnalog,
SwitchMeasuring,
TemperatureDifferenceSensor2, TemperatureDifferenceSensor2,
TemperatureHumiditySensorDisplay, TemperatureHumiditySensorDisplay,
TemperatureHumiditySensorOutdoor, TemperatureHumiditySensorOutdoor,
@ -143,14 +141,7 @@ async def async_setup_entry(
), ),
): ):
entities.append(HomematicipIlluminanceSensor(hap, device)) entities.append(HomematicipIlluminanceSensor(hap, device))
if isinstance( if isinstance(device, SwitchMeasuring):
device,
(
PlugableSwitchMeasuring,
BrandSwitchMeasuring,
FullFlushSwitchMeasuring,
),
):
entities.append(HomematicipPowerSensor(hap, device)) entities.append(HomematicipPowerSensor(hap, device))
entities.append(HomematicipEnergySensor(hap, device)) entities.append(HomematicipEnergySensor(hap, device))
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)): if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):

View File

@ -4,20 +4,19 @@ from __future__ import annotations
from typing import Any from typing import Any
from homematicip.base.enums import DeviceType
from homematicip.device import ( from homematicip.device import (
BrandSwitch2, BrandSwitch2,
BrandSwitchMeasuring,
DinRailSwitch, DinRailSwitch,
DinRailSwitch4, DinRailSwitch4,
FullFlushInputSwitch, FullFlushInputSwitch,
FullFlushSwitchMeasuring,
HeatingSwitch2, HeatingSwitch2,
MultiIOBox, MultiIOBox,
OpenCollector8Module, OpenCollector8Module,
PlugableSwitch, PlugableSwitch,
PlugableSwitchMeasuring,
PrintedCircuitBoardSwitch2, PrintedCircuitBoardSwitch2,
PrintedCircuitBoardSwitchBattery, PrintedCircuitBoardSwitchBattery,
SwitchMeasuring,
WiredSwitch8, WiredSwitch8,
) )
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
@ -43,12 +42,10 @@ async def async_setup_entry(
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup)) if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
] ]
for device in hap.home.devices: for device in hap.home.devices:
if isinstance(device, BrandSwitchMeasuring): if (
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring isinstance(device, SwitchMeasuring)
# This entity is implemented in the light platform and will and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING
# not be added in the switch platform ):
pass
elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)):
entities.append(HomematicipSwitchMeasuring(hap, device)) entities.append(HomematicipSwitchMeasuring(hap, device))
elif isinstance(device, WiredSwitch8): elif isinstance(device, WiredSwitch8):
entities.extend( entities.extend(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise", "documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pydrawise"], "loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.3.0"] "requirements": ["pydrawise==2025.6.0"]
} }

View File

@ -8,5 +8,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioimmich"], "loggers": ["aioimmich"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aioimmich==0.6.0"] "requirements": ["aioimmich==0.9.1"]
} }

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from logging import getLogger from logging import getLogger
import mimetypes
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.exceptions import ImmichError from aioimmich.exceptions import ImmichError
@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
async def async_get_media_source(hass: HomeAssistant) -> MediaSource: async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Immich media source.""" """Set up Immich media source."""
entries = hass.config_entries.async_entries(
DOMAIN, include_disabled=False, include_ignore=False
)
hass.http.register_view(ImmichMediaView(hass)) hass.http.register_view(ImmichMediaView(hass))
return ImmichMediaSource(hass, entries) return ImmichMediaSource(hass)
class ImmichMediaSourceIdentifier: class ImmichMediaSourceIdentifier:
@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None: def __init__(self, identifier: str) -> None:
"""Split identifier into parts.""" """Split identifier into parts."""
parts = identifier.split("/") parts = identifier.split("|")
# coonfig_entry.unique_id/album_id/asset_it/filename # config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
self.unique_id = parts[0] self.unique_id = parts[0]
self.album_id = parts[1] if len(parts) > 1 else None self.collection = parts[1] if len(parts) > 1 else None
self.asset_id = parts[2] if len(parts) > 2 else None self.collection_id = parts[2] if len(parts) > 2 else None
self.file_name = parts[3] if len(parts) > 2 else None self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None
self.mime_type = parts[5] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource): class ImmichMediaSource(MediaSource):
@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
name = "Immich" name = "Immich"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize Immich media source.""" """Initialize Immich media source."""
super().__init__(DOMAIN) super().__init__(DOMAIN)
self.hass = hass self.hass = hass
self.entries = entries
async def async_browse_media( async def async_browse_media(
self, self,
item: MediaSourceItem, item: MediaSourceItem,
) -> BrowseMediaSource: ) -> BrowseMediaSource:
"""Return media.""" """Return media."""
if not self.hass.config_entries.async_loaded_entries(DOMAIN): if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
raise BrowseError("Immich is not configured") raise BrowseError("Immich is not configured")
return BrowseMediaSource( return BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
can_expand=True, can_expand=True,
children_media_class=MediaClass.DIRECTORY, children_media_class=MediaClass.DIRECTORY,
children=[ children=[
*await self._async_build_immich(item), *await self._async_build_immich(item, entries),
], ],
) )
async def _async_build_immich( async def _async_build_immich(
self, item: MediaSourceItem self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]: ) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances.""" """Handle browsing different immich instances."""
if not item.identifier: if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [ return [
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
can_play=False, can_play=False,
can_expand=True, can_expand=True,
) )
for entry in self.entries for entry in entries
] ]
identifier = ImmichMediaSourceIdentifier(item.identifier) identifier = ImmichMediaSourceIdentifier(item.identifier)
entry: ImmichConfigEntry | None = ( entry: ImmichConfigEntry | None = (
@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
assert entry assert entry
immich_api = entry.runtime_data.api immich_api = entry.runtime_data.api
if identifier.album_id is None: if identifier.collection is None:
# Get Albums LOGGER.debug("Render all collections for %s", entry.title)
return [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|albums",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="albums",
can_play=False,
can_expand=True,
)
]
if identifier.collection_id is None:
LOGGER.debug("Render all albums for %s", entry.title)
try: try:
albums = await immich_api.albums.async_get_all_albums() albums = await immich_api.albums.async_get_all_albums()
except ImmichError: except ImmichError:
@ -118,80 +130,85 @@ class ImmichMediaSource(MediaSource):
return [ return [
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}", identifier=f"{identifier.unique_id}|albums|{album.album_id}",
media_class=MediaClass.DIRECTORY, media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE, media_content_type=MediaClass.IMAGE,
title=album.name, title=album.album_name,
can_play=False, can_play=False,
can_expand=True, can_expand=True,
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail", thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
) )
for album in albums for album in albums
] ]
# Request items of album LOGGER.debug(
"Render all assets of album %s for %s",
identifier.collection_id,
entry.title,
)
try: try:
album_info = await immich_api.albums.async_get_album_info( album_info = await immich_api.albums.async_get_album_info(
identifier.album_id identifier.collection_id
) )
except ImmichError: except ImmichError:
return [] return []
ret = [ ret: list[BrowseMediaSource] = []
BrowseMediaSource( for asset in album_info.assets:
domain=DOMAIN, if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
identifier=( ("image/", "video/")
f"{identifier.unique_id}/" ):
f"{identifier.album_id}/" continue
f"{asset.asset_id}/"
f"{asset.file_name}"
),
media_class=MediaClass.IMAGE,
media_content_type=asset.mime_type,
title=asset.file_name,
can_play=False,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
)
for asset in album_info.assets
if asset.mime_type.startswith("image/")
]
ret.extend( if mime_type.startswith("image/"):
BrowseMediaSource( media_class = MediaClass.IMAGE
domain=DOMAIN, can_play = False
identifier=( thumb_mime_type = mime_type
f"{identifier.unique_id}/" else:
f"{identifier.album_id}/" media_class = MediaClass.VIDEO
f"{asset.asset_id}/" can_play = True
f"{asset.file_name}" thumb_mime_type = "image/jpeg"
),
media_class=MediaClass.VIDEO, ret.append(
media_content_type=asset.mime_type, BrowseMediaSource(
title=asset.file_name, domain=DOMAIN,
can_play=True, identifier=(
can_expand=False, f"{identifier.unique_id}|albums|"
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail", f"{identifier.collection_id}|"
f"{asset.asset_id}|"
f"{asset.original_file_name}|"
f"{mime_type}"
),
media_class=media_class,
media_content_type=mime_type,
title=asset.original_file_name,
can_play=can_play,
can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}",
)
) )
for asset in album_info.assets
if asset.mime_type.startswith("video/")
)
return ret return ret
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url.""" """Resolve media to a url."""
identifier = ImmichMediaSourceIdentifier(item.identifier) try:
if identifier.file_name is None: identifier = ImmichMediaSourceIdentifier(item.identifier)
raise Unresolvable("No file name") except IndexError as err:
mime_type, _ = mimetypes.guess_type(identifier.file_name) raise Unresolvable(
if not isinstance(mime_type, str): f"Could not parse identifier: {item.identifier}"
raise Unresolvable("No file extension") ) from err
if identifier.mime_type is None:
raise Unresolvable(
f"Could not resolve identifier that has no mime-type: {item.identifier}"
)
return PlayMedia( return PlayMedia(
( (
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize" f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
), ),
mime_type, identifier.mime_type,
) )
@ -212,10 +229,10 @@ class ImmichMediaView(HomeAssistantView):
if not self.hass.config_entries.async_loaded_entries(DOMAIN): if not self.hass.config_entries.async_loaded_entries(DOMAIN):
raise HTTPNotFound raise HTTPNotFound
asset_id, file_name, size = location.split("/") try:
mime_type, _ = mimetypes.guess_type(file_name) asset_id, size, mime_type_base, mime_type_format = location.split("/")
if not isinstance(mime_type, str): except ValueError as err:
raise HTTPNotFound raise HTTPNotFound from err
entry: ImmichConfigEntry | None = ( entry: ImmichConfigEntry | None = (
self.hass.config_entries.async_entry_for_domain_unique_id( self.hass.config_entries.async_entry_for_domain_unique_id(
@ -226,7 +243,7 @@ class ImmichMediaView(HomeAssistantView):
immich_api = entry.runtime_data.api immich_api = entry.runtime_data.api
# stream response for videos # stream response for videos
if mime_type.startswith("video/"): if mime_type_base == "video":
try: try:
resp = await immich_api.assets.async_play_video_stream(asset_id) resp = await immich_api.assets.async_play_video_stream(asset_id)
except ImmichError as exc: except ImmichError as exc:
@ -243,4 +260,4 @@ class ImmichMediaView(HomeAssistantView):
image = await immich_api.assets.async_view_asset(asset_id, size) image = await immich_api.assets.async_view_asset(asset_id, size)
except ImmichError as exc: except ImmichError as exc:
raise HTTPNotFound from exc raise HTTPNotFound from exc
return Response(body=image, content_type=mime_type) return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")

View File

@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import ( from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device, async_remove_stale_devices_links_keep_entity_device,
) )
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from .const import CONF_SOURCE_SENSOR from .const import CONF_SOURCE_SENSOR
@ -21,6 +23,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.options[CONF_SOURCE_SENSOR], entry.options[CONF_SOURCE_SENSOR],
) )
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_SOURCE_SENSOR]
),
source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True return True

View File

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyiskra"], "loggers": ["pyiskra"],
"requirements": ["pyiskra==0.1.15"] "requirements": ["pyiskra==0.1.21"]
} }

View File

@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
coordinator = JellyfinDataUpdateCoordinator( coordinator = JellyfinDataUpdateCoordinator(
hass, entry, client, server_info, user_id hass, entry, client, server_info, user_id
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
)
entry.runtime_data = coordinator entry.runtime_data = coordinator
entry.async_on_unload(client.stop) entry.async_on_unload(client.stop)

View File

@ -4,10 +4,10 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_NAME, DOMAIN from .const import DOMAIN
from .coordinator import JellyfinDataUpdateCoordinator from .coordinator import JellyfinDataUpdateCoordinator
@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
"""Initialize the Jellyfin entity.""" """Initialize the Jellyfin entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, coordinator.server_id)}, identifiers={(DOMAIN, coordinator.server_id)},
manufacturer=DEFAULT_NAME,
name=coordinator.server_name,
sw_version=coordinator.server_version,
) )

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
"iot_class": "calculated", "iot_class": "calculated",
"loggers": ["hdate"], "loggers": ["hdate"],
"requirements": ["hdate[astral]==1.1.0"], "requirements": ["hdate[astral]==1.1.1"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -225,7 +225,7 @@ async def async_setup_entry(
JewishCalendarTimeSensor(config_entry, description) JewishCalendarTimeSensor(config_entry, description)
for description in TIME_SENSORS for description in TIME_SENSORS
) )
async_add_entities(sensors) async_add_entities(sensors, update_before_add=True)
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
async def async_added_to_hass(self) -> None: async def async_update(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
await self.async_update_data()
async def async_update_data(self) -> None:
"""Update the state of the sensor.""" """Update the state of the sensor."""
now = dt_util.now() now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location) _LOGGER.debug("Now: %s Location: %r", now, self.data.location)

View File

@ -11,8 +11,9 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP, SERVICE_VOLUME_UP,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
DOMAIN = "keyboard" DOMAIN = "keyboard"
@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Listen for keyboard events.""" """Listen for keyboard events."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Keyboard",
},
)
keyboard = PyKeyboard() keyboard = PyKeyboard()
keyboard.special_key_assignment() keyboard.special_key_assignment()

View File

@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1) SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5) SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15) STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.6"] "requirements": ["pylamarzocco==2.0.8"]
} }

View File

@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_on", key="prebrew_on",
translation_key="prebrew_time_on", translation_key="prebrew_time_on",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,
@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
key="prebrew_off", key="prebrew_off",
translation_key="prebrew_time_off", translation_key="prebrew_time_off",
device_class=NumberDeviceClass.DURATION, device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.SECONDS,
native_step=PRECISION_TENTHS, native_step=PRECISION_TENTHS,
native_min_value=0, native_min_value=0,
native_max_value=10, native_max_value=10,

View File

@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle Zeroconf discovery.""" """Handle Zeroconf discovery."""
# Do not probe the device if the host is already configured
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
session: ClientSession = await async_get_client_session(self.hass) session: ClientSession = await async_get_client_session(self.hass)
bridge: LinkPlayBridge | None = None bridge: LinkPlayBridge | None = None

View File

@ -7,6 +7,6 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["linkplay"], "loggers": ["linkplay"],
"requirements": ["python-linkplay==0.2.8"], "requirements": ["python-linkplay==0.2.11"],
"zeroconf": ["_linkplay._tcp.local."] "zeroconf": ["_linkplay._tcp.local."]
} }

View File

@ -7,8 +7,9 @@ import time
import lirc import lirc
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LIRC capability.""" """Set up the LIRC capability."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "LIRC",
},
)
# blocking=True gives unexpected behavior (multiple responses for 1 press) # blocking=True gives unexpected behavior (multiple responses for 1 press)
# also by not blocking, we allow hass to shut down the thread gracefully # also by not blocking, we allow hass to shut down the thread gracefully
# on exit. # on exit.

View File

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

View File

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

View File

@ -24,3 +24,11 @@ async def async_get_auth_implementation(
token_url=OAUTH2_TOKEN, token_url=OAUTH2_TOKEN,
), ),
) )
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
"developer_dashboard_url": "https://developer.honeywellhome.com",
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
}

View File

@ -1,4 +1,7 @@
{ {
"application_credentials": {
"description": "To be able to log in to Honeywell Lyric the integration requires a client ID and secret. To acquire those, please follow the following steps.\n\n1. Go to the [Honeywell Lyric Developer Apps Dashboard]({developer_dashboard_url}).\n1. Sign up for a developer account if you don't have one yet. This is a separate account from your Honeywell account.\n1. Log in with your Honeywell Lyric developer account.\n1. Go to the **My Apps** section.\n1. Press the **CREATE NEW APP** button.\n1. Give the application a name of your choice.\n1. Set the **Callback URL** to `{redirect_url}`.\n1. Save your changes.\\n1. Copy the **Consumer Key** and paste it here as the **Client ID**, then copy the **Consumer Secret** and paste it here as the **Client Secret**."
},
"config": { "config": {
"step": { "step": {
"pick_implementation": { "pick_implementation": {
@ -9,7 +12,7 @@
"description": "The Lyric integration needs to re-authenticate your account." "description": "The Lyric integration needs to re-authenticate your account."
}, },
"oauth_discovery": { "oauth_discovery": {
"description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric." "description": "Home Assistant has found a Honeywell Lyric device on your network. Be aware that the setup of the Lyric integration is more complicated than other integrations. Press **Submit** to continue setting up Honeywell Lyric."
} }
}, },
"abort": { "abort": {

View File

@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty # don't discover this entry if the supported state list is empty
secondary_value_is_not=[], secondary_value_is_not=[],
), ),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MinPINCodeLength",
translation_key="min_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="MaxPINCodeLength",
translation_key="max_pin_code_length",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=None,
),
entity_class=MatterSensor,
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
),
MatterDiscoverySchema( MatterDiscoverySchema(
platform=Platform.SENSOR, platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription( entity_description=MatterSensorEntityDescription(
key="TargetPositionLiftPercent100ths", key="TargetPositionLiftPercent100ths",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
translation_key="window_covering_target_position", translation_key="window_covering_target_position",
measurement_to_ha=lambda x: round((10000 - x) / 100), measurement_to_ha=lambda x: round((10000 - x) / 100),
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,

View File

@ -390,12 +390,6 @@
"evse_user_max_charge_current": { "evse_user_max_charge_current": {
"name": "User max charge current" "name": "User max charge current"
}, },
"min_pin_code_length": {
"name": "Min PIN code length"
},
"max_pin_code_length": {
"name": "Max PIN code length"
},
"window_covering_target_position": { "window_covering_target_position": {
"name": "Target opening position" "name": "Target opening position"
} }

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