Compare commits

..

218 Commits

Author SHA1 Message Date
Franck Nijhof 25d1480f2a Hotfix ruff warnings 2025-06-13 18:09:05 +00:00
hahn-th 2175754a1f Fix throttling issue in HomematicIP Cloud (#146683)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-13 17:57:39 +00:00
Franck Nijhof df5f253146 Bump version to 2025.6.1 2025-06-13 17:20:24 +00:00
J. Nick Koston a017d9415b Bump aiodns to 3.5.0 (#146758) 2025-06-13 17:20:09 +00:00
Marc Mueller e89c3b1e92 Ignore lingering pycares shutdown thread (#146733) 2025-06-13 17:20:07 +00:00
Allen Porter d4ffeedc87 Partial revert of update to remote calendar to fix issue where calendar does not update (#146702)
Partial revert
2025-06-13 17:20:06 +00:00
Allen Porter cb74b2663f Revert scan interval change in local calendar (#146700) 2025-06-13 17:20:05 +00:00
tronikos 4ec711bd63 Fix opower to work with aiohttp>=3.12.7 by disabling cookie quoting (#146697)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-13 17:20:03 +00:00
Simone Chemelli e81c8ce44d Bump aioamazondevices to 3.1.2 (#146690) 2025-06-13 17:20:02 +00:00
Simone Chemelli c2cf348255 Filter speak notify entity for WHA devices in Alexa Devices (#146688) 2025-06-13 17:20:00 +00:00
Simon Lamon e048a3da38 Bump linkplay to v0.2.12 (#146669) 2025-06-13 17:19:59 +00:00
Tsvi Mostovicz 7cf3116f5b Bump hdate to 1.1.2 (#146659) 2025-06-13 17:19:58 +00:00
epenet 5cd7ea06ad Bump wakeonlan to 3.1.0 (#146655)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-13 17:19:56 +00:00
Simone Chemelli 52c62b31fd Fix cookies with aiohttp >= 3.12.7 for Vodafone Station (#146647) 2025-06-13 17:19:55 +00:00
Paul Bottein b2bb0aeb64 Update frontend to 20250531.3 (#146638) 2025-06-13 17:19:53 +00:00
Vasilis Valatsos f0fc87e2b6 Drop HostKeyAlgorithms in aruba (#146619) 2025-06-13 17:19:52 +00:00
epenet e7a88e99f9 Fix fan is_on status in xiaomi_miio (#146592) 2025-06-13 17:19:50 +00:00
Avi Miller c3e3a36b4c Fix palette handling for LIFX Ceiling SKY effect (#146582)
Signed-off-by: Avi Miller <me@dje.li>
2025-06-13 17:19:49 +00:00
Franck Nijhof 54d8d71de5 Bump version to 2025.6.0 2025-06-11 19:14:05 +00:00
Robert Resch fb4c77d43b Add aiofiles to pyproject.toml (#146561) 2025-06-11 18:39:53 +00:00
Franck Nijhof cada2f84a9 Hotfix ruff warnings 2025-06-11 18:13:03 +00:00
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 f96030a6c8dccca7888b6d1274d5ed3a251ac03c.
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
785 changed files with 10441 additions and 17083 deletions
+4 -4
View File
@@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v10
uses: dawidd6/action-download-artifact@v9
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -105,7 +105,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v10
uses: dawidd6/action-download-artifact@v9
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package
@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
+2 -2
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.7"
HA_SHORT_VERSION: "2025.6"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -360,7 +360,7 @@ jobs:
- name: Run ruff
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.19
uses: github/codeql-action/init@v3.28.18
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.19
uses: github/codeql-action/analyze@v3.28.18
with:
category: "/language:python"
+3 -3
View File
@@ -1,8 +1,8 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12
rev: v0.11.0
hooks:
- id: ruff-check
- id: ruff
args:
- --fix
- id: ruff-format
@@ -30,7 +30,7 @@ repos:
- --branch=master
- --branch=rc
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
rev: v1.35.1
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier
+1 -1
View File
@@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.alexa_devices.*
homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_devices.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.*
+1 -1
View File
@@ -45,7 +45,7 @@
{
"label": "Ruff",
"type": "shell",
"command": "pre-commit run ruff-check --all-files",
"command": "pre-commit run ruff --all-files",
"group": {
"kind": "test",
"isDefault": true
Generated
+2 -2
View File
@@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
/tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amazon_devices/ @chemelli74
/tests/components/amazon_devices/ @chemelli74
/homeassistant/components/alexa_devices/ @chemelli74
/tests/components/alexa_devices/ @chemelli74
/homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot
+4 -10
View File
@@ -171,6 +171,8 @@ FRONTEND_INTEGRATIONS = {
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
# The substage containing recorder should have no timeout, as it could cancel a database migration.
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
# The substages preceding it should also have no timeout, until we ensure that the recorder
# is not accidentally promoted as a dependency of any of the integrations in them.
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
STAGE_0_INTEGRATIONS = (
# Load logging and http deps as soon as possible
@@ -927,11 +929,7 @@ async def _async_set_up_integrations(
await _async_setup_multi_components(hass, stage_all_domains, config)
continue
try:
async with hass.timeout.async_timeout(
timeout,
cool_down=COOLDOWN_TIME,
cancel_message=f"Bootstrap stage {name} timeout",
):
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
await _async_setup_multi_components(hass, stage_all_domains, config)
except TimeoutError:
_LOGGER.warning(
@@ -943,11 +941,7 @@ async def _async_set_up_integrations(
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(
WRAP_UP_TIMEOUT,
cool_down=COOLDOWN_TIME,
cancel_message="Bootstrap startup wrap up timeout",
):
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done()
except TimeoutError:
_LOGGER.warning(
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Amazon",
"integrations": [
"alexa",
"amazon_devices",
"alexa_devices",
"amazon_polly",
"aws",
"aws_s3",
+73 -3
View File
@@ -14,24 +14,30 @@ from jaraco.abode.exceptions import (
)
from jaraco.abode.helpers.timeline import Groups as GROUPS
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DATE,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_TIME,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import CONF_POLLING, DOMAIN, LOGGER
from .services import async_setup_services
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_TYPE = "device_type"
@@ -39,12 +45,22 @@ ATTR_EVENT_CODE = "event_code"
ATTR_EVENT_NAME = "event_name"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_UTC = "event_utc"
ATTR_SETTING = "setting"
ATTR_USER_NAME = "user_name"
ATTR_APP_TYPE = "app_type"
ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
@@ -69,7 +85,7 @@ class AbodeSystem:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
async_setup_services(hass)
setup_hass_services(hass)
return True
@@ -122,6 +138,60 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
def setup_hass_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
def change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
hass.data[DOMAIN].abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
def capture_image(call: ServiceCall) -> None:
"""Capture a new image."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_camera_capture_{entity_id}"
dispatcher_send(hass, signal)
def trigger_automation(call: ServiceCall) -> None:
"""Trigger an Abode automation."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_trigger_automation_{entity_id}"
dispatcher_send(hass, signal)
hass.services.async_register(
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
)
async def setup_hass_events(hass: HomeAssistant) -> None:
"""Home Assistant start and stop callbacks."""
@@ -1,89 +0,0 @@
"""Support for the Abode Security System."""
from __future__ import annotations
from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, LOGGER
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_SETTING = "setting"
ATTR_VALUE = "value"
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def _change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
call.hass.data[DOMAIN].abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
def _capture_image(call: ServiceCall) -> None:
"""Capture a new image."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_camera_capture_{entity_id}"
dispatcher_send(call.hass, signal)
def _trigger_automation(call: ServiceCall) -> None:
"""Trigger an Abode automation."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_trigger_automation_{entity_id}"
dispatcher_send(call.hass, signal)
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
hass.services.async_register(
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_TRIGGER_AUTOMATION,
_trigger_automation,
schema=AUTOMATION_SCHEMA,
)
+1 -1
View File
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from .const import CONNECTION_TYPE, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
+1 -24
View File
@@ -41,30 +41,7 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Adax."""
try:
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
rooms = await self.adax_data_handler.fetch_rooms_info() or []
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
else:
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
rooms = []
if not rooms:
_LOGGER.debug(
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
)
rooms = await self.adax_data_handler.get_rooms() or []
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
if not rooms:
raise UpdateFailed("No rooms available from Adax API")
except OSError as e:
raise UpdateFailed(f"Error communicating with API: {e}") from e
for room in rooms:
room["energyWh"] = int(room.get("energyWh", 0))
rooms = await self.adax_data_handler.get_rooms() or []
return {r["id"]: r for r in rooms}
-77
View File
@@ -1,77 +0,0 @@
"""Support for Adax energy sensors."""
from __future__ import annotations
from typing import cast
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AdaxConfigEntry
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: AdaxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Adax energy sensors with config flow."""
if entry.data.get(CONNECTION_TYPE) != LOCAL:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
# Create individual energy sensors for each device
async_add_entities(
AdaxEnergySensor(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
)
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
"""Representation of an Adax energy sensor."""
_attr_has_entity_name = True
_attr_translation_key = "energy"
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_suggested_display_precision = 3
def __init__(
self,
coordinator: AdaxCloudCoordinator,
device_id: str,
) -> None:
"""Initialize the energy sensor."""
super().__init__(coordinator)
self._device_id = device_id
room = coordinator.data[device_id]
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=room["name"],
manufacturer="Adax",
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available and "energyWh" in self.coordinator.data[self._device_id]
)
@property
def native_value(self) -> int:
"""Return the native value of the sensor."""
return int(self.coordinator.data[self._device_id]["energyWh"])
+2 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
)
from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
identifiers={(AGENT_DOMAIN, self.unique_id)},
manufacturer="Agent",
model="Camera",
name=f"{device.client.name} {device.name}",
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.6"]
"requirements": ["aioairq==0.4.4"]
}
+19 -5
View File
@@ -5,22 +5,23 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
from .const import CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
@@ -31,8 +32,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
async def _update_method() -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
@@ -1,36 +0,0 @@
"""The Airthings integration."""
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,
)
self.airthings = airthings
async def _update_method(self) -> dict[str, AirthingsDevice]:
"""Get the latest data from Airthings."""
try:
return await self.airthings.update_devices() # type: ignore[no-any-return]
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
+3 -11
View File
@@ -19,7 +19,6 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -28,9 +27,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirthingsConfigEntry
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
from .const import DOMAIN
from .coordinator import AirthingsDataUpdateCoordinator
SENSORS: dict[str, SensorEntityDescription] = {
"radonShortTermAvg": SensorEntityDescription(
@@ -56,12 +54,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"sla": SensorEntityDescription(
key="sla",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
),
"battery": SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
@@ -148,7 +140,7 @@ async def async_setup_entry(
class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
):
"""Representation of a Airthings Sensor device."""
@@ -157,7 +149,7 @@ class AirthingsHeaterEnergySensor(
def __init__(
self,
coordinator: AirthingsDataUpdateCoordinator,
coordinator: AirthingsDataCoordinatorType,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.3.0"]
"requirements": ["airtouch5py==0.2.11"]
}
@@ -1,4 +1,4 @@
"""Amazon Devices integration."""
"""Alexa Devices integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -13,7 +13,7 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Amazon Devices platform."""
"""Set up Alexa Devices platform."""
coordinator = AmazonDevicesCoordinator(hass, entry)
@@ -26,7 +26,7 @@ PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Amazon Devices binary sensor entity description."""
"""Alexa Devices binary sensor entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
@@ -52,7 +52,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> 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
@@ -1,4 +1,4 @@
"""Config flow for Amazon Devices integration."""
"""Config flow for Alexa Devices integration."""
from __future__ import annotations
@@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, 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(
self, user_input: dict[str, Any] | None = None
@@ -1,8 +1,8 @@
"""Amazon Devices constants."""
"""Alexa Devices constants."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "amazon_devices"
DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data"
@@ -1,4 +1,4 @@
"""Support for Amazon Devices."""
"""Support for Alexa Devices."""
from datetime import timedelta
@@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Base coordinator for Amazon Devices."""
"""Base coordinator for Alexa Devices."""
config_entry: AmazonConfigEntry
@@ -1,4 +1,4 @@
"""Diagnostics support for Amazon Devices integration."""
"""Diagnostics support for Alexa Devices integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Defines a base Amazon Devices entity."""
"""Defines a base Alexa Devices entity."""
from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SPEAKER_GROUP_MODEL
@@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines a base Amazon Devices entity."""
"""Defines a base Alexa Devices entity."""
_attr_has_entity_name = True
@@ -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.1.2"]
}
@@ -7,6 +7,7 @@ from dataclasses import dataclass
from typing import Any, Final
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
from homeassistant.core import HomeAssistant
@@ -20,8 +21,9 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonNotifyEntityDescription(NotifyEntityDescription):
"""Amazon Devices notify entity description."""
"""Alexa Devices notify entity description."""
is_supported: Callable[[AmazonDevice], bool] = lambda _device: True
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
subkey: str
@@ -31,6 +33,7 @@ NOTIFY: Final = (
key="speak",
translation_key="speak",
subkey="AUDIO_PLAYER",
is_supported=lambda _device: _device.device_family != SPEAKER_GROUP_FAMILY,
method=lambda api, device, message: api.call_alexa_speak(device, message),
),
AmazonNotifyEntityDescription(
@@ -49,7 +52,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> 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
@@ -58,6 +61,7 @@ async def async_setup_entry(
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
@@ -45,7 +45,9 @@ rules:
discovery-update-info:
status: exempt
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-examples: todo
docs-known-limitations: todo
@@ -12,16 +12,16 @@
"step": {
"user": {
"data": {
"country": "[%key:component::amazon_devices::common::data_country%]",
"country": "[%key:component::alexa_devices::common::data_country%]",
"username": "[%key:common::config_flow::data::username%]",
"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": {
"country": "[%key:component::amazon_devices::common::data_description_country%]",
"username": "[%key:component::amazon_devices::common::data_description_username%]",
"password": "[%key:component::amazon_devices::common::data_description_password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
"country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
}
}
},
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonSwitchEntityDescription(SwitchEntityDescription):
"""Amazon Devices switch entity description."""
"""Alexa Devices switch entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
subkey: str
@@ -43,7 +43,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> 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
@@ -1,122 +0,0 @@
{
"domain": "amazon_devices",
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "007147*" },
{ "macaddress": "00FC8B*" },
{ "macaddress": "0812A5*" },
{ "macaddress": "086AE5*" },
{ "macaddress": "08849D*" },
{ "macaddress": "089115*" },
{ "macaddress": "08A6BC*" },
{ "macaddress": "08C224*" },
{ "macaddress": "0CDC91*" },
{ "macaddress": "0CEE99*" },
{ "macaddress": "1009F9*" },
{ "macaddress": "109693*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "10CE02*" },
{ "macaddress": "140AC5*" },
{ "macaddress": "149138*" },
{ "macaddress": "1848BE*" },
{ "macaddress": "1C12B0*" },
{ "macaddress": "1C4D66*" },
{ "macaddress": "1C93C4*" },
{ "macaddress": "1CFE2B*" },
{ "macaddress": "244CE3*" },
{ "macaddress": "24CE33*" },
{ "macaddress": "2873F6*" },
{ "macaddress": "2C71FF*" },
{ "macaddress": "34AFB3*" },
{ "macaddress": "34D270*" },
{ "macaddress": "38F73D*" },
{ "macaddress": "3C5CC4*" },
{ "macaddress": "3CE441*" },
{ "macaddress": "440049*" },
{ "macaddress": "40A2DB*" },
{ "macaddress": "40A9CF*" },
{ "macaddress": "40B4CD*" },
{ "macaddress": "443D54*" },
{ "macaddress": "44650D*" },
{ "macaddress": "485F2D*" },
{ "macaddress": "48785E*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "4CEFC0*" },
{ "macaddress": "5007C3*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "50F5DA*" },
{ "macaddress": "5C415A*" },
{ "macaddress": "6837E9*" },
{ "macaddress": "6854FD*" },
{ "macaddress": "689A87*" },
{ "macaddress": "68B691*" },
{ "macaddress": "68DBF5*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "6C5697*" },
{ "macaddress": "7458F3*" },
{ "macaddress": "74C246*" },
{ "macaddress": "74D637*" },
{ "macaddress": "74E20C*" },
{ "macaddress": "74ECB2*" },
{ "macaddress": "786C84*" },
{ "macaddress": "78A03F*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "7C6305*" },
{ "macaddress": "7CD566*" },
{ "macaddress": "8871E5*" },
{ "macaddress": "901195*" },
{ "macaddress": "90235B*" },
{ "macaddress": "90A822*" },
{ "macaddress": "90F82E*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "98CCF3*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A002DC*" },
{ "macaddress": "A0D2B1*" },
{ "macaddress": "A40801*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "AC416A*" },
{ "macaddress": "AC63BE*" },
{ "macaddress": "ACCCFC*" },
{ "macaddress": "B0739C*" },
{ "macaddress": "B0CFCB*" },
{ "macaddress": "B0F7C4*" },
{ "macaddress": "B85F98*" },
{ "macaddress": "C091B9*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "C49500*" },
{ "macaddress": "C86C3D*" },
{ "macaddress": "CC9EA2*" },
{ "macaddress": "CCF735*" },
{ "macaddress": "DC54D7*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "D8FBD6*" },
{ "macaddress": "DC91BF*" },
{ "macaddress": "DCA0D0*" },
{ "macaddress": "E0F728*" },
{ "macaddress": "EC2BEB*" },
{ "macaddress": "EC8AC4*" },
{ "macaddress": "ECA138*" },
{ "macaddress": "F02F9E*" },
{ "macaddress": "F0272D*" },
{ "macaddress": "F0F0A4*" },
{ "macaddress": "F4032A*" },
{ "macaddress": "F854B8*" },
{ "macaddress": "FC492D*" },
{ "macaddress": "FC65DE*" },
{ "macaddress": "FCA183*" },
{ "macaddress": "FCE9D8*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.0.6"]
}
+50 -4
View File
@@ -16,7 +16,10 @@ from amcrest import AmcrestError, ApiWrapper, LoginError
import httpx
import voluptuous as vol
from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_AUTHENTICATION,
CONF_BINARY_SENSORS,
CONF_HOST,
@@ -27,17 +30,21 @@ from homeassistant.const import (
CONF_SENSORS,
CONF_SWITCHES,
CONF_USERNAME,
ENTITY_MATCH_ALL,
ENTITY_MATCH_NONE,
HTTP_BASIC_AUTHENTICATION,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.service import async_extract_entity_ids
from homeassistant.helpers.typing import ConfigType
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
from .camera import STREAM_SOURCE_LIST
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .const import (
CAMERAS,
COMM_RETRIES,
@@ -51,7 +58,6 @@ from .const import (
)
from .helpers import service_signal
from .sensor import SENSOR_KEYS
from .services import async_setup_services
from .switch import SWITCH_KEYS
_LOGGER = logging.getLogger(__name__)
@@ -449,7 +455,47 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not hass.data[DATA_AMCREST][DEVICES]:
return False
async_setup_services(hass)
def have_permission(user: User | None, entity_id: str) -> bool:
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
async def async_extract_from_service(call: ServiceCall) -> list[str]:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call: ServiceCall) -> None:
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
return True
@@ -1,61 +0,0 @@
"""Support for Amcrest IP cameras."""
from __future__ import annotations
from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
from .camera import CAMERA_SERVICES
from .const import CAMERAS, DATA_AMCREST, DOMAIN
from .helpers import service_signal
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Amcrest IP Camera services."""
def have_permission(user: User | None, entity_id: str) -> bool:
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
async def async_extract_from_service(call: ServiceCall) -> list[str]:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call: ServiceCall) -> None:
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
get_instance as get_recorder_instance,
)
from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -225,8 +225,7 @@ class Analytics:
LOGGER.error(err)
return
configuration_set = _domains_from_yaml_config(yaml_configuration)
configuration_set = set(yaml_configuration)
er_platforms = {
entity.platform
for entity in ent_reg.entities.values()
@@ -371,13 +370,3 @@ class Analytics:
for entry in entries
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
)
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
"""Extract domains from the YAML configuration."""
domains = set(yaml_configuration)
for platforms in conf_util.extract_platform_integrations(
yaml_configuration, BASE_PLATFORMS
).values():
domains.update(platforms)
return domains
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.6.0"]
"requirements": ["apsystems-ez1==2.7.0"]
}
@@ -89,7 +89,7 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self) -> dict[str, dict[str, str]] | None:
"""Retrieve data from Aruba Access Point and return parsed result."""
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
connect = f"ssh {self.username}@{self.host}"
ssh: pexpect.spawn[str] = pexpect.spawn(connect, encoding="utf-8")
query = ssh.expect(
[
@@ -1207,15 +1207,6 @@ class PipelineRun:
self._streamed_response_text = True
self.process_event(
PipelineEvent(
PipelineEventType.INTENT_PROGRESS,
{
"tts_start_streaming": True,
},
)
)
async def tts_input_stream_generator() -> AsyncGenerator[str]:
"""Yield TTS input stream."""
while (tts_input := await tts_input_stream.get()) is not None:
@@ -6,7 +6,6 @@ from homeassistant.components.water_heater import (
STATE_ECO,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant
@@ -33,7 +32,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
"""Representation of an ATAG water heater."""
_attr_operation_list = OPERATION_LIST
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@property
+2 -2
View File
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import ATTR_MANUFACTURER, DOMAIN
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
from .config import AxisConfig
from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource
@@ -79,7 +79,7 @@ class AxisHub:
config_entry_id=self.config.entry.entry_id,
configuration_url=self.api.config.url,
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
identifiers={(DOMAIN, self.unique_id)},
identifiers={(AXIS_DOMAIN, self.unique_id)},
manufacturer=ATTR_MANUFACTURER,
model=f"{self.config.model} {self.product_type}",
name=self.config.name,
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==2.1.0"]
"requirements": ["python-bsblan==1.2.1"]
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.8.0"]
"requirements": ["PyTurboJPEG==1.7.5"]
}
+13 -24
View File
@@ -3,8 +3,7 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
import logging
import socket
@@ -27,18 +26,8 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE
_LOGGER = logging.getLogger(__name__)
type CloudflareConfigEntry = ConfigEntry[CloudflareRuntimeData]
@dataclass
class CloudflareRuntimeData:
"""Runtime data for Cloudflare config entry."""
client: pycfdns.Client
dns_zone: pycfdns.ZoneModel
async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cloudflare from a config entry."""
session = async_get_clientsession(hass)
client = pycfdns.Client(
@@ -56,12 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
except pycfdns.ComunicationException as error:
raise ConfigEntryNotReady from error
entry.runtime_data = CloudflareRuntimeData(client, dns_zone)
async def update_records(now: datetime) -> None:
async def update_records(now):
"""Set up recurring update."""
try:
await _async_update_cloudflare(hass, entry)
await _async_update_cloudflare(
hass, client, dns_zone, entry.data[CONF_RECORDS]
)
except (
pycfdns.AuthenticationException,
pycfdns.ComunicationException,
@@ -71,7 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
async def update_records_service(call: ServiceCall) -> None:
"""Set up service for manual trigger."""
try:
await _async_update_cloudflare(hass, entry)
await _async_update_cloudflare(
hass, client, dns_zone, entry.data[CONF_RECORDS]
)
except (
pycfdns.AuthenticationException,
pycfdns.ComunicationException,
@@ -88,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -
return True
async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Cloudflare config entry."""
return True
@@ -96,12 +87,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry)
async def _async_update_cloudflare(
hass: HomeAssistant,
entry: CloudflareConfigEntry,
client: pycfdns.Client,
dns_zone: pycfdns.ZoneModel,
target_records: list[str],
) -> None:
client = entry.runtime_data.client
dns_zone = entry.runtime_data.dns_zone
target_records: list[str] = entry.data[CONF_RECORDS]
_LOGGER.debug("Starting update for zone %s", dns_zone["name"])
records = await client.list_dns_records(zone_id=dns_zone["id"], type="A")
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/compensation",
"iot_class": "calculated",
"quality_scale": "legacy",
"requirements": ["numpy==2.3.0"]
"requirements": ["numpy==2.2.2"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
}
+3 -3
View File
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import CONF_GESTURE, DOMAIN
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
from .device_trigger import (
CONF_BOTH_BUTTONS,
@@ -200,6 +200,6 @@ def async_describe_events(
}
async_describe_event(
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
)
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
@@ -2,19 +2,15 @@
from __future__ import annotations
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from .const import DOMAIN
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Derivative from a config entry."""
@@ -29,19 +25,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options={**entry.options, CONF_SOURCE: source_entity_id},
)
entity_registry = er.async_get(hass)
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,
get_helper_entity_id=lambda: entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, 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,))
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.4.0"]
"requirements": ["aiodns==3.5.0"]
}
+3 -4
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
from datetime import timedelta
from ipaddress import IPv4Address, IPv6Address
import logging
from typing import Literal
import aiodns
from aiodns.error import DNSError
@@ -35,7 +34,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=120)
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
def sort_ips(ips: list, querytype: str) -> list:
"""Join IPs into a single string."""
if querytype == "AAAA":
@@ -90,7 +89,7 @@ class WanIpSensor(SensorEntity):
self.hostname = hostname
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
self.resolver.nameservers = [resolver]
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
self.querytype = "AAAA" if ipv6 else "A"
self._retries = DEFAULT_RETRIES
self._attr_extra_state_attributes = {
"resolver": resolver,
@@ -107,7 +106,7 @@ class WanIpSensor(SensorEntity):
async def async_update(self) -> None:
"""Get the current DNS IP address for hostname."""
try:
response = await self.resolver.query(self.hostname, self.querytype)
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err)
response = None
@@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import _LOGGER, CONF_DOWNLOAD_DIR
from .services import async_setup_services
from .services import register_services
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -25,6 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return False
async_setup_services(hass)
register_services(hass)
return True
@@ -141,7 +141,7 @@ def download_file(service: ServiceCall) -> None:
threading.Thread(target=do_download).start()
def async_setup_services(hass: HomeAssistant) -> None:
def register_services(hass: HomeAssistant) -> None:
"""Register the services for the downloader component."""
async_register_admin_service(
hass,
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
}
+65 -5
View File
@@ -8,7 +8,7 @@ import re
from typing import Any
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk
from elkm1_lib.elk import Elk, Panel
from elkm1_lib.util import parse_url
import voluptuous as vol
@@ -26,11 +26,12 @@ from homeassistant.const import (
Platform,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.network import is_ip_address
from .const import (
@@ -61,7 +62,6 @@ from .discovery import (
async_update_entry_from_discovery,
)
from .models import ELKM1Data
from .services import async_setup_services
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
@@ -79,6 +79,19 @@ PLATFORMS = [
Platform.SWITCH,
]
SPEAK_SERVICE_SCHEMA = vol.Schema(
{
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
vol.Optional("prefix", default=""): cv.string,
}
)
SET_TIME_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("prefix", default=""): cv.string,
}
)
def hostname_from_url(url: str) -> str:
"""Return the hostname from a url."""
@@ -166,7 +179,7 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
async_setup_services(hass)
_create_elk_services(hass)
async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
@@ -313,6 +326,17 @@ def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
"""Search all config entries for a given prefix."""
for entry in hass.config_entries.async_entries(DOMAIN):
if not entry.runtime_data:
continue
elk_data: ELKM1Data = entry.runtime_data
if elk_data.prefix == prefix:
return elk_data.elk
return None
async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -366,3 +390,39 @@ async def async_wait_for_elk_to_sync(
_LOGGER.debug("Received %s event", name)
return success
@callback
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
"""Get the ElkM1 panel from a service call."""
prefix = service.data["prefix"]
elk = _find_elk_by_prefix(hass, prefix)
if elk is None:
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
return elk.panel
def _create_elk_services(hass: HomeAssistant) -> None:
"""Create ElkM1 services."""
@callback
def _speak_word_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
@callback
def _speak_phrase_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
@callback
def _set_time_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).set_time(dt_util.now())
hass.services.async_register(
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
)
@@ -1,77 +0,0 @@
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
from __future__ import annotations
from elkm1_lib.elk import Elk, Panel
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .models import ELKM1Data
SPEAK_SERVICE_SCHEMA = vol.Schema(
{
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
vol.Optional("prefix", default=""): cv.string,
}
)
SET_TIME_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("prefix", default=""): cv.string,
}
)
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
"""Search all config entries for a given prefix."""
for entry in hass.config_entries.async_entries(DOMAIN):
if not entry.runtime_data:
continue
elk_data: ELKM1Data = entry.runtime_data
if elk_data.prefix == prefix:
return elk_data.elk
return None
@callback
def _async_get_elk_panel(service: ServiceCall) -> Panel:
"""Get the ElkM1 panel from a service call."""
prefix = service.data["prefix"]
elk = _find_elk_by_prefix(service.hass, prefix)
if elk is None:
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
return elk.panel
@callback
def _speak_word_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).speak_word(service.data["number"])
@callback
def _speak_phrase_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).speak_phrase(service.data["number"])
@callback
def _set_time_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).set_time(dt_util.now())
def async_setup_services(hass: HomeAssistant) -> None:
"""Create ElkM1 services."""
hass.services.async_register(
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
)
@@ -2,6 +2,7 @@
from __future__ import annotations
import httpx
from pyenphase import Envoy
from homeassistant.config_entries import ConfigEntry
@@ -9,9 +10,14 @@ from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN, PLATFORMS
from .const import (
DOMAIN,
OPTION_DISABLE_KEEP_ALIVE,
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
PLATFORMS,
)
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
@@ -19,8 +25,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
"""Set up Enphase Envoy from a config entry."""
host = entry.data[CONF_HOST]
session = async_create_clientsession(hass, verify_ssl=False)
envoy = Envoy(host, session)
options = entry.options
envoy = (
Envoy(
host,
httpx.AsyncClient(
verify=False, limits=httpx.Limits(max_keepalive_connections=0)
),
)
if options.get(
OPTION_DISABLE_KEEP_ALIVE, OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE
)
else Envoy(host, get_async_client(hass, verify_ssl=False))
)
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
await coordinator.async_config_entry_first_refresh()
@@ -24,7 +24,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.typing import VolDictType
@@ -63,7 +63,7 @@ async def validate_input(
description_placeholders: dict[str, str],
) -> Envoy:
"""Validate the user input allows us to connect."""
envoy = Envoy(host, async_get_clientsession(hass, verify_ssl=False))
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
try:
await envoy.setup()
await envoy.authenticate(username=username, password=password)
@@ -6,7 +6,6 @@ import copy
from datetime import datetime
from typing import TYPE_CHECKING, Any
from aiohttp import ClientResponse
from attr import asdict
from pyenphase.envoy import Envoy
from pyenphase.exceptions import EnvoyError
@@ -70,14 +69,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
for end_point in end_points:
try:
response: ClientResponse = await envoy.request(end_point)
fixture_data[end_point] = (
(await response.text()).replace("\n", "").replace(serial, CLEAN_TEXT)
response = await envoy.request(end_point)
fixture_data[end_point] = response.text.replace("\n", "").replace(
serial, CLEAN_TEXT
)
fixture_data[f"{end_point}_log"] = json_dumps(
{
"headers": dict(response.headers.items()),
"code": response.status,
"code": response.status_code,
}
)
except EnvoyError as err:
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from aiohttp import ClientError
from httpx import HTTPError
from pyenphase import EnvoyData
from pyenphase.exceptions import EnvoyError
@@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import EnphaseUpdateCoordinator
ACTIONERRORS = (EnvoyError, ClientError)
ACTIONERRORS = (EnvoyError, HTTPError)
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.0.1"],
"requirements": ["pyenphase==1.26.1"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -71,11 +71,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
_attr_name = "DHW controller"
_attr_icon = "mdi:thermometer-lines"
_attr_operation_list = list(HA_STATE_TO_EVO)
_attr_supported_features = (
WaterHeaterEntityFeature.AWAY_MODE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_evo_device: evo.HotWater
@@ -96,6 +91,9 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
self._attr_precision = (
PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE
)
self._attr_supported_features = (
WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE
)
@property
def current_operation(self) -> str | None:
+41 -10
View File
@@ -11,25 +11,32 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
CONTENT_TYPE_MULTIPART,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.signal_type import SignalType
from homeassistant.util.system_info import is_official_image
from .const import (
DOMAIN,
SIGNAL_FFMPEG_RESTART,
SIGNAL_FFMPEG_START,
SIGNAL_FFMPEG_STOP,
)
from .services import async_setup_services
DOMAIN = "ffmpeg"
SERVICE_START = "start"
SERVICE_STOP = "stop"
SERVICE_RESTART = "restart"
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
DATA_FFMPEG = "ffmpeg"
@@ -56,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the FFmpeg component."""
@@ -65,7 +74,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await manager.async_get_version()
async_setup_services(hass)
# Register service
async def async_service_handle(service: ServiceCall) -> None:
"""Handle service ffmpeg process."""
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
if service.service == SERVICE_START:
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
elif service.service == SERVICE_STOP:
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
else:
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
hass.services.async_register(
DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
hass.data[DATA_FFMPEG] = manager
return True
-9
View File
@@ -1,9 +0,0 @@
"""Support for FFmpeg."""
from homeassistant.util.signal_type import SignalType
DOMAIN = "ffmpeg"
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
@@ -1,51 +0,0 @@
"""Support for FFmpeg."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
DOMAIN,
SIGNAL_FFMPEG_RESTART,
SIGNAL_FFMPEG_START,
SIGNAL_FFMPEG_STOP,
)
SERVICE_START = "start"
SERVICE_STOP = "stop"
SERVICE_RESTART = "restart"
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
async def _async_service_handle(service: ServiceCall) -> None:
"""Handle service ffmpeg process."""
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
if service.service == SERVICE_START:
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_START, entity_ids)
elif service.service == SERVICE_STOP:
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_STOP, entity_ids)
else:
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
def async_setup_services(hass: HomeAssistant) -> None:
"""Register FFmpeg services."""
hass.services.async_register(
DOMAIN, SERVICE_START, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_STOP, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_RESTART, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250531.0"]
"requirements": ["home-assistant-frontend==20250531.3"]
}
@@ -5,11 +5,18 @@ import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
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
DOMAIN = "generic_hygrostat"
@@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True
@@ -1,12 +1,16 @@
"""The generic_thermostat component."""
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 (
async_entity_id_to_device_id,
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:
@@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.entry_id,
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)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True
@@ -2,13 +2,21 @@
from __future__ import annotations
import dataclasses
import aiohttp
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
@@ -23,9 +31,21 @@ from .helpers import (
GoogleAssistantSDKConfigEntry,
GoogleAssistantSDKRuntimeData,
InMemoryStorage,
async_send_text_commands,
best_matching_language_code,
)
from .services import async_setup_services
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
{
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
),
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
},
)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -38,8 +58,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
async_setup_services(hass)
return True
@@ -63,6 +81,8 @@ async def async_setup_entry(
mem_storage = InMemoryStorage(hass)
hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage))
await async_setup_service(hass)
entry.runtime_data = GoogleAssistantSDKRuntimeData(
session=session, mem_storage=mem_storage
)
@@ -85,6 +105,36 @@ async def async_unload_entry(
return True
async def async_setup_service(hass: HomeAssistant) -> None:
"""Add the services for Google Assistant SDK."""
async def send_text_command(call: ServiceCall) -> ServiceResponse:
"""Send a text command to Google Assistant SDK."""
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
media_players: list[str] | None = call.data.get(
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
)
command_response_list = await async_send_text_commands(
hass, commands, media_players
)
if call.return_response:
return {
"responses": [
dataclasses.asdict(command_response)
for command_response in command_response_list
]
}
return None
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT_COMMAND,
send_text_command,
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
"""Google Assistant SDK conversation agent."""
@@ -1,61 +0,0 @@
"""Support for Google Assistant SDK."""
from __future__ import annotations
import dataclasses
import voluptuous as vol
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .helpers import async_send_text_commands
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
{
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
),
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
},
)
async def _send_text_command(call: ServiceCall) -> ServiceResponse:
"""Send a text command to Google Assistant SDK."""
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
media_players: list[str] | None = call.data.get(
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
)
command_response_list = await async_send_text_commands(
call.hass, commands, media_players
)
if call.return_response:
return {
"responses": [
dataclasses.asdict(command_response)
for command_response in command_response_list
]
}
return None
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Assistant SDK."""
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT_COMMAND,
_send_text_command,
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
@@ -24,11 +24,9 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Google Mail integration."""
"""Set up the Google Mail platform."""
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
await async_setup_services(hass)
return True
@@ -54,6 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
await async_setup_services(hass)
return True
@@ -7,26 +7,17 @@ from google_photos_library_api.api import GooglePhotosLibraryApi
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from . import api
from .const import DOMAIN
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
from .services import async_setup_services
from .services import async_register_services
__all__ = ["DOMAIN"]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Google Photos integration."""
async_setup_services(hass)
return True
__all__ = [
"DOMAIN",
]
async def async_setup_entry(
@@ -57,6 +48,8 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
async_register_services(hass)
return True
@@ -77,7 +77,7 @@ def _read_file_contents(
return results
def async_setup_services(hass: HomeAssistant) -> None:
def async_register_services(hass: HomeAssistant) -> None:
"""Register Google Photos services."""
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
@@ -152,10 +152,11 @@ def async_setup_services(hass: HomeAssistant) -> None:
}
return None
hass.services.async_register(
DOMAIN,
UPLOAD_SERVICE,
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
hass.services.async_register(
DOMAIN,
UPLOAD_SERVICE,
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
@@ -2,33 +2,48 @@
from __future__ import annotations
from datetime import datetime
import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.selector import ConfigEntrySelector
from .const import DEFAULT_ACCESS, DOMAIN
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
DATA = "data"
DATA_CONFIG_ENTRY = "config_entry"
WORKSHEET = "worksheet"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate the Google Sheets component."""
SERVICE_APPEND_SHEET = "append_sheet"
async_setup_services(hass)
return True
SHEET_SERVICE_SCHEMA = vol.All(
{
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
vol.Optional(WORKSHEET): cv.string,
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
},
)
async def async_setup_entry(
@@ -52,6 +67,8 @@ async def async_setup_entry(
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
entry.runtime_data = session
await async_setup_service(hass)
return True
@@ -64,4 +81,55 @@ async def async_unload_entry(
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
) -> bool:
"""Unload a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
for service_name in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service_name)
return True
async def async_setup_service(hass: HomeAssistant) -> None:
"""Add the services for Google Sheets."""
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
"""Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
try:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:
entry.async_start_reauth(hass)
raise
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
now = str(datetime.now())
rows = []
for d in call.data[DATA]:
row_data = {"created": now} | d
row = [row_data.get(column, "") for column in columns]
for key, value in row_data.items():
if key not in columns:
columns.append(key)
worksheet.update_cell(1, len(columns), key)
row.append(value)
rows.append(row)
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
async def append_to_sheet(call: ServiceCall) -> None:
"""Append new line of data to a Google Sheets document."""
entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
call.data[DATA_CONFIG_ENTRY]
)
if not entry or not hasattr(entry, "runtime_data"):
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
await entry.runtime_data.async_ensure_token_valid()
await hass.async_add_executor_job(_append_to_sheet, call, entry)
hass.services.async_register(
DOMAIN,
SERVICE_APPEND_SHEET,
append_to_sheet,
schema=SHEET_SERVICE_SCHEMA,
)
@@ -1,87 +0,0 @@
"""Support for Google Sheets."""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import ConfigEntrySelector
from .const import DOMAIN
if TYPE_CHECKING:
from . import GoogleSheetsConfigEntry
DATA = "data"
DATA_CONFIG_ENTRY = "config_entry"
WORKSHEET = "worksheet"
SERVICE_APPEND_SHEET = "append_sheet"
SHEET_SERVICE_SCHEMA = vol.All(
{
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
vol.Optional(WORKSHEET): cv.string,
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
},
)
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
"""Run append in the executor."""
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
try:
sheet = service.open_by_key(entry.unique_id)
except RefreshError:
entry.async_start_reauth(call.hass)
raise
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
now = str(datetime.now())
rows = []
for d in call.data[DATA]:
row_data = {"created": now} | d
row = [row_data.get(column, "") for column in columns]
for key, value in row_data.items():
if key not in columns:
columns.append(key)
worksheet.update_cell(1, len(columns), key)
row.append(value)
rows.append(row)
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
async def _async_append_to_sheet(call: ServiceCall) -> None:
"""Append new line of data to a Google Sheets document."""
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
call.data[DATA_CONFIG_ENTRY]
)
if not entry or not hasattr(entry, "runtime_data"):
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
await entry.runtime_data.async_ensure_token_valid()
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Sheets."""
hass.services.async_register(
DOMAIN,
SERVICE_APPEND_SHEET,
_async_append_to_sheet,
schema=SHEET_SERVICE_SCHEMA,
)
+5 -5
View File
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
from .entity import GroupEntity
DEFAULT_NAME = "Sensor Group"
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return state_classes[0]
async_create_issue(
self.hass,
DOMAIN,
GROUP_DOMAIN,
f"{self.entity_id}_state_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
return device_classes[0]
async_create_issue(
self.hass,
DOMAIN,
GROUP_DOMAIN,
f"{self.entity_id}_device_classes_not_matching",
is_fixable=False,
is_persistent=False,
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
if device_class:
async_create_issue(
self.hass,
DOMAIN,
GROUP_DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False,
is_persistent=False,
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
else:
async_create_issue(
self.hass,
DOMAIN,
GROUP_DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False,
is_persistent=False,
+77 -1
View File
@@ -9,8 +9,10 @@ from functools import partial
import logging
import os
import re
import struct
from typing import Any, NamedTuple
import aiofiles
from aiohasupervisor import SupervisorError
import voluptuous as vol
@@ -37,6 +39,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery_flow,
issue_registry as ir,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.deprecation import (
@@ -51,6 +54,7 @@ from homeassistant.helpers.hassio import (
get_supervisor_ip as _get_supervisor_ip,
is_hassio as _is_hassio,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service_info.hassio import (
HassioServiceInfo as _HassioServiceInfo,
)
@@ -109,7 +113,7 @@ from .coordinator import (
get_core_info, # noqa: F401
get_core_stats, # noqa: F401
get_host_info, # noqa: F401
get_info, # noqa: F401
get_info,
get_issues_info, # noqa: F401
get_os_info,
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]+$"))
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:
"""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):
"""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()
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)
return True
@@ -5,26 +5,13 @@ from __future__ import annotations
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started
from homeassistant.util import dt as dt_util
from .const import (
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 .const import TRAVEL_MODE_PUBLIC
from .coordinator import (
HereConfigEntry,
HERERoutingDataUpdateCoordinator,
HERETransitDataUpdateCoordinator,
)
from .model import HERETravelTimeConfig
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."""
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]
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
cls = HERETransitDataUpdateCoordinator
else:
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
async def _async_update_at_start(_: HomeAssistant) -> None:
@@ -26,7 +26,7 @@ from here_transit import (
import voluptuous as vol
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.helpers import config_validation as cv
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.unit_conversion import DistanceConverter
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
from .model import HERETravelTimeConfig, HERETravelTimeData
from .const import (
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
@@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
"""here_routing DataUpdateCoordinator."""
"""HERETravelTime DataUpdateCoordinator for the routing API."""
config_entry: HereConfigEntry
@@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
hass: HomeAssistant,
config_entry: HereConfigEntry,
api_key: str,
config: HERETravelTimeConfig,
) -> None:
"""Initialize."""
super().__init__(
@@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self._api = HERERoutingApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData:
"""Get the latest data from the HERE Routing API."""
origin, destination, arrival, departure = prepare_parameters(
self.hass, self.config
)
route_mode = (
RoutingMode.FAST
if self.config.route_mode == ROUTE_MODE_FASTEST
else RoutingMode.SHORT
)
params = prepare_parameters(self.hass, self.config_entry)
_LOGGER.debug(
(
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
" mode: %s, arrival: %s, departure: %s"
),
origin,
destination,
route_mode,
TransportMode(self.config.travel_mode),
arrival,
departure,
params.origin,
params.destination,
params.route_mode,
TransportMode(params.travel_mode),
params.arrival,
params.departure,
)
try:
response = await self._api.route(
transport_mode=TransportMode(self.config.travel_mode),
origin=here_routing.Place(origin[0], origin[1]),
destination=here_routing.Place(destination[0], destination[1]),
routing_mode=route_mode,
arrival_time=arrival,
departure_time=departure,
transport_mode=TransportMode(params.travel_mode),
origin=here_routing.Place(params.origin[0], params.origin[1]),
destination=here_routing.Place(
params.destination[0], params.destination[1]
),
routing_mode=params.route_mode,
arrival_time=params.arrival,
departure_time=params.departure,
return_values=[Return.POLYINE, Return.SUMMARY],
spans=[Spans.NAMES],
)
@@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
class HERETransitDataUpdateCoordinator(
DataUpdateCoordinator[HERETravelTimeData | None]
):
"""HERETravelTime DataUpdateCoordinator."""
"""HERETravelTime DataUpdateCoordinator for the transit API."""
config_entry: HereConfigEntry
@@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator(
hass: HomeAssistant,
config_entry: HereConfigEntry,
api_key: str,
config: HERETravelTimeConfig,
) -> None:
"""Initialize."""
super().__init__(
@@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator(
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self._api = HERETransitApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData | None:
"""Get the latest data from the HERE Routing API."""
origin, destination, arrival, departure = prepare_parameters(
self.hass, self.config
)
params = prepare_parameters(self.hass, self.config_entry)
_LOGGER.debug(
(
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
" departure: %s"
),
origin,
destination,
arrival,
departure,
params.origin,
params.destination,
params.arrival,
params.departure,
)
try:
response = await self._api.route(
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
destination=here_transit.Place(
latitude=destination[0], longitude=destination[1]
origin=here_transit.Place(
latitude=params.origin[0], longitude=params.origin[1]
),
arrival_time=arrival,
departure_time=departure,
destination=here_transit.Place(
latitude=params.destination[0], longitude=params.destination[1]
),
arrival_time=params.arrival,
departure_time=params.departure,
return_values=[
here_transit.Return.POLYLINE,
here_transit.Return.TRAVEL_SUMMARY,
@@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator(
def prepare_parameters(
hass: HomeAssistant,
config: HERETravelTimeConfig,
) -> tuple[list[str], list[str], str | None, str | None]:
config_entry: HereConfigEntry,
) -> HERETravelTimeAPIParams:
"""Prepare parameters for the HERE api."""
def _from_entity_id(entity_id: str) -> list[str]:
@@ -305,32 +308,55 @@ def prepare_parameters(
return formatted_coordinates
# Destination
if config.destination_entity_id is not None:
destination = _from_entity_id(config.destination_entity_id)
if (
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
) is not None:
destination = _from_entity_id(str(destination_entity_id))
else:
destination = [
str(config.destination_latitude),
str(config.destination_longitude),
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
]
# Origin
if config.origin_entity_id is not None:
origin = _from_entity_id(config.origin_entity_id)
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
origin = _from_entity_id(str(origin_entity_id))
else:
origin = [
str(config.origin_latitude),
str(config.origin_longitude),
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
]
# Arrival/Departure
arrival: str | None = None
departure: str | None = None
if config.arrival is not None:
arrival = next_datetime(config.arrival).isoformat()
if config.departure is not None:
departure = next_datetime(config.departure).isoformat()
arrival: datetime | None = None
if (
conf_arrival := dt_util.parse_time(
config_entry.options.get(CONF_ARRIVAL_TIME, "")
)
) 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:
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import time
from datetime import datetime
from typing import TypedDict
@@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict):
@dataclass
class HERETravelTimeConfig:
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
class HERETravelTimeAPIParams:
"""Configuration for polling the HERE API."""
destination_latitude: float | None
destination_longitude: float | None
destination_entity_id: str | None
origin_latitude: float | None
origin_longitude: float | None
origin_entity_id: str | None
destination: list[str]
origin: list[str]
travel_mode: str
route_mode: str
arrival: time | None
departure: time | None
arrival: datetime | None
departure: datetime | None
@@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
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 .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
@@ -51,6 +53,30 @@ async def async_setup_entry(
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)
entry.async_on_unload(entry.add_update_listener(update_listener))
@@ -107,7 +107,7 @@ OPTIONS_FLOW = {
}
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for History stats."""
config_flow = CONFIG_FLOW
@@ -73,9 +73,7 @@ async def async_setup_entry(
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
"""Hive Water Heater Device."""
_attr_supported_features = (
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
)
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_operation_list = SUPPORT_WATER_HEATER
@@ -4,8 +4,10 @@ import asyncio
from collections.abc import Callable, Coroutine
import itertools as it
import logging
from typing import TYPE_CHECKING, Any
import struct
from typing import Any
import aiofiles
import voluptuous as vol
from homeassistant import config as conf_util, core_config
@@ -38,7 +40,6 @@ from homeassistant.helpers import (
restore_state,
)
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.service import (
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
"""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)
installation_type = info["installation_type"][15:]
deprecated_method = installation_type in {
"Core",
"Supervised",
}
arch = info["arch"]
if arch == "armv7":
if installation_type == "OS":
# Local import to avoid circular dependencies
# We use the import helper because hassio
# may not be loaded yet and we don't want to
# do blocking I/O in the event loop to import it.
if TYPE_CHECKING:
# pylint: disable-next=import-outside-toplevel
from homeassistant.components import hassio
else:
hassio = await async_import_module(
hass, "homeassistant.components.hassio"
)
os_info = hassio.get_os_info(hass)
assert os_info is not None
issue_id = "deprecated_os_"
board = os_info.get("board")
if board in {"rpi3", "rpi4"}:
issue_id += "aarch64"
elif board in {"tinker", "odroid-xu4", "rpi2"}:
issue_id += "armv7"
if installation_type in {"Core", "Container"}:
deprecated_method = installation_type == "Core"
bit32 = _is_32_bit()
arch = info["arch"]
if bit32 and installation_type == "Container":
arch = await _get_arch()
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_container",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_container",
translation_placeholders={"arch": arch},
)
deprecated_architecture = bit32 and installation_type != "Container"
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_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
@@ -107,9 +107,9 @@
"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."
},
"deprecated_container_armv7": {
"deprecated_container": {
"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": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
@@ -215,17 +215,17 @@ class HomeAssistantSkyConnectOptionsFlowHandler(
"""Instantiate options flow."""
super().__init__(*args, **kwargs)
self._usb_info = get_usb_service_info(self._config_entry)
self._usb_info = get_usb_service_info(self.config_entry)
self._hw_variant = HardwareVariant.from_usb_product_name(
self._config_entry.data[PRODUCT]
self.config_entry.data[PRODUCT]
)
self._hardware_name = self._hw_variant.full_name
self._device = self._usb_info.device
self._probed_firmware_info = FirmwareInfo(
device=self._device,
firmware_type=ApplicationType(self._config_entry.data[FIRMWARE]),
firmware_version=self._config_entry.data[FIRMWARE_VERSION],
firmware_type=ApplicationType(self.config_entry.data[FIRMWARE]),
firmware_version=self.config_entry.data[FIRMWARE_VERSION],
source="guess",
owners=[],
)
@@ -288,7 +288,7 @@ class HomeAssistantYellowOptionsFlowHandler(
self._probed_firmware_info = FirmwareInfo(
device=self._device,
firmware_type=ApplicationType(self._config_entry.data["firmware"]),
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
firmware_version=None,
source="guess",
owners=[],
@@ -27,7 +27,6 @@ PLATFORMS = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SIREN,
Platform.SWITCH,
Platform.VALVE,
]
+9 -23
View File
@@ -83,7 +83,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
self._heating_mode is not None
):
if self._heating_mode.current_value == self._heating_mode.minimum:
if self._heating_mode.current_value == 0:
return HVACMode.OFF
return HVACMode.HEAT
@@ -91,10 +91,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction:
"""Return the hvac action."""
if (
self._heating_mode is not None
and self._heating_mode.current_value == self._heating_mode.minimum
):
if self._heating_mode is not None and self._heating_mode.current_value == 0:
return HVACAction.OFF
if (
@@ -113,12 +110,10 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
if (
ClimateEntityFeature.PRESET_MODE in self.supported_features
and self._heating_mode is not None
and self._heating_mode.current_value > self._heating_mode.minimum
and self._heating_mode.current_value > 0
):
assert self._attr_preset_modes is not None
return self._attr_preset_modes[
int(self._heating_mode.current_value - self._heating_mode.minimum) - 1
]
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
return PRESET_NONE
@@ -152,16 +147,14 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
# Currently only HEAT and OFF are supported.
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode,
(hvac_mode == HVACMode.HEAT) + self._heating_mode.minimum,
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
assert self._heating_mode is not None and self._attr_preset_modes is not None
await self.async_set_homee_value(
self._heating_mode,
self._attr_preset_modes.index(preset_mode) + self._heating_mode.minimum + 1,
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
)
async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -175,16 +168,12 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
async def async_turn_on(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode, 1 + self._heating_mode.minimum
)
await self.async_set_homee_value(self._heating_mode, 1)
async def async_turn_off(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode, 0 + self._heating_mode.minimum
)
await self.async_set_homee_value(self._heating_mode, 0)
def get_climate_features(
@@ -204,10 +193,7 @@ def get_climate_features(
if attribute.maximum > 1:
# Node supports more modes than off and heating.
features |= ClimateEntityFeature.PRESET_MODE
if attribute.maximum < 5:
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
else:
preset_modes.extend([PRESET_ECO])
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
if len(preset_modes) > 0:
preset_modes.insert(0, PRESET_NONE)
@@ -83,54 +83,3 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reconfigure flow."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input:
self.homee = Homee(
user_input[CONF_HOST],
reconfigure_entry.data[CONF_USERNAME],
reconfigure_entry.data[CONF_PASSWORD],
)
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()
self.homee.disconnect()
await self.homee.wait_until_disconnected()
await self.async_set_unique_id(self.homee.settings.uid)
self._abort_if_unique_id_mismatch(reason="wrong_hub")
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(), data_updates=user_input
)
return self.async_show_form(
data_schema=vol.Schema(
{
vol.Required(
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
): str
}
),
description_placeholders={
"name": reconfigure_entry.runtime_data.settings.uid
},
errors=errors,
)
@@ -1,43 +0,0 @@
"""Diagnostics for homee integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from . import DOMAIN, HomeeConfigEntry
TO_REDACT = [CONF_PASSWORD, CONF_USERNAME, "latitude", "longitude", "wlan_ssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: HomeeConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"settings": async_redact_data(entry.runtime_data.settings.raw_data, TO_REDACT),
"devices": [{"node": node.raw_data} for node in entry.runtime_data.nodes],
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: HomeeConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
# Extract node_id from the device identifiers
split_uid = next(
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
).split("-")
# Homee hub itself only has MAC as identifier and a node_id of -1
node_id = -1 if len(split_uid) < 2 else split_uid[1]
node = entry.runtime_data.get_node_by_id(int(node_id))
assert node is not None
return {
"homee node": node.raw_data,
}
-29
View File
@@ -31,22 +31,6 @@ class HomeeNumberEntityDescription(NumberEntityDescription):
NUMBER_DESCRIPTIONS = {
AttributeType.BUTTON_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
key="button_brightness_active",
entity_category=EntityCategory.CONFIG,
),
AttributeType.BUTTON_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
key="button_brightness_dimmed",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DISPLAY_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
key="display_brightness_active",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DISPLAY_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
key="display_brightness_dimmed",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
key="down_position",
entity_category=EntityCategory.CONFIG,
@@ -64,14 +48,6 @@ NUMBER_DESCRIPTIONS = {
key="endposition_configuration",
entity_category=EntityCategory.CONFIG,
),
AttributeType.EXTERNAL_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
key="external_temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.FLOOR_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
key="floor_temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
key="motion_alarm_cancelation_delay",
device_class=NumberDeviceClass.DURATION,
@@ -107,11 +83,6 @@ NUMBER_DESCRIPTIONS = {
key="temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.TEMPERATURE_REPORT_INTERVAL: HomeeNumberEntityDescription(
key="temperature_report_interval",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.UP_TIME: HomeeNumberEntityDescription(
key="up_time",
device_class=NumberDeviceClass.DURATION,
-5
View File
@@ -14,11 +14,6 @@ from .entity import HomeeEntity
PARALLEL_UPDATES = 0
SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
AttributeType.DISPLAY_TEMPERATURE_SELECTION: SelectEntityDescription(
key="display_temperature_selection",
options=["target", "current"],
entity_category=EntityCategory.CONFIG,
),
AttributeType.REPEATER_MODE: SelectEntityDescription(
key="repeater_mode",
options=["off", "level1", "level2"],
-10
View File
@@ -129,16 +129,6 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
AttributeType.EXTERNAL_TEMPERATURE: HomeeSensorEntityDescription(
key="external_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.FLOOR_TEMPERATURE: HomeeSensorEntityDescription(
key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.INDOOR_RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
key="indoor_humidity",
device_class=SensorDeviceClass.HUMIDITY,
-49
View File
@@ -1,49 +0,0 @@
"""The homee siren platform."""
from typing import Any
from pyHomee.const import AttributeType
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .entity import HomeeEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add siren entities for homee."""
async_add_devices(
HomeeSiren(attribute, config_entry)
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type == AttributeType.SIREN
)
class HomeeSiren(HomeeEntity, SirenEntity):
"""Representation of a homee siren device."""
_attr_name = None
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
@property
def is_on(self) -> bool:
"""Return the state of the siren."""
return self._attribute.current_value == 1.0
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the siren on."""
await self.async_set_homee_value(1)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the siren off."""
await self.async_set_homee_value(0)
+1 -47
View File
@@ -2,9 +2,7 @@
"config": {
"flow_title": "homee {name} ({host})",
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_hub": "Address belongs to a different homee."
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -24,16 +22,6 @@
"username": "The username for your homee.",
"password": "The password for your homee."
}
},
"reconfigure": {
"title": "Reconfigure homee {name}",
"description": "Reconfigure the IP address of your homee.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The IP address of your homee."
}
}
}
},
@@ -199,18 +187,6 @@
}
},
"number": {
"button_brightness_active": {
"name": "Button brightness (active)"
},
"button_brightness_dimmed": {
"name": "Button brightness (dimmed)"
},
"display_brightness_active": {
"name": "Display brightness (active)"
},
"display_brightness_dimmed": {
"name": "Display brightness (dimmed)"
},
"down_position": {
"name": "Down position"
},
@@ -223,12 +199,6 @@
"endposition_configuration": {
"name": "End position"
},
"external_temperature_offset": {
"name": "External temperature offset"
},
"floor_temperature_offset": {
"name": "Floor temperature offset"
},
"motion_alarm_cancelation_delay": {
"name": "Motion alarm delay"
},
@@ -253,9 +223,6 @@
"temperature_offset": {
"name": "Temperature offset"
},
"temperature_report_interval": {
"name": "Temperature report interval"
},
"up_time": {
"name": "Up-movement duration"
},
@@ -267,13 +234,6 @@
}
},
"select": {
"display_temperature_selection": {
"name": "Displayed temperature",
"state": {
"target": "Target",
"current": "Measured"
}
},
"repeater_mode": {
"name": "Repeater mode",
"state": {
@@ -305,12 +265,6 @@
"exhaust_motor_revs": {
"name": "Exhaust motor speed"
},
"external_temperature": {
"name": "External temperature"
},
"floor_temperature": {
"name": "Floor temperature"
},
"indoor_humidity": {
"name": "Indoor humidity"
},
@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.15"],
"requirements": ["aiohomekit==3.2.14"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}
@@ -21,7 +21,7 @@ from .const import (
HMIPC_NAME,
)
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .services import async_setup_services
from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
{
@@ -63,8 +63,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
await async_setup_services(hass)
return True
@@ -85,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
if not await hap.async_setup():
return False
await async_setup_services(hass)
_async_remove_obsolete_entities(hass, entry, hap)
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
@@ -116,6 +115,8 @@ async def async_unload_entry(
assert hap.reset_connection_listener is not None
hap.reset_connection_listener()
await async_unload_services(hass)
return await hap.async_reset()
@@ -128,6 +128,7 @@ class HomematicipHAP:
self.config_entry.data.get(HMIPC_AUTHTOKEN),
self.config_entry.data.get(HMIPC_NAME),
)
except HmipcConnectionError as err:
raise ConfigEntryNotReady from err
except Exception as err: # noqa: BLE001
@@ -210,41 +211,13 @@ class HomematicipHAP:
for device in self.home.devices:
device.fire_update_event()
async def async_connect(self) -> None:
"""Start WebSocket connection."""
tries = 0
while True:
retry_delay = 2 ** min(tries, 8)
async def async_connect(self, home: AsyncHome) -> None:
"""Connect to HomematicIP Cloud Websocket."""
await home.enable_events()
try:
await self.home.get_current_state_async()
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
await hmip_events
except HmipConnectionError:
_LOGGER.error(
(
"Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds"
),
self.config_entry.unique_id,
retry_delay,
)
if self._ws_close_requested:
break
self._ws_close_requested = False
tries += 1
try:
self._retry_task = self.hass.async_create_task(
asyncio.sleep(retry_delay)
)
await self._retry_task
except asyncio.CancelledError:
break
home.set_on_connected_handler(self.ws_connected_handler)
home.set_on_disconnected_handler(self.ws_disconnected_handler)
home.set_on_reconnect_handler(self.ws_reconnected_handler)
async def async_reset(self) -> bool:
"""Close the websocket connection."""
@@ -272,14 +245,22 @@ class HomematicipHAP:
async def ws_connected_handler(self) -> None:
"""Handle websocket connected."""
_LOGGER.debug("WebSocket connection to HomematicIP established")
_LOGGER.info("Websocket connection to HomematicIP Cloud 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")
_LOGGER.warning("Websocket connection to HomematicIP Cloud closed")
self._ws_connection_closed.set()
async def ws_reconnected_handler(self, reason: str) -> None:
"""Handle websocket reconnection."""
_LOGGER.info(
"Websocket connection to HomematicIP Cloud re-established due to reason: %s",
reason,
)
self._ws_connection_closed.set()
async def get_hap(
@@ -306,6 +287,6 @@ class HomematicipHAP:
home.on_update(self.async_update)
home.on_create(self.async_create_entity)
hass.loop.create_task(self.async_connect())
await self.async_connect(home)
return home
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.4"]
"requirements": ["homematicip==2.0.5"]
}

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