Compare commits

..

412 Commits

Author SHA1 Message Date
Franck Nijhof 18e12740d9 2024.11.0 (#129970) 2024-11-06 20:10:51 +01:00
Franck Nijhof 5a24b670a2 Ran ruff 2024-11-06 19:32:23 +01:00
Franck Nijhof 94c5c8f42e Bump version to 2024.11.0 2024-11-06 19:29:07 +01:00
Manu e84d5fba11 Add state invitation to list access sensor in Bring integration (#129960) 2024-11-06 19:28:54 +01:00
Franck Nijhof 782417528c Bump version to 2024.11.0b9 2024-11-06 18:25:29 +01:00
Robert Resch 7757423d18 Bump go2rtc-client to 0.1.0 (#129965) 2024-11-06 18:24:12 +01:00
Joost Lekkerkerker e5a28f4f25 Remove deprecation issues for LCN once entities removed (#129955) 2024-11-06 18:21:32 +01:00
Erik Montnemery c18d50910f Call async_refresh_providers when camera entity feature changes (#129941) 2024-11-06 18:21:28 +01:00
Franck Nijhof 3b840c684b Bump version to 2024.11.0b8 2024-11-06 15:44:10 +01:00
Bram Kragten bc84fdc64a Update frontend to 20241106.0 (#129953) 2024-11-06 15:43:33 +01:00
Robert Resch 401262c23d Bump go2rtc-client to 0.0.1b5 (#129952) 2024-11-06 15:42:22 +01:00
Manu 795384ca2d Improve error messages in Habitica (#129948)
Improve error messages
2024-11-06 15:41:44 +01:00
J. Diego Rodríguez Royo dfc3423c83 Delete binary door deprecation issue on unload at Home Connect (#129947) 2024-11-06 15:41:39 +01:00
Robert Resch 22b5071c26 Bump go2rtc-client to 0.0.1b4 (#129942) 2024-11-06 15:40:30 +01:00
Joost Lekkerkerker 4b9524c5c1 Write squeezebox player state after query (#129939) 2024-11-06 15:39:07 +01:00
Joost Lekkerkerker 9cd46c7f03 Bump spotifyaio to 0.8.5 (#129938) 2024-11-06 15:39:03 +01:00
Robert Resch 232a6868ff Fix native sync WebRTC offer (#129931) 2024-11-06 15:39:00 +01:00
Kunal Aggarwal 361e0d4fc7 Adding "peaceful" status as on value to Tuya Presence Sensor (#129925) 2024-11-06 15:38:57 +01:00
Paulus Schoutsen 26d8d5343a Ensure all template names are strings (#129921) 2024-11-06 15:38:53 +01:00
starkillerOG 995aab8347 Bump reolink_aio to 0.10.4 (#129914) 2024-11-06 15:38:50 +01:00
Robert Resch 399011552b Disable uv cache (#129912) 2024-11-06 15:38:46 +01:00
Markus Jacobsen 0c9f30364c Update Bang & Olufsen source list as availability changes (#129910) 2024-11-06 15:38:43 +01:00
Louis Christ bdc17621ee Map "stop" to MediaPlayerState.IDLE in bluesound integration (#129904)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-11-06 15:38:40 +01:00
Joost Lekkerkerker 399c53a57e Bump spotifyaio to 0.8.4 (#129899) 2024-11-06 15:38:36 +01:00
Daniel Hjelseth Høyer f55e13bde4 Bump pyTibber to 0.30.4 (#129844) 2024-11-06 15:38:32 +01:00
Michael Hansen 48d9df89ac Bump intents and add HassRespond test (#129830) 2024-11-06 15:36:46 +01:00
kingal123 adf836d9ac Update pylutron to 0.2.16 (#129653)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-06 15:33:16 +01:00
Franck Nijhof 211ce43127 Bump version to 2024.11.0b7 2024-11-05 20:33:48 +01:00
G Johansson f5555df990 Bump holidays to 0.60 (#129909) 2024-11-05 20:33:39 +01:00
Paul Bottein 82c2422990 Update frontend to 20241105.0 (#129906) 2024-11-05 20:33:36 +01:00
Erik Montnemery 734ebc1adb Improve improv BLE error handling (#129902) 2024-11-05 20:33:33 +01:00
Paulus Schoutsen eb3371beef Change Ollama default to llama3.2 (#129901) 2024-11-05 20:33:30 +01:00
Manu e1ef1063fe Prevent update entity becoming unavailable on device disconnect in IronOS (#129840)
* Don't render update entity unavailable when Pinecil device disconnects

* fixes
2024-11-05 20:33:27 +01:00
Diogo Gomes c355a53485 Set friendly name of utility meter select entity when configured through YAML (#128267)
* set select friendly name in YAML

* backward compatibility added

* clean

* cleaner backward compatibility approach

* don't introduce default unique_id

* split test according to review
2024-11-05 20:33:23 +01:00
Franck Nijhof c85eb6bf8e Bump version to 2024.11.0b6 2024-11-05 16:51:05 +01:00
Joost Lekkerkerker cc30d34e87 Remove timers from LG ThinQ (#129898) 2024-11-05 16:50:41 +01:00
Erik Montnemery 14875a1101 Map go2rtc log levels to Python log levels (#129894) 2024-11-05 16:50:38 +01:00
Joost Lekkerkerker 030aebb97f Use default package for yt-dlp (#129886) 2024-11-05 16:50:35 +01:00
Erik Montnemery 6e2f36b6d4 Log go2rtc output with warning level on error (#129882) 2024-11-05 16:50:32 +01:00
Robert Resch 25a05eb156 Append a 1 to all go2rtc ports to avoid port conflicts (#129881) 2024-11-05 16:50:29 +01:00
J. Diego Rodríguez Royo b71c4377f6 Removed stale translation and improved set_setting translation at Home Connect (#129878) 2024-11-05 16:50:25 +01:00
Michael Arthur d671341864 Update snapshot for lg thinq (#129856)
update snapshot for lg thinq

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-05 16:39:02 +01:00
Mike Degatano 383f712d43 Add repair for add-on boot fail (#129847) 2024-11-05 16:38:59 +01:00
Alex Bush 8a20cd77a0 Bump pyfibaro to 0.8.0 (#129846) 2024-11-05 16:38:56 +01:00
Richard Kroegel 14023644ef Bump bimmer_connected to 0.16.4 (#129838) 2024-11-05 16:38:53 +01:00
dotvav 496fc42b94 Bump pypalazzetti to 0.1.10 (#129832) 2024-11-05 16:38:50 +01:00
Erik Montnemery da0688ce8e Validate go2rtc server version (#129810) 2024-11-05 16:38:47 +01:00
Robert Resch 89d3707cb7 Skip adding providers if the camera has native WebRTC (#129808)
* Skip adding providers if the camera has native WebRTC

* Update homeassistant/components/camera/__init__.py

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

* Implement suggestion

* Add tests

* Shorten test name

* Fix test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-11-05 16:38:44 +01:00
Kunal Aggarwal 3f5e395e2f Adding new on values for Tuya Presence Detection Sensor (#129801) 2024-11-05 16:38:41 +01:00
Joost Lekkerkerker 00ea1cab9f Add basic testing framework to LG ThinQ (#127785)
Co-authored-by: jangwon.lee <jangwon.lee@lge.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: YunseonPark-LGE <34848373+YunseonPark-LGE@users.noreply.github.com>
Co-authored-by: LG-ThinQ-Integration <LG-ThinQ-Integration@lge.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-11-05 16:38:37 +01:00
Paulus Schoutsen c7b2ffbc8e Bump version to 2024.11.0b5 2024-11-05 03:00:18 +00:00
J. Nick Koston 3a1502e2bb Disable SRTP for unifiprotect RTSPS stream (#129852) 2024-11-05 02:59:23 +00:00
J. Nick Koston b830f83a34 Bump uiprotect to 6.4.0 (#129851) 2024-11-05 02:59:23 +00:00
J. Nick Koston 2982e733bc Fix unifiprotect supported features being set too late (#129850) 2024-11-05 02:59:22 +00:00
starkillerOG e89ce215c6 Bump reolink-aio to 0.10.3 (#129841) 2024-11-05 02:59:21 +00:00
G Johansson b6345f8d07 Fix translations in hydrawise (#129834) 2024-11-05 02:59:20 +00:00
G Johansson 9d261bab48 Fix translation in ovo energy (#129833) 2024-11-05 02:59:19 +00:00
Michael Hansen b6f875134e Add HassRespond intent (#129755)
* Add HassHello intent

* Rename to HassRespond

* LLM's ignore HassRespond intent
2024-11-05 02:59:18 +00:00
Artur Pragacz 90ceebdf91 Fix source mapping in Onkyo (#129716)
* Fix source mapping

* Fix copy paste
2024-11-05 02:59:18 +00:00
Paulus Schoutsen 03e6a13896 Bump version to 2024.11.0b4 2024-11-04 18:48:58 +00:00
G Johansson 9fb3261f02 Fix translations in landisgyr (#129831) 2024-11-04 18:48:37 +00:00
Bram Kragten 0bc6b8b0d4 Update frontend to 20241104.0 (#129829) 2024-11-04 18:48:36 +00:00
G Johansson 18d2ced045 Fix translations in homeworks (#129824) 2024-11-04 18:48:35 +00:00
Robert Resch 6c75e0bee1 Remove all ice_servers on native sync WebRTC cameras (#129819) 2024-11-04 18:48:35 +00:00
Steven B. 0b981f42bb Bump python-kasa to 0.7.7 (#129817)
Bump tplink dependency python-kasa to 0.7.7
2024-11-04 18:48:34 +00:00
Paulus Schoutsen 82868a8588 Fix ESPHome dashboard check (#129812) 2024-11-04 18:48:33 +00:00
Erik Montnemery 6e93777f54 Fix create flow logic for single config entry integrations (#129807)
* Fix create flow logic for single config entry integrations

* Adjust MQTT test
2024-11-04 18:47:41 +00:00
Erik Montnemery 9349292464 Fix aborting flows for single config entry integrations (#129805) 2024-11-04 18:43:56 +00:00
Robert Resch 7084b3b52c Update go2rtc stream if stream_source is not matching (#129804) 2024-11-04 18:43:55 +00:00
epenet 0f0f5fd0ab Fix incorrect description placeholders in azure event hub (#129803) 2024-11-04 18:43:54 +00:00
Joost Lekkerkerker cb0b942db3 Improve error handling in Spotify (#129799) 2024-11-04 18:43:53 +00:00
Erik Montnemery b1c9f83952 Fix stringification of discovered hassio uuid (#129797) 2024-11-04 18:43:52 +00:00
Joost Lekkerkerker 1ff0efc97b Bump yt-dlp to 2024.11.04 (#129794) 2024-11-04 18:43:51 +00:00
Robert Resch a4da2a9eb5 Use RTCIceCandidate instead of str for candidate (#129793) 2024-11-04 18:43:51 +00:00
Antoine Reversat ba3cfb5f87 Bump ayla-iot-unofficial to 1.4.3 (#129743)
Upgrade to ayla-iot-unofficial v1.4.3
2024-11-04 18:43:50 +00:00
Luca Angemi bf196935f6 Add state class to precipitation_intensity in Aemet (#129670)
Update sensor.py
2024-11-04 18:43:49 +00:00
Joost Lekkerkerker 6e98343706 Update Spotify state after mutation (#129607) 2024-11-04 18:43:48 +00:00
Erik Montnemery de453ab5c1 Add watchdog to monitor and respawn go2rtc server (#129497) 2024-11-04 18:43:47 +00:00
Andre Lengwenus f408de4fc3 Bump lcn-frontend to 0.2.1 (#129457) 2024-11-04 18:43:47 +00:00
Bram Kragten 5141a4d292 Bump version to 2024.11.0b3 2024-11-04 09:32:53 +01:00
LG-ThinQ-Integration cf8b7607ae Bump thinqconnect to 1.0.0 (#129769)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-11-04 09:31:43 +01:00
Joost Lekkerkerker b38fe00387 Bump spotifyaio to 0.8.3 (#129729) 2024-11-04 09:31:42 +01:00
J. Nick Koston 5d446f0e14 Bump HAP-python to 4.9.2 (#129715) 2024-11-04 09:31:41 +01:00
Josef Zweck a592ece9c8 Add missing translation string to lamarzocco (#129713)
* add missing translation string

* Update strings.json

* import pytest again
2024-11-04 09:31:40 +01:00
Allen Porter 9cb60c61d1 Fix nest streams broken due to CameraCapabilities change (#129711)
* Fix nest streams broken due to CameraCapabilities change

* Fix stream cleanup

* Apply suggestions from code review

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/nest/camera.py

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2024-11-04 09:31:39 +01:00
J. Nick Koston 90ed06c354 Bump DoorBirdPy to 3.0.8 (#129709) 2024-11-04 09:31:39 +01:00
Manu 22d64cb8f4 Bump bring-api to 0.9.1 (#129702) 2024-11-04 09:31:38 +01:00
Nathan Spencer 453039e860 Change alexa arm handler to allow switching arm states unless in armed_away mode (#129701)
* Change alexa arm handler to allow switching arm states unless in armed_away mode

* Address PR comments
2024-11-04 09:31:37 +01:00
Simon Lamon e727162225 Bump python-linkplay to 0.0.17 (#129683) 2024-11-04 09:31:36 +01:00
Ståle Storø Hauknes a898a5996e Bump Airthings BLE to 0.9.2 (#129659)
Bump airthings ble
2024-11-04 09:31:35 +01:00
Jesse Hills d501bb8d52 Only set ESPHome configuration url to addon if there is an existing configuration for the device (#129356)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-04 09:31:34 +01:00
Bram Kragten 5ef45fd12e Bump version to 2024.11.0b2 2024-11-02 20:42:48 +01:00
Klaas Schoute 8a293a41f5 Bump autarco lib to v3.1.0 (#129684)
Bump autarco to v3.1.0
2024-11-02 20:42:44 +01:00
J. Nick Koston 931820a170 Bump sensorpush-ble to 1.7.1 (#129657) 2024-11-02 20:42:44 +01:00
J. Nick Koston e9944b964a Bump aioesphomeapi to 27.0.1 (#129643) 2024-11-02 20:42:43 +01:00
J. Nick Koston dbae1d2f8b Bump aiohomekit to 3.2.6 (#129640) 2024-11-02 20:42:42 +01:00
Joost Lekkerkerker 0dc8feba05 Bump spotifyaio to 0.8.2 (#129639) 2024-11-02 20:42:41 +01:00
Robert Resch 5c7c2347f7 Bump webrtc-models to 0.2.0 (#129627) 2024-11-02 20:42:40 +01:00
J. Nick Koston d069907948 Pin async-timeout to 4.0.3 (#129592) 2024-11-02 20:42:39 +01:00
Erik Montnemery 725ab477a8 Revert "Create a script service schema based on fields" (#129591) 2024-11-02 20:42:38 +01:00
Robert Resch d05ee9ff60 Add go2rtc debug_ui yaml key to enable go2rtc ui (#129587)
* Add go2rtc debug_ui yaml key to enable go2rtc ui

* Apply suggestions from code review

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

* Order imports

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-11-02 20:42:36 +01:00
Joost Lekkerkerker 3c1f6d97cc Bump aiowithings to 3.1.1 (#129586) 2024-11-02 20:42:33 +01:00
epenet 5fe827f6c4 Fix flaky camera test (#129576) 2024-11-02 20:42:31 +01:00
Erik Montnemery 76f9a93ed7 Bump aiohasupervisor to version 0.2.1 (#129574) 2024-11-02 20:42:30 +01:00
Joost Lekkerkerker df2506bfbb Bump spotifyaio to 0.8.1 (#129573) 2024-11-02 20:42:29 +01:00
Joost Lekkerkerker b25ab04d2c Fix Geniushub setup (#129569) 2024-11-02 20:42:28 +01:00
Steven B. 6f094e8a54 Check for async web offer overrides in camera capabilities (#129519) 2024-11-02 20:42:27 +01:00
Bram Kragten 41590f91ac Bump version to 2024.11.0b1 2024-10-31 16:38:09 +01:00
Paul Bottein e9d1f4f46e Update frontend to 20241031.0 (#129583) 2024-10-31 16:36:58 +01:00
epenet 7f287412ba Log type as well as value for unique_id checks (#129575) 2024-10-31 16:36:57 +01:00
Erik Montnemery 2df094de2b Stringify discovered hassio uuid (#129572)
* Stringify discovered hassio uuid

* Correct DiscoveryKey

* Adjust tests
2024-10-31 16:36:56 +01:00
starkillerOG 964ab5b351 Log Reolink select value KeyError only once (#129559) 2024-10-31 16:36:55 +01:00
Brett Adams 3f6e9a54fe Fix "home" route in Tesla Fleet & Teslemetry (#129546)
* translate Home to home

* refactor for mypy

* Fix home state

* Revert key change

* Add testing
2024-10-31 16:36:55 +01:00
J. Nick Koston 4ec5d5ae1e Bump yarl to 1.17.1 (#129539)
changelog: https://github.com/aio-libs/yarl/compare/v1.17.0...v1.17.1
2024-10-31 16:36:54 +01:00
Erik Montnemery c49b155c29 Allow importing homeassistant.core.Config until 2025.11 (#129537) 2024-10-31 16:36:53 +01:00
Luca Angemi fc602b1888 Fix bthome UnitOfConductivity (#129535)
Fix unit
2024-10-31 16:36:52 +01:00
G Johansson 81421992a2 Missing config_flow in manifest for local_file (#129529) 2024-10-31 16:36:51 +01:00
starkillerOG 4ef31f9331 Bump reolink_aio to 0.10.2 (#129528) 2024-10-31 16:36:50 +01:00
G Johansson d7e304badf Fix async_config_entry_first_refresh used after config entry is loaded in speedtestdotcom (#129527)
* Fix async_config_entry_first_refresh used after config entry is loaded in speedtestdotcom

* is
2024-10-31 16:36:49 +01:00
cryptk bf3f1b4b49 Bump uiprotect to 6.3.2 (#129513) 2024-10-31 16:36:49 +01:00
Jan Bouwhuis 2ac0ff03fc Fix current temperature calculation for incomfort boiler (#129496) 2024-10-31 16:36:48 +01:00
Aurore d10553d624 Fix timeout issue on Roomba integration when adding a new device (#129230)
* Update const.py

DEFAULT_DELAY = 1 to DEFAULT_DELAY = 100 to fix timeout when adding a new device

* Update config_flow.py

continuous=False to continuous=True to fix timeout when adding a new device

* Update homeassistant/components/roomba/const.py

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* Update test_config_flow.py

Change CONF_DELAY to match DEFAULT_DELAY (30 sec instead of 1)

* Update tests/components/roomba/test_config_flow.py

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* Use constant for DEFAULT_DELAY in tests

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: jbouwh <jan@jbsoft.nl>
2024-10-31 16:36:47 +01:00
Marcel van der Veldt 4dc2433e8b Revert "Add musicassistant integration (#128919)" (#129565)
This reverts commit 568bdef61f.
2024-10-31 12:18:10 +01:00
Bram Kragten 60c93456c0 Merge branch 'dev' into rc 2024-10-30 18:33:24 +01:00
G Johansson a4f210379d Raise on non-string unique id for config entry (#125950)
* Raise on non-string unique id for config entry

* Add test update entry

* Fix breaking

* Add check get_entry_by_domain_and_unique_id

* Naming

* Add test

* Fix logic

* No unique id

* Fix tests

* Fixes

* Fix gardena

* Not related to this PR

* Update docstring and comment

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-10-30 18:09:50 +01:00
Bram Kragten 27e6205a37 Merge branch 'dev' into rc 2024-10-30 17:41:05 +01:00
G Johansson 3db6d82904 Add name to description placeholders automatically for reauth flows (#129232)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-10-30 17:38:59 +01:00
puddly b8ddfd642e Bump ZHA dependencies (#129510) 2024-10-30 17:38:24 +01:00
Bram Kragten c98acd42db Bump version to 2024.11.0b0 2024-10-30 17:34:45 +01:00
Paul Bottein 39f418f2d2 Update frontend to 20241030.0 (#129508) 2024-10-30 17:31:41 +01:00
Jan Bouwhuis 9fbd484dfe Add progress support to MQTT update platform (#129468)
* Add progress support to MQTT update platform and add validation on state updates

* Clean up cast to type class

* Add support for display_precision attribute
2024-10-30 17:22:55 +01:00
Jan Bouwhuis 1773f2aadc Allow MQTT device based auto discovery (#118757)
* Allow MQTT device based auto discovery

* Fix merge error

* Remove unused import

* Fix discovery device based topics

* Fix cannot delete twice

* Improve cleanup test

* Follow up comment

* Typo

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Explain more

* Use tuple

* Default a device payload to have priority over a platform based payload

* Add unique_id to sensor test data

* Set migration flag to mark a discovery topic for migration

* Correct type hint

* Make unique_id required for components in device based discovery payload

* Remove CONF_MIGRATE_DISCOVERY from platform schema

* Unload discovered MQTT item to allow migration

* Follow up comments from code review

* ruff

* Subscribe to platform discovery wildcards first

* Use normal dict

* Use dict to persist wildcard subscription order

* Remove missed unused parameter

* Add a comment to explain we use a dict  to preserve the subscription order

* Add wildcard subscription order test

* Remove discovery flag from test

* Improve discovery migration origin logging

* Assert initial  wildcard discovery topics subscription order and after reconnect

* Improve log messages

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2024-10-30 17:10:15 +01:00
Michael Hansen cb1b72d6ba Bump intents to 2024.10.30 (#129505) 2024-10-30 16:20:59 +01:00
Manu f5a2ec961d Remove unused snapshots from Habitica (#129499) 2024-10-30 15:44:21 +01:00
Krisjanis Lejejs bf40e77d65 Add Stun server with port 3478 (#129501) 2024-10-30 15:40:23 +01:00
Jozef Kruszynski 568bdef61f Add musicassistant integration (#128919)
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
2024-10-30 14:57:01 +01:00
Manu 2303521778 Use common translation strings for Habitica (#129498) 2024-10-30 14:56:47 +01:00
Josef Zweck 3bf2946d13 Change type of the config_entry in coordinator in tedee (#129502) 2024-10-30 14:53:11 +01:00
Josef Zweck 484e5cb3e8 Explicitly pass config_entry to coordinator in lamarzocco (#129434)
* Update __init__.py

* Update coordinator.py

* Update coordinator.py

* ruff

* Update coordinator.py

* move type to coordinator
2024-10-30 14:43:41 +01:00
Josef Zweck fbe8b6c34d Pass config_entry explicitly to coordinator in tedee (#129432)
* pass entry

* pass entry

* Update coordinator.py

* move type definition
2024-10-30 14:42:19 +01:00
Jan Bouwhuis 4e7397dc9d Test discovery subscriptions not done when discovery is disabled (#129458)
Test discovery subscriptions not performend when discovery is disabled
2024-10-30 14:38:44 +01:00
starkillerOG a6189106e1 Reolink add TCP push event connection as primary method (#129490) 2024-10-30 14:34:32 +01:00
Artur Pragacz ed6123a3e6 Add reconfigure step to Onkyo config flow (#129088) 2024-10-30 14:31:43 +01:00
Noah Husby 0cd5deaa3f Add audio output select to Cambridge Audio (#129366) 2024-10-30 14:28:01 +01:00
Allen Porter 6c047e2678 Refresh Nest WebRTC streams before expiration (#129478) 2024-10-30 14:25:43 +01:00
Martin Hjelmare 405a480cae Create repair issue for legacy webrtc provider (#129334)
* Add repair issue

* Add tests

* Add option to not use builtin go2rtc provider

* Add test

* Add domain to new providers

* Add learn more url

* Update placeholder

* Promote the builtin provider

* Refactor provider storage

* Move check for legacy provider conflict to refresh

* Test provider registration race

* Add test for registering the same legacy provider twice

* Test test_get_not_supported_legacy_provider

* Remove blank line between bullets

* Call it built-in

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Revert "Add option to not use builtin go2rtc provider"

This reverts commit 4e31bad6c0c23d5a1c0935c985351808a46163d6.

* Revert "Add test"

This reverts commit ddf85fd4db2c78b15c1cdc716804b965f3a1f4e3.

* Update issue description

* async_close_session is optional

* Clean up after rebase

* Add required domain property to provider tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-30 14:11:17 +01:00
Erik Montnemery b4e69bab71 Improve shutdown of esphome ffmpeg proxy (#129326)
* Improve shutdown of esphome ffmpeg proxy

* Add test
2024-10-30 13:46:05 +01:00
Erik Montnemery db81edfb2b Add config entry to go2rtc (#129436)
* Add config entry to go2rtc

* Address review comments

* Remove config entry if go2rtc is not configured

* Allow importing default_config

* Address review comment
2024-10-30 13:39:54 +01:00
Martin Hjelmare 24829bc44f Fix webrtc provider interface and tests (#129488)
* Fix webrtc provider tests

* Remove future code

* Add a test of the optional provider interface
2024-10-30 13:24:23 +01:00
starkillerOG c8594045df Bump reolink_aio to 0.10.1 (#129493) 2024-10-30 13:19:45 +01:00
YogevBokobza ea3f9b971f Bump aioswitcher to 4.4.0 (#129489) 2024-10-30 12:50:38 +01:00
Robert Resch 380974eed4 Remove hassio from ALLOWED_USED_COMPONENTS and move some functions to helper (#127228)
* Remove hassio from ALLOWED_USED_COMPONENTS

* Move HassioServiceInfo to helpers.service_info

* Deprecate moved functions

* Add note about deprecation

* Fix tests

* Implement suggestion

* Typo

* Update pyproject.toml

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-10-30 12:43:41 +01:00
Alistair Francis 8151403bf6 Bump automower-ble to 0.2.0 (#129473) 2024-10-30 12:31:11 +01:00
Christopher Fenner 16f5e76f00 Update PyViCare dependency to 2.35.0 (#129038) 2024-10-30 12:21:54 +01:00
J. Nick Koston b6b178cac0 Fix nexia emergency heat migration (#129365) 2024-10-30 12:20:19 +01:00
Robert Resch 0f020366e3 Bump go2rtc-client to 0.0.1b3 (#129486) 2024-10-30 12:13:03 +01:00
LG-ThinQ-Integration 27a19be369 Add translation_key in LG ThinQ (#129476)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-10-30 11:28:28 +01:00
Blake Bryant 0c166eb307 Bump pydeako to 0.5.4 (#129475) 2024-10-30 11:25:11 +01:00
Erik Montnemery 79d73c28a7 Deduplicate wav creation in esphome ffmpeg_proxy tests (#129484) 2024-10-30 10:35:19 +01:00
LG-ThinQ-Integration 2aed01b530 Add entity_category to avoid header_toggle for switch (#129477)
add entity_category to avoid header_toggle

Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-10-30 10:34:04 +01:00
Erik Montnemery 3fb0d61271 Remove useless code from esphome ffmpeg_proxy tests (#129481) 2024-10-30 09:56:12 +01:00
Erik Montnemery 599acaf514 Improve demo integration's update entity (#129401)
* Improve demo integration's update entity

* Improve tests
2024-10-30 08:06:22 +01:00
TimL 5f4103a4a7 Allow smlight device to reboot before updating firmware data coordinator (#127442)
* Add delay before updating firmware coordinator

* fix update tests

* change sleep to 1s

* Timeout incase reboot fails

* update test

* test reboot timeout

* log hostname in warning
2024-10-30 08:02:30 +01:00
Kayden van Rijn c7c72231c7 Bump opower to 0.8.6 (#129454)
* Bump opower to 0.8.6

* Bump opower to 0.8.6
2024-10-29 22:44:06 -07:00
Manu 6887a4419e Add calendar platform to Habitica integration (#128248)
* Add calendar platform

* Add tests

* add missing reminders filter by date

* Add +1 day to todo end

* add 1 day to dailies, remove unused line of code

* Removing reminders calendar to a separate PR

* fix upcoming event for dailies

* util function for rrule string

* Add test for get_recurrence_rule

* use habitica daystart and account for isDue flag

* yesterdaily is still an active event

* Fix yesterdailies and add attribute

* Update snapshot

* Use iter, return attribute with None value

* various changes

* update snapshot

* fix merge error

* update snapshot

* change date range filtering for todos

* use datetimes instead of date in async_get_events

* Sort events

* Update snapshot

* add method for todos

* filter for upcoming events

* dailies

* refactor todos

* update dailies logic

* dedent loops
2024-10-29 20:53:49 -07:00
Erik Montnemery db5cb6233c Correct condition signalling non-live DB migration is in progress (#129464) 2024-10-29 12:26:52 -10:00
Robert Resch 963829712d Add CameraCapabilities (#128455) 2024-10-29 21:36:30 +01:00
Steven B. 46ceccfbb3 Use new try_connect_all discover command in tplink config flow (#128994)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-10-29 10:26:34 -10:00
J. Nick Koston aaf3039967 Bump DoorBirdPy to 3.0.7 (#129114) 2024-10-29 10:06:24 -10:00
Shay Levy 2509f18def Bump aioshelly to 12.0.1 (#129453) 2024-10-29 22:01:38 +02:00
Krisjanis Lejejs a1e2d79613 Add cloud ICE server registration (#128942)
* Add cloud ICE server registration

* Add ice_servers to prefs, fix registration flow

* Add support for list of ICE servers

* Add ICE server cleanup on cloud logout, create tests

* Fix RTCIceServer types

* Update homeassistant/components/cloud/client.py

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

* Improve tests based on PR reviews

* Improve tests

* Use set_cloud_prefs fixture

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2024-10-29 20:35:52 +01:00
Andre Lengwenus 96ba5c3983 Remove LCN translation placeholder key (#129452) 2024-10-29 20:27:13 +01:00
ollo69 041282190a Allow set ScreenCap interval as option for AndroidTV (#124470)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-29 20:24:20 +01:00
functionpointer 8cdd5de75c Change Tibber get_prices action to return datetimes as str (#123901) 2024-10-29 20:15:08 +01:00
Michael a95c232f11 Add addon support to Home Assistant Analytics Insights (#128806) 2024-10-29 20:13:56 +01:00
Andre Lengwenus c9aba288b4 Add switch entities for LCN key-locks and regulator-locks (#127731)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 20:08:30 +01:00
G Johansson 35a9d502af Use coordinator async_setup in dwd weather (#129448) 2024-10-29 20:07:37 +01:00
G Johansson 409c8783fe Use coordinator async_setup in iotty (#129449) 2024-10-29 20:07:13 +01:00
Keilin Bickar 3adc3d7732 Add sensors for energy trends for devices (#129439)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 20:02:08 +01:00
Steven B. ec19712388 Bump tplink python-kasa dependency to 0.7.6 (#129444) 2024-10-29 09:00:43 -10:00
Åke Strandberg 2c89e89c84 Improve mapping of myuplink entities (#129137) 2024-10-29 19:59:04 +01:00
Manu e602a464db Add tests for buttons in Habitica integration (#128194)
* Add tests for button platform

* update tests

* Add skill buttons

* Assert state, add fixtures/parametrization

* entity as list
2024-10-29 19:03:41 +01:00
Erik Montnemery ffc0651d89 Report update_percentage in zwave_js update entity (#129386) 2024-10-29 13:31:34 -04:00
Erik Montnemery 7162efd836 Remove duplicated entity_picture config from MQTT update entity (#129390) 2024-10-29 18:22:06 +01:00
epenet 8e7d782102 Move validation routine out of wallbox coordinator (#129415) 2024-10-29 18:13:11 +01:00
Marc Mueller dc2028f99c Fix devolo_home_network DataCoordinator arguments (#129441) 2024-10-29 18:06:42 +01:00
Adam Goode f12ba5f7a9 Unexport unavailable metrics in Prometheus (#125492) 2024-10-29 17:56:54 +01:00
Erik Montnemery 45fb21e32d Suppress update entity's update_percentage when update not in progress (#129397) 2024-10-29 17:56:09 +01:00
Erik Montnemery ecbb417736 Report update_percentage in esphome update entity (#129376) 2024-10-29 17:51:54 +01:00
Erik Montnemery 3a59a862d5 Report update_percentage in smlight update entity (#129383) 2024-10-29 17:50:43 +01:00
Erik Montnemery e34fab0045 Report update_percentage in tessie update entity (#129385) 2024-10-29 17:48:29 +01:00
Erik Montnemery 7254ebe0e3 Report update_percentage in teslemetry update entity (#129384) 2024-10-29 17:48:03 +01:00
Keilin Bickar b43bc3f32d Add Sense Devices for entities (#129182) 2024-10-29 17:44:19 +01:00
Erik Montnemery ca3d13b5cc Sort some code in core_config (#129388) 2024-10-29 17:26:08 +01:00
Robert Resch c8818bcce3 Bump go2rtc to 1.9.6 (#129430) 2024-10-29 16:46:58 +01:00
Guido Schmitz b234b5937a Disable pylint for DevoloScannerEntity (#129429) 2024-10-29 16:40:38 +01:00
Krisjanis Lejejs 1bdef0f2f7 Bump hass-nabucasa to 0.83.0 (#129422) 2024-10-29 16:34:02 +01:00
Erik Montnemery 56fb61bd6f Refactor esphome ffmpeg proxy (#129330) 2024-10-29 16:26:32 +01:00
epenet 2c7d0b8909 Initialise coordinator with config_entry in components (part 1) (#128080) 2024-10-29 16:18:04 +01:00
Marcel van der Veldt cbb8d76da7 Add support for vacuum cleaners to the Matter integration (#129420) 2024-10-29 16:17:40 +01:00
Erik Montnemery cce925c06c Fix bad falsy-check in homeassistant.set_location service (#129389) 2024-10-29 16:11:48 +01:00
Marco 505a4bfc34 Add Smarty versions to device (#129418) 2024-10-29 16:06:15 +01:00
Robert Resch 58e151966c Fix go2rtc no audio issue (#129428) 2024-10-29 16:01:51 +01:00
Michael 8a6c9b7afc Remove Mobile App config entries, when the related user gets removed (#129268)
* remove config entries, when related user gets removed

* add test
2024-10-29 15:53:00 +01:00
Jirka e72e2071b0 Fix typo in nest string (#129423)
Update strings.json

Fixed typos
2024-10-29 15:38:55 +01:00
epenet 5d3af27928 Set config_entry explicitly in history stats coordinator (#129417)
Set config_entry explicitely in history stats coordinator
2024-10-29 15:32:56 +01:00
Petar Petrov 5dc0bedbc4 Allow fetching HA url to display it in the network settings (#128432)
* Allow fetching HA url to display it in the network settings

* add tests

* use a constant for the url types

* just return all url types

* Prefer callback without await

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-10-29 15:28:54 +01:00
epenet 8f7ae2665c Set config_entry explicitly in switcher kis coordinator (#129419) 2024-10-29 16:14:36 +02:00
epenet 10fdf819d3 Set config_entry explicitely in scrape coordinator (#129416) 2024-10-29 14:54:24 +01:00
LG-ThinQ-Integration 02928601ef Add min, max for WATER_HEATER device (#129414)
Co-authored-by: jangwon.lee <jangwon.lee@lge.com>
2024-10-29 14:52:26 +01:00
LG-ThinQ-Integration c227f6dc2c Add timer sensor entity which has rw hour and read-only minute (#129413)
Co-authored-by: jangwon.lee <jangwon.lee@lge.com>
2024-10-29 14:44:06 +01:00
Mike Degatano 673f0224c9 Continue migration of methods from handler to aiohasupervisor (#129183) 2024-10-29 14:33:21 +01:00
Manu 79c602f59c Fix available conditions for chilling frost and stealth in Habitica (#129234)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-29 14:24:23 +01:00
Raj Laud 07c070e253 Refactor squeezebox integration media_player to use coordinator (#127695) 2024-10-29 14:21:28 +01:00
Vendetta01 9bda3bd477 Fix bosch shc multi controller support (#127844)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 14:19:33 +01:00
Marc Hörsken 2c9ad9562e Fix visualization by inverting open/closed state of patio awnings (#128079) 2024-10-29 14:09:49 +01:00
Manu c264ee22e7 Add tests for switch platform of Habitica integration (#128204) 2024-10-29 14:08:05 +01:00
J. Diego Rodríguez Royo f194a689cc Fetch power off state for Home Connect appliances' power switch (#129289) 2024-10-29 13:56:45 +01:00
David Bonnes a36b350954 Fix evohome HVAC modes for VisionPro Wifi systems (#129161)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 13:37:35 +01:00
Josef Zweck db4278fb9d Cleanup select mappings in lamarzocco (#129407) 2024-10-29 13:32:14 +01:00
David Bonnes 39ba4cff2f Refactor evohome tests as per best practice (#129229)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 13:29:10 +01:00
Christopher Fenner d68da74790 Add number entities to set target temp for cooling programs in ViCare (#127267)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 13:28:12 +01:00
Tomer Shemesh 5fc45cd736 Add support for Lutron HWQS Proc discovery (#129274) 2024-10-29 13:27:44 +01:00
Guido Schmitz 5ae2f3d081 Add own coordinator to devolo_home_network (#128159) 2024-10-29 13:23:28 +01:00
Josef Zweck 478bf643bf Add smart standby functionality to lamarzocco (#129333)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 13:22:37 +01:00
Daniel Hjelseth Høyer 7929895b11 Change Tibber request spread (#129276) 2024-10-29 13:12:07 +01:00
Erik Montnemery da11a72b4c Create repair asking user to remove duplicate config entries (#127948)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-29 13:10:56 +01:00
Mike Degatano 1649368cee Bump aiohasupervisor to 0.2.0 (#129348) 2024-10-29 13:07:59 +01:00
dontinelli a528d62c16 Add test for extended data in setup for solarlog (#129345) 2024-10-29 13:07:48 +01:00
Guido Schmitz bd13dbdad0 Use new generic notation in devolo_home_network (#129080) 2024-10-29 13:07:13 +01:00
Allen Porter 8e7ffd9e16 Update Nest configuration flow to handle upcoming changes to Pub/Sub provisioning (#128909)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-29 12:58:36 +01:00
Manu f0bff09b5e Bump habitipy to 0.3.3 (#129322) 2024-10-29 12:48:20 +01:00
J. Diego Rodríguez Royo 0e959b3019 Added deprecation to binary door sensor at Home Connect (#129245)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-29 12:46:39 +01:00
Thomas55555 983cd9c3fc Add and remove entities during runtime in Husqvarna Automower (#127878) 2024-10-29 12:46:04 +01:00
Erik Montnemery 2236ca3e12 Fix typo in cv.url_no_path (#129402) 2024-10-29 12:06:59 +01:00
Robert Resch f3afa6a7d9 Fix hassfest docker image by pinning Python 3.12 (#129403) 2024-10-29 11:57:20 +01:00
Brett Adams ce7e2e3243 Clean up SensorRestore in Tesla Fleet (#129116)
* Remove, fix, and test restore

* slightly better comment

* use restore instead

* parametrize test

* Apply suggestions from code review

* revert change to Teslemetry

* revert change to Teslemetry

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-10-29 11:41:35 +01:00
Robert Resch 13416825b1 Go2rtc server start is waiting until we got the api listen stdout line (#129391) 2024-10-29 11:28:40 +01:00
J. Nick Koston 6c664e7ba9 Bump protobuf to 5.28.3 (#129370) 2024-10-29 11:22:31 +01:00
LG-ThinQ-Integration 34359617b5 Bump thinqconnect to 0.9.9 (#129394) 2024-10-29 11:16:19 +01:00
Erik Montnemery 9e2696b9bc Report update_percentage in matter update entity (#129380) 2024-10-29 10:57:52 +01:00
Paul Bottein bf840e8bfa Use device name for matter entities (#127798) 2024-10-29 10:54:25 +01:00
Robert Resch 1f03c140f5 Bump go2rtc-client to 0.0.1b2 (#129395) 2024-10-29 10:45:00 +01:00
Marc Mueller 2de161ce0e Fix mariadb recorder tests for Python 3.13 (#129303) 2024-10-29 09:17:47 +01:00
Marc Mueller 1171106afb Run postgres job on ubuntu 24.04 [ci] (#129381) 2024-10-29 09:15:04 +01:00
Robert Resch f57ae73071 Bump webrtc-models to 0.1.0 (#129373) 2024-10-29 08:33:54 +01:00
Robert Resch 59872b5698 Enable strict typing for go2rtc (#129374) 2024-10-29 08:25:49 +01:00
Robert Resch 7cd8ea00d1 Bump uv to 0.4.28 (#129372) 2024-10-28 21:20:59 -10:00
Robert Resch 4b2f38926a Bump go2rtc binary to 1.9.5 (#129371) 2024-10-29 08:01:59 +01:00
Allen Porter 537c95cf29 Update nest to use the async WebRTC APIs (#129369)
* Update nest to use the new `async_handle_webrtc_offer` APIs.

* Close sessions when sessions end

* Switch to the correct close API
2024-10-29 07:18:59 +01:00
epenet 81a5722708 Fix flaky DHCP tests in CI (#129327) 2024-10-28 13:41:50 -10:00
Jan Bouwhuis c150b913ac Use URL validation schema for mqtt update entity_picture and remove custom implementation (#129360) 2024-10-28 23:36:17 +01:00
J. Nick Koston 3e4b67db6c Bump yarl to 1.17.0 (#129358) 2024-10-28 23:11:14 +01:00
G Johansson d727f8ff50 Clarify event tracking in docstrings for track_state_change/report (#129338)
* Clarify event tracking in docstrings for track_state_change/report

* Fixes

* Update homeassistant/helpers/event.py

* Update homeassistant/helpers/event.py

Co-authored-by: J. Nick Koston <nick@koston.org>

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-10-28 23:05:06 +01:00
G Johansson 9546bf1dee Use shorthand attribute for native value in statistics (#129355) 2024-10-28 22:43:09 +01:00
Michael Hansen dd9ce34d18 Allow a fixed number of ffmpeg proxy conversions per device (#129246)
Allow a fixed number of conversions per device
2024-10-28 13:26:43 -07:00
G Johansson 73f2d972e4 Use shorthand attribute for available in statistics (#129354) 2024-10-28 21:01:34 +01:00
G Johansson 7d699c6c35 Fix calculation of attributes in statistics (#128475)
* Fix calculation of attributes in statistics

* Cleanup

* Mods

* Fix device class

* Typing

* Mod uom calc

* Fix UoM

* Fix docstrings

* state class docstring
2024-10-28 19:45:47 +01:00
dontinelli 21f23f67f4 Fix spelling mistake in notify (#129349) 2024-10-28 18:39:36 +01:00
Joost Lekkerkerker 8874ba2779 Add LG ThinQ to LG brand (#129346) 2024-10-28 18:24:24 +01:00
LG-ThinQ-Integration 420538e6e7 Add LG ThinQ integration (#129299)
Co-authored-by: jangwon.lee <jangwon.lee@lge.com>
2024-10-28 17:22:24 +01:00
dotvav 8eb68b54d9 Palazzetti integration (#128259)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-28 17:19:05 +01:00
Robert Resch 80202f33cb Fix go2rtc tests (#129342) 2024-10-28 17:12:28 +01:00
YogevBokobza c24579bfb2 Add switcher s12 support (#127277)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2024-10-28 16:57:24 +01:00
Noah Husby 21256c4529 Remove media player shuffle check from Cambridge Audio (#129235)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-28 16:57:09 +01:00
J. Diego Rodríguez Royo 668626b920 Add ServiceValidationError to Home Connect (#129309)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-28 16:48:56 +01:00
Wendelin cbfa3bb56d Hassio logs boots (#129151)
* Add hassio logs/boots proxy settings

* Add hassio http tests
2024-10-28 16:41:14 +01:00
Robert Resch 536fcf02d7 Fix CI by running gen_requirements_all.py (#129339) 2024-10-28 16:39:49 +01:00
Erik Montnemery a8ac3acbbe Bump pychromecast to 14.0.5 (#129251) 2024-10-28 16:07:23 +01:00
TheJulianJES 7980155375 Bump ZHA to 0.0.36 (#129247) 2024-10-28 16:07:04 +01:00
Robert Resch aa855e31c8 Convert async_get_webrtc_client_configuration to a callback (#129329) 2024-10-28 15:47:22 +01:00
Robert Resch 675ee8e813 Add async webrtc offer support (#127981)
* Add async webrtc offer support

* Create dataclass for messages

* Send session ID over websocket

* Fixes

* Rename

* Implement some review findings

* Add WebRTCError and small renames

* Use dedicated function instead of inspec

* Update go2rtc-client to 0.0.1b1

* Improve checking for sync offer

* Revert change as not needed anymore

* Typo

* Fix tests

* Add missing go2rtc tests

* Move webrtc offer tests to test_webrtc file

* Add ws camera/webrtc/candidate tests

* Add missing tests

* Implement suggestions

* Implement review changes

* rename

* Revert test to use ws endpoints

* Change doc string

* Don't import from submodule

* Get type form class name

* Update homeassistant/components/camera/__init__.py

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

* Adopt tests

* Apply suggestions from code review

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

* Fix tests

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
2024-10-28 15:46:15 +01:00
unfug-at-github 50ccce7387 React to state report events to increase sample size of statistics (#129211)
* react to state reported events to increase sample size

* added test case for timinig and minor corrections
2024-10-28 14:41:48 +01:00
Markus Jacobsen 40b561ea69 Add shuffle media controls to Bang & Olufsen (#129325) 2024-10-28 13:39:49 +01:00
G Johansson a0f73bd30f Add reconfigure flow to Sensibo (#129280) 2024-10-28 12:29:06 +01:00
Tsvi Mostovicz 1b7fcce42d Assert keys exist in Jewish calendar tests (#129295) 2024-10-28 12:23:45 +01:00
J. Nick Koston 4749af6e90 Convert WebSocket messages to bytes before passing them to send_message (#129300) 2024-10-28 12:21:12 +01:00
Maikel Punie f7ad40263b Bump velbusaio to 2024.10.0 (#129305) 2024-10-28 12:19:08 +01:00
epenet e5b25bfa58 Use reauth_confirm in ovo_energy (#129306) 2024-10-28 11:52:38 +01:00
epenet 1d23adcda3 Use start_reauth_flow in system_bridge tests (#129318) 2024-10-28 11:52:13 +01:00
epenet 0216d36ab7 Use start_reauth_flow in permobil tests (#129314) 2024-10-28 11:51:16 +01:00
epenet 2bec20ad76 Ensure config entry is added to hass in reauth/reconfigure tests (#129315) 2024-10-28 11:03:42 +01:00
G Johansson 93c1245b0f Use start_reauth_flow in apple_tv test (#129313)
* Use start_reauth_flow in apple_tv test

* Fix
2024-10-28 10:42:19 +01:00
epenet 72504d7619 Use async_start_reauth helper in broadlink (#129308) 2024-10-28 09:00:11 +01:00
G Johansson 320aa34d39 Use async_start_reauth in xiaomi_miio (#129282)
* Use async_start_reauth in xiaomi_miio

* Apply suggestions from code review

Co-authored-by: Teemu R. <tpr@iki.fi>

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Teemu R. <tpr@iki.fi>
2024-10-28 08:37:38 +01:00
G Johansson 87f2a4242e Use async_start_reauth in blink (#129281) 2024-10-28 07:57:18 +01:00
Joel Hawksley 9bf0cbd659 Omit declined Google Calendar events (#128900)
* Omit decline Google Calendar events

* move comment to top of function and update

* Apply suggestions from code review

* import ResponseStatus
2024-10-27 21:54:09 -07:00
Franck Nijhof b1470fd9b8 Merge branch 'master' into dev 2024-10-28 02:46:15 +01:00
Nicolás Alonso 08016dc3b6 Lazy discover for dmaker.fan.1c (#129297) 2024-10-28 02:09:08 +01:00
G Johansson 7a448f5528 Add battery binary sensor to Yale Smart Alarm (#129277)
* Add battery binary sensor to Yale Smart Alarm

* Fix docstrings
2024-10-27 20:57:10 +01:00
Michael 4ac23bf14c Add diagnostics platform to PEGELONLINE (#129279)
add diagnostics platform
2024-10-27 20:36:56 +01:00
Michael bc708dee30 Mark PEGELONLINE entries as service (#129278)
set entry_type service
2024-10-27 20:35:19 +01:00
Erik Montnemery 2888e5748e Fix ESPHome media proxy exit criteria (#129267) 2024-10-27 12:39:49 -05:00
Simone Chemelli 88f0a33e69 Update uptime deviation interval for Vodafone Station (#129257)
update uptime deviation interval
2024-10-27 15:40:58 +01:00
Michael 3165f92b6b Fix conntected_to attribute of device tracker entities in a AVM Fritz mesh setup (#129259)
ignore orphan node links
2024-10-27 14:42:43 +01:00
Marc Mueller 3bd0fca633 Properly validate License-Expression data for licenses check (#129216) 2024-10-27 10:43:21 +01:00
tleydxdy cdff10d281 Add new ZHA Inovelli blue switch strings (#127124)
ref: https://github.com/zigpy/zha/pull/203
2024-10-27 05:33:06 +01:00
Álvaro Fernández Rojas e425741c34 Update aioairzone-cloud to v0.6.10 (#129227) 2024-10-26 13:19:34 -10:00
Marc Mueller 20a367b243 Fix zha tests for Python 3.13 (#129241) 2024-10-27 00:18:21 +02:00
Manu fdded9e7ee Add tests for todo platform of Habitica integration (#128199)
* Add tests for todo platform

* refactor mock_called_with

* update tests
2024-10-26 10:48:07 -07:00
Galorhallen 7d29bff136 Update govee-local-api to 1.5.3 (#129226) 2024-10-26 18:28:22 +02:00
G Johansson 0abfbeed3c Fix flaky gardena_ble test (#129225) 2024-10-26 17:57:00 +02:00
Franck Nijhof 35b7c3038a Revert "Fix unused snapshots not triggering failure in CI" (#129223)
Revert "Fix unused snapshots not triggering failure in CI (#128162)"

This reverts commit e888a95bd1.
2024-10-26 16:12:47 +02:00
boergegrunicke 46dd96a4b7 Add dishwasher salt and rinse aid nearly empty sensors (#127762)
Co-authored-by: Robert Contreras <beastie29a@users.noreply.github.com>
2024-10-26 16:09:11 +02:00
dontinelli 788232ca35 Add and remove plants (i.e. devices) dynamically in fyta (#129221) 2024-10-26 15:35:43 +02:00
J. Nick Koston 3b458738e0 Fix setting brightness to 0 in HomeKit when the On characteristic is not sent (#129201) 2024-10-26 15:29:15 +02:00
David Bonnes 2c8fc67ab1 Fix evohome failing to start with 'NoneType' object has no attribute 'get' (#129222) 2024-10-26 15:24:41 +02:00
David Bonnes 9b3ed3ed72 Add tests of evohome integration-specific services (#129206)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-26 14:44:46 +02:00
Joost Lekkerkerker c59197e87a Add more spotify sensors (#129215) 2024-10-26 14:43:32 +02:00
Álvaro Fernández Rojas 03e3c88d8b Update aioairzone-cloud to v0.6.9 (#129217) 2024-10-26 14:37:58 +02:00
Joost Lekkerkerker 39693786ef Remove remnants of removed list_events action (#129210) 2024-10-26 14:37:05 +02:00
dontinelli 357c324df1 Add logger for fyta library in manifest.json (#129218) 2024-10-26 14:36:07 +02:00
dontinelli 650482208c Bump fyta_cli to 0.6.10 (#129220) 2024-10-26 14:34:45 +02:00
J. Diego Rodríguez Royo 2acad4a78c Home connect number platform with temperature set points entities (#126145) 2024-10-26 14:04:52 +02:00
jb101010-2 65ee4e1916 Bump pysuezV2 to 0.2.2 (#129205)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-26 11:44:02 +02:00
J. Diego Rodríguez Royo 275bbc81f0 Add Time platform with alarm clock to Home Connect (#126155)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-26 11:42:51 +02:00
Marc Mueller beafcf74ab Update zeroconf to 0.136.0 (#129204) 2024-10-26 11:35:00 +02:00
Marc Mueller e47909bb3e Update gardena-bluetooth to 1.4.4 (#129202) 2024-10-26 11:34:32 +02:00
David Bonnes 0b3b9c2257 Make minor fixes / doc tweaks to evohome's WaterHeater tests (#129138) 2024-10-26 10:52:32 +02:00
Marc Mueller 8fb7a7e4cd Refactor licenses check (#129194) 2024-10-26 10:30:10 +02:00
unfug-at-github c5ed148c52 Fix race condition in statistics that created spikes (#129066)
* fixed race condition and added test case for updates before db load

* removed duplicated code

* improved comments, removed superfluous errors / assertions

* allow both possible outcomes of race condition

* use approx for float comparison

* Update tests/components/statistics/test_sensor.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* force new state before database load in race condition test

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2024-10-26 09:23:47 +02:00
IceBotYT e774c710a8 Bump lacrosse_view to 1.0.3 (#129174)
Add Pydantic v2 support to LaCrosse View
2024-10-26 08:59:08 +02:00
Jan Bouwhuis d237180a98 Allow re-discovery of mqtt integration config payloads (#127362) 2024-10-26 07:21:52 +02:00
Erik Montnemery d8b618f7c3 Remove support for live recorder data migration of context ids (#125309) 2024-10-26 07:19:03 +02:00
epenet e888a95bd1 Fix unused snapshots not triggering failure in CI (#128162) 2024-10-26 07:15:51 +02:00
Joost Lekkerkerker 36c2404a46 Add base entity to Spotify (#128847)
Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
2024-10-26 07:09:18 +02:00
J. Nick Koston ba673beb82 Bump anyio to 4.6.2.post1 (#129199) 2024-10-26 07:06:27 +02:00
Erik Montnemery 4b56701152 Move core config class to core_config.py (#129163) 2024-10-26 07:00:31 +02:00
J. Nick Koston 59227116f3 Ensure go2rtc server starts using posix_spawn/vfork (#129196) 2024-10-26 06:51:29 +02:00
J. Nick Koston 9b0975b2ac Fix rainmachine update entities missing display_precision (#129195) 2024-10-26 06:29:39 +02:00
epenet 3a39a5caa3 Move brunt coordinator to separate module (#129090) 2024-10-26 02:30:59 +02:00
epenet 93e270f379 Use runtime_data in aranet (#129155) 2024-10-26 02:30:48 +02:00
epenet 98c81fa2af Move airthings coordinator to separate module (#129158) 2024-10-26 02:29:57 +02:00
Joost Lekkerkerker 1bb32a05a9 Migrate Smarty to has entity name (#129145) 2024-10-26 02:28:26 +02:00
Sid 5dd4b77270 Add JSON schema for manifest.json (#128560) 2024-10-26 02:10:58 +02:00
Andre Lengwenus 737d1aac7c Bump lcn-frontend to 0.2.0 (#129061) 2024-10-26 01:57:56 +02:00
Maciej Bieniek 886feae4ca Add support for Xiaomi Miio Standing Fan 2 (dmaker.fan.p18) (#129160) 2024-10-26 01:52:18 +02:00
Marc Mueller 1dfe26f14f Update apple_weatherkit to 1.1.3 (#129193) 2024-10-26 01:51:28 +02:00
Marc Mueller d66fcd23df Update radios to 0.3.2 and pycountry to 24.6.1 (#129186) 2024-10-26 01:49:26 +02:00
Marc Mueller bdfb47e999 Fix AsyncMock imports (#129192) 2024-10-26 01:47:27 +02:00
Paulus Schoutsen 10300cc478 Create a script service schema based on fields (#128622) 2024-10-26 01:05:00 +02:00
Marc Mueller ababa639b3 Fix cambridge_audio RuntimeWarning during tests (#129191) 2024-10-26 01:03:52 +02:00
Bouwe Westerdijk 9f6569d658 Bump plugwise to v1.4.4 (#129170) 2024-10-25 23:55:28 +02:00
J. Nick Koston 24c22ebdc7 Fix powerview entity unique id migration when the config entry unique id is missing (#129188)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 11:41:07 -10:00
Markus Jacobsen 6c365fffde Add media seek for sources other than Deezer for Bang & Olufsen (#128661)
* Add seeking for sources other than Deezer

* Add is_seekable attribute to fallback sources and BangOlufsenSource
Add testing

* Update comment

* Use support flags instead of raising errors when seeking on incompatible source
2024-10-25 23:34:39 +02:00
Marc Mueller dbb80dd6c0 Update krakenex to 2.2.2 (#129185) 2024-10-25 22:38:02 +02:00
Artur Pragacz 624834de9c Fix service target devices by label (#127229)
* Fix service target devices by label

* More explicit test
2024-10-25 21:30:04 +02:00
Franck Nijhof d31995f878 2024.10.4 (#129181) 2024-10-25 21:27:01 +02:00
Marc Mueller 017b1cae26 Update aiooui to 0.1.7 (#129179) 2024-10-25 21:24:43 +02:00
Franck Nijhof c09f15b0e9 Bump version to 2024.10.4 2024-10-25 20:49:36 +02:00
Keilin Bickar 68284bed74 Add coordinators to Sense (#129171) 2024-10-25 20:45:55 +02:00
Joost Lekkerkerker 9a44d668d6 Bump nyt_games to 0.4.4 (#129152) 2024-10-25 20:43:16 +02:00
Joost Lekkerkerker 67e0197a7a Fix NYT Games connection max streak (#129149) 2024-10-25 20:43:09 +02:00
Guido Schmitz a5a8cfa17d Fix adding multiple devices simultaneously to devolo Home Network's device tracker (#129082) 2024-10-25 20:43:02 +02:00
tronikos 60c3e701e9 Partially revert "LLM Tool parameters check (#123621)" (#129064) 2024-10-25 20:42:55 +02:00
Bram Kragten b9b129dcf5 Update frontend to 20241002.4 (#129049) 2024-10-25 20:42:48 +02:00
Daniel Albers d882ab236a Remove DHCP match from awair (#129047)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-25 20:42:40 +02:00
Joost Lekkerkerker 140cc0e486 Bump yt-dlp to 2024.10.22 (#129034) 2024-10-25 20:42:17 +02:00
Guido Schmitz 6ac7c0f893 Fix devolo_home_network devices not reporting a MAC address (#129021) 2024-10-25 20:42:11 +02:00
J. Nick Koston 096d50617f Fix cancellation leaking upward from the timeout util (#129003) 2024-10-25 20:42:04 +02:00
Simone Chemelli 9dd8c0cc4f Fix uptime floating values for Vodafone Station (#128974) 2024-10-25 20:41:57 +02:00
Maikel Punie de0fab86ec Bump pyduotecno to 2024.10.1 (#128968) 2024-10-25 20:39:38 +02:00
Noah Husby bb36dd3893 Use translated exceptions for Cambridge Audio (#129177) 2024-10-25 20:30:49 +02:00
Simone Chemelli ada837ee95 Add diagnostics to Vodafone Station (#128923)
* Add diagnostics to Vodafone Station

* cleanup and exclude props based on date
2024-10-25 20:22:47 +02:00
Daniel Hjelseth Høyer 67e73173f6 Bump pyTibber to 0.30.3 (#128860) 2024-10-25 20:22:40 +02:00
Jan Bouwhuis 4b63829eef Allow to set entity picture on mqtt entity platforms (#128404) 2024-10-25 20:16:11 +02:00
Simone Chemelli 029411d3fa Add diagnostics to Comelit SimpleHome (#128794)
* Add diagnostics to Comelit SimpleHome

* add test

* add missing tests

* introduce SnapshotAssertion

* cleanup

* exclude date based props
2024-10-25 20:12:54 +02:00
Steven B. 6ba033f934 Bump ring-doorbell library to 0.9.8 (#128662) 2024-10-25 20:12:48 +02:00
Simon Lamon 3734fa948f LinkPlay multiroom support (#127862) 2024-10-25 20:12:42 +02:00
Steven B. 336742e335 Bump ring-doorbell to 0.9.7 (#127554) 2024-10-25 20:12:41 +02:00
Markus Jacobsen 66ca424d3a Add repeat media controls to Bang & Olufsen (#128170)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 20:10:08 +02:00
Heiko Carrasco 2da0a91a36 Add lock to switchbot_cloud (#115128)
Co-authored-by: Ravaka Razafimanantsoa <3774520+SeraphicRav@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2024-10-25 20:09:14 +02:00
J. Diego Rodríguez Royo fee1bde231 Fix program switches unique ID at Home Connect (#128397) 2024-10-25 20:05:29 +02:00
mkmer 4a94430bf0 Handle temprorary hold in Honeywell (#128460) 2024-10-25 20:05:14 +02:00
David Bonnes cc337f7b1e Fix evohome regression preventing helpful messages when setup fails (#126441)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-10-25 20:05:05 +02:00
J. Diego Rodríguez Royo d8a06777fe Fix coffee maker device type name at applicances with programs list at Home Connect (#128538) 2024-10-25 20:04:53 +02:00
Marc Mueller 9207eedbfb Update heatmiserV3 to 2.0.3 (#129175) 2024-10-25 20:04:37 +02:00
bru73f0rc3 c97b832648 Add more Vesync IDs for the Vital200S (#127616) 2024-10-25 18:58:54 +02:00
alorente 4ef629f79d Remove check for obsolete "rain_product_available" in meteo_france (#128533)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 18:58:34 +02:00
Michael Hansen 0b4e3c3db5 Remove category from Assist satellite entities (#129172) 2024-10-25 18:43:42 +02:00
Noah Husby f12cc523b4 Enforce strict typing for Cambridge Audio (#129004) 2024-10-25 18:41:33 +02:00
Marc Mueller 5c3c9d2ed1 Update goslide-api to 0.7.0 (#129168) 2024-10-25 18:33:37 +02:00
Russell Cloran 3ac3673326 Improve prometheus metric name sanitization (#126967) 2024-10-25 18:33:16 +02:00
cdheiser 1a3940575e Use TAP to activate Lutron scenes (#127899) 2024-10-25 18:30:19 +02:00
Noah Husby 16c8b1efab Add all models to diagnostics for Cambridge Audio (#129157) 2024-10-25 18:20:54 +02:00
Marc Hörsken 0e789be09f Add light support to WMS WebControl pro (#128308)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 18:20:40 +02:00
J. Diego Rodríguez Royo a948c7d69d Door entity as enum sensor at Home Connect (#126158) 2024-10-25 18:18:21 +02:00
Marc Mueller d8ec0103a9 Update zeversolar to 0.3.2 (#129167) 2024-10-25 18:14:04 +02:00
Isaac 50161670ce Add "Albums" sensor to Lidarr (#125631)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 18:13:03 +02:00
Noah Husby c1f612dce1 Bump aiostreammagic to 2.8.4 (#129166) 2024-10-25 18:10:38 +02:00
J. Diego Rodríguez Royo 6fb74482d7 Add Diegorro98 as Home Connect code owner (#129169) 2024-10-25 18:06:22 +02:00
dontinelli 4b680ffa5f Dynamic add/remove devices for solarlog (#128668)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-10-25 18:02:14 +02:00
Marc Mueller c71c8d56ce Update pyxeoma to 1.4.2 (#129164) 2024-10-25 18:01:21 +02:00
IceBotYT 295ae7b4bc Add support for Mighty Mule MMS100 to Nice G.O. (#127765) 2024-10-25 17:49:32 +02:00
Marc Mueller 839c884cef Update aioopenexchangerates to 0.6.8 (#129162) 2024-10-25 17:40:02 +02:00
Jeef 13ffe7acfb Add Intellifire cloud/local connectivity sensors (#127122) 2024-10-25 17:23:51 +02:00
Manu 39a0c0d96e Add List access sensor to Bring integration (#126844) 2024-10-25 17:20:31 +02:00
Keilin Bickar a95a542148 Update sense-energy to 0.13.2 (#128670) 2024-10-25 16:59:39 +02:00
Alistair Francis b3cb2ac3ee Add husqvarna automower ble integration (#108326)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-10-25 16:54:02 +02:00
Andre Lengwenus 759fe54132 Fix transition config storage in LCN light and scene platform (#127847) 2024-10-25 16:25:41 +02:00
Noah Husby 519a888e82 Bump aiostreammagic to 2.8.3 (#129113) 2024-10-25 16:21:08 +02:00
Erik Montnemery 4f1e4e7471 Include go2rtc in default_config (#129144)
* Include go2rtc in default_config

* Fail if binary not found in docker environment
2024-10-25 16:10:14 +02:00
epenet 7b8a32f630 Cleanup hass.data default in airtouch5 (#129156) 2024-10-25 15:37:07 +02:00
ashionky 92d91a65bb Add refoss em16 device model (#126798) 2024-10-25 15:22:24 +02:00
rappenze dab5289177 Add opening closing state to fibaro cover (#126958) 2024-10-25 15:10:20 +02:00
J. Diego Rodríguez Royo a77cb1e579 Home connect light generalization and RGB support (#126144) 2024-10-25 15:08:50 +02:00
Joost Lekkerkerker 01bdda0ae6 Bump nyt_games to 0.4.4 (#129152) 2024-10-25 14:46:43 +02:00
Joost Lekkerkerker fbe35e6e6b Fix NYT Games connection max streak (#129149) 2024-10-25 14:19:46 +02:00
Alexandre CUER a3cd74e30b Bump pymoncms library to version 0.1.1 (#129135) 2024-10-25 14:15:35 +02:00
YogevBokobza dbd4781de1 Bump aioswitcher to 4.2.0 (#129118)
* bump aioswitcher to 4.2.0

* Update cover.py

* switcher fix based on requested changes
2024-10-25 14:41:49 +03:00
Anton Tolchanov 6d48316436 Avoid creating Prometheus metrics for non-numeric states (#127262) 2024-10-25 13:31:30 +02:00
David Bonnes cca6965cd1 Fix evohome regression preventing helpful messages when setup fails (#126441)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-10-25 13:23:17 +02:00
897 changed files with 39646 additions and 8399 deletions
+7 -1
View File
@@ -58,7 +58,13 @@
],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
},
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
"url": "./script/json_schemas/manifest_schema.json"
}
]
}
}
}
+4 -2
View File
@@ -1102,7 +1102,7 @@ jobs:
./script/check_dirty
pytest-postgres:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
services:
postgres:
image: ${{ matrix.postgresql-group }}
@@ -1142,7 +1142,9 @@ jobs:
sudo apt-get -y install \
bluez \
ffmpeg \
libturbojpeg \
libturbojpeg
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
+2
View File
@@ -124,6 +124,7 @@ homeassistant.components.bryant_evolution.*
homeassistant.components.bthome.*
homeassistant.components.button.*
homeassistant.components.calendar.*
homeassistant.components.cambridge_audio.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cert_expiry.*
@@ -208,6 +209,7 @@ homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
homeassistant.components.glances.*
homeassistant.components.go2rtc.*
homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.*
+9 -1
View File
@@ -6,5 +6,13 @@
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment"
"pylint.importStrategy": "fromEnvironment",
"json.schemas": [
{
"fileMatch": [
"homeassistant/components/*/manifest.json"
],
"url": "./script/json_schemas/manifest_schema.json"
}
]
}
+8 -2
View File
@@ -617,8 +617,8 @@ build.json @home-assistant/supervisor
/tests/components/hlk_sw16/ @jameshilliard
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
/tests/components/holiday/ @jrieger @gjohansson-ST
/homeassistant/components/home_connect/ @DavidMStraub
/tests/components/home_connect/ @DavidMStraub
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
/tests/components/home_connect/ @DavidMStraub @Diegorro98
/homeassistant/components/homeassistant/ @home-assistant/core
/tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
@@ -659,6 +659,8 @@ build.json @home-assistant/supervisor
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555
/tests/components/husqvarna_automower/ @Thomas55555
/homeassistant/components/husqvarna_automower_ble/ @alistair23
/tests/components/husqvarna_automower_ble/ @alistair23
/homeassistant/components/huum/ @frwickst
/tests/components/huum/ @frwickst
/homeassistant/components/hvv_departures/ @vigonotion
@@ -819,6 +821,8 @@ build.json @home-assistant/supervisor
/tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi
@@ -1089,6 +1093,8 @@ build.json @home-assistant/supervisor
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
/tests/components/p1_monitor/ @klaasnicolaas
/homeassistant/components/palazzetti/ @dotvav
/tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT
+4 -3
View File
@@ -7,12 +7,13 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=240000 \
UV_SYSTEM_PYTHON=true
UV_SYSTEM_PYTHON=true \
UV_NO_CACHE=true
ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.4.22
RUN pip3 install uv==0.4.28
WORKDIR /usr/src
@@ -54,7 +55,7 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
+5
View File
@@ -0,0 +1,5 @@
{
"domain": "husqvarna",
"name": "Husqvarna",
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"domain": "lg",
"name": "LG",
"integrations": ["lg_netcast", "lg_soundbar", "webostv"]
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
}
@@ -7,7 +7,6 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
@@ -18,6 +17,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DOMAIN
@@ -55,6 +55,7 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name="Advantage Air",
update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
+2
View File
@@ -249,6 +249,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Rain",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
key=ATTR_API_RAIN_PROB,
@@ -263,6 +264,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Snow",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
key=ATTR_API_SNOW_PROB,
@@ -42,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=_update_method,
update_interval=SCAN_INTERVAL,
@@ -2,75 +2,27 @@
from __future__ import annotations
from datetime import timedelta
import logging
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP
from .const import MAX_RETRIES_AFTER_STARTUP
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
async def async_setup_entry(
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
) -> bool:
"""Set up Airthings BLE device from a config entry."""
hass.data.setdefault(DOMAIN, {})
address = entry.unique_id
is_metric = hass.config.units is METRIC_SYSTEM
assert address is not None
await close_stale_connections_by_address(address)
ble_device = bluetooth.async_ble_device_from_address(hass, address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Airthings device with address {address}"
)
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
async def _async_update_method() -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await airthings.update_device(ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data
coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN,
update_method=_async_update_method,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
# Once its setup and we know we are not going to delay
# the startup of Home Assistant, we can set the max attempts
# to a higher value. If the first connection attempt fails,
# Home Assistant's built-in retry logic will take over.
airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
entry.runtime_data = coordinator
@@ -0,0 +1,68 @@
"""The Airthings BLE integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from bleak.backends.device import BLEDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
"""Class to manage fetching Airthings BLE data."""
ble_device: BLEDevice
config_entry: AirthingsBLEConfigEntry
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
"""Initialize the coordinator."""
self.airthings = AirthingsBluetoothDeviceData(
_LOGGER, hass.config.units is METRIC_SYSTEM
)
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
async def _async_setup(self) -> None:
"""Set up the coordinator."""
address = self.config_entry.unique_id
assert address is not None
await close_stale_connections_by_address(address)
ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Airthings device with address {address}"
)
self.ble_device = ble_device
async def _async_update_data(self) -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data
@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==0.9.1"]
"requirements": ["airthings-ble==0.9.2"]
}
@@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -9,8 +9,6 @@ from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
@@ -19,8 +17,6 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
"""Set up Airtouch 5 from a config entry."""
hass.data.setdefault(DOMAIN, {})
# Create API instance
host = entry.data[CONF_HOST]
client = Airtouch5SimpleClient(host)
@@ -204,6 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
config_entry=entry,
name=async_get_geography_id(entry.data),
# We give a placeholder update interval in order to create the coordinator;
# then, below, we use the coordinator's presence (along with any other
@@ -81,6 +81,7 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
config_entry=entry,
name="Node/Pro data",
update_interval=UPDATE_INTERVAL,
update_method=async_get_data,
@@ -310,6 +310,10 @@ class AirzoneDeviceClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
params[API_SETPOINT] = {
@@ -333,9 +337,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
}
await self._async_update_params(params)
if ATTR_HVAC_MODE in kwargs:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
class AirzoneDeviceGroupClimate(AirzoneClimate):
"""Define an Airzone Cloud DeviceGroup base class."""
@@ -366,6 +367,10 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
params[API_PARAMS] = {
@@ -376,9 +381,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
}
await self._async_update_params(params)
if ATTR_HVAC_MODE in kwargs:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""
params: dict[str, Any] = {
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.8"]
"requirements": ["aioairzone-cloud==0.6.10"]
}
+7 -1
View File
@@ -1083,7 +1083,13 @@ async def async_api_arm(
arm_state = directive.payload["armState"]
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.state != alarm_control_panel.AlarmControlPanelState.DISARMED:
# Per Alexa Documentation: users are not allowed to switch from armed_away
# directly to another armed state without first disarming the system.
# https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-securitypanelcontroller.html#arming
if (
entity.state == alarm_control_panel.AlarmControlPanelState.ARMED_AWAY
and arm_state != "ARMED_AWAY"
):
msg = "You must disarm the system before you can set the requested arm state."
raise AlexaSecurityPanelAuthorizationRequired(msg)
@@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.loader import (
@@ -136,7 +137,7 @@ class Analytics:
@property
def supervisor(self) -> bool:
"""Return bool if a supervisor is present."""
return hassio.is_hassio(self.hass)
return is_hassio(self.hass)
async def load(self) -> None:
"""Load preferences."""
@@ -1,7 +1,7 @@
{
"domain": "analytics",
"name": "Analytics",
"after_dependencies": ["energy", "recorder"],
"after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core", "@ludeeus"],
"dependencies": ["api", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
@@ -27,6 +27,7 @@ from homeassistant.helpers.selector import (
)
from .const import (
CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -55,8 +56,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
if all(
[
not user_input.get(CONF_TRACKED_ADDONS),
not user_input.get(CONF_TRACKED_INTEGRATIONS),
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
]
):
errors["base"] = "no_integrations_selected"
else:
@@ -64,6 +69,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
title="Home Assistant Analytics Insights",
data={},
options={
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
@@ -77,6 +83,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
session=async_get_clientsession(self.hass)
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
@@ -99,6 +106,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
data_schema=vol.Schema(
{
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
SelectSelectorConfig(
options=list(addons),
multiple=True,
sort=True,
)
),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
@@ -127,14 +141,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Manage the options."""
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
if all(
[
not user_input.get(CONF_TRACKED_ADDONS),
not user_input.get(CONF_TRACKED_INTEGRATIONS),
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
]
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="",
data={
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
@@ -148,6 +167,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
session=async_get_clientsession(self.hass)
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
@@ -168,6 +188,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
SelectSelectorConfig(
options=list(addons),
multiple=True,
sort=True,
)
),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
@@ -4,6 +4,7 @@ import logging
DOMAIN = "analytics_insights"
CONF_TRACKED_ADDONS = "tracked_addons"
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
@@ -12,11 +12,13 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsConnectionError,
HomeassistantAnalyticsNotModifiedError,
)
from python_homeassistant_analytics.models import Addon
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS,
DOMAIN,
@@ -33,6 +35,7 @@ class AnalyticsData:
active_installations: int
reports_integrations: int
addons: dict[str, int]
core_integrations: dict[str, int]
custom_integrations: dict[str, int]
@@ -53,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
update_interval=timedelta(hours=12),
)
self._client = client
self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
self._tracked_integrations = self.config_entry.options[
CONF_TRACKED_INTEGRATIONS
]
@@ -62,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
async def _async_update_data(self) -> AnalyticsData:
try:
addons_data = await self._client.get_addons()
data = await self._client.get_current_analytics()
custom_data = await self._client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError as err:
@@ -70,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
) from err
except HomeassistantAnalyticsNotModifiedError:
return self.data
addons = {
addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
}
core_integrations = {
integration: data.integrations.get(integration, 0)
for integration in self._tracked_integrations
@@ -81,11 +89,19 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
return AnalyticsData(
data.active_installations,
data.reports_integrations,
addons,
core_integrations,
custom_integrations,
)
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
"""Get addon value."""
if name_slug in data:
return data[name_slug].total
return 0
def get_custom_integration_value(
data: dict[str, CustomIntegration], domain: str
) -> int:
@@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[AnalyticsData], StateType]
def get_addon_entity_description(
name_slug: str,
) -> AnalyticsSensorEntityDescription:
"""Get addon entity description."""
return AnalyticsSensorEntityDescription(
key=f"addon_{name_slug}_active_installations",
translation_key="addons",
name=name_slug,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.addons.get(name_slug),
)
def get_core_integration_entity_description(
domain: str, name: str
) -> AnalyticsSensorEntityDescription:
@@ -89,6 +103,13 @@ async def async_setup_entry(
analytics_data.coordinator
)
entities: list[HomeassistantAnalyticsSensor] = []
entities.extend(
HomeassistantAnalyticsSensor(
coordinator,
get_addon_entity_description(addon_name_slug),
)
for addon_name_slug in coordinator.data.addons
)
entities.extend(
HomeassistantAnalyticsSensor(
coordinator,
@@ -3,10 +3,12 @@
"step": {
"user": {
"data": {
"tracked_addons": "Addons",
"tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations"
},
"data_description": {
"tracked_addons": "Select the addons you want to track",
"tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track"
}
@@ -24,10 +26,12 @@
"step": {
"init": {
"data": {
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
},
"data_description": {
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
}
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
import os
from typing import Any
@@ -40,6 +41,7 @@ from .const import (
CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT,
CONF_ADBKEY,
CONF_SCREENCAP_INTERVAL,
CONF_STATE_DETECTION_RULES,
DEFAULT_ADB_SERVER_PORT,
DEVICE_ANDROIDTV,
@@ -66,6 +68,8 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_LOGGER = logging.getLogger(__name__)
@dataclass
class AndroidTVRuntimeData:
@@ -157,6 +161,32 @@ async def async_connect_androidtv(
return aftv, None
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
)
if entry.version == 1:
new_options = {**entry.options}
# Migrate MinorVersion 1 -> MinorVersion 2: New option
if entry.minor_version < 2:
new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
hass.config_entries.async_update_entry(
entry, options=new_options, minor_version=2, version=1
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
entry.version,
entry.minor_version,
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
"""Set up Android Debug Bridge platform."""
@@ -34,7 +34,7 @@ from .const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES,
CONF_SCREENCAP,
CONF_SCREENCAP_INTERVAL,
CONF_STATE_DETECTION_RULES,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
@@ -43,7 +43,7 @@ from .const import (
DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES,
DEFAULT_PORT,
DEFAULT_SCREENCAP,
DEFAULT_SCREENCAP_INTERVAL,
DEVICE_CLASSES,
DOMAIN,
PROP_ETHMAC,
@@ -76,6 +76,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
MINOR_VERSION = 2
@callback
def _show_setup_form(
@@ -253,10 +254,12 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
),
): bool,
vol.Optional(
CONF_SCREENCAP,
default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
): bool,
vol.Required(
CONF_SCREENCAP_INTERVAL,
default=options.get(
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
),
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
vol.Optional(
CONF_TURN_OFF_COMMAND,
description={
+2 -1
View File
@@ -9,6 +9,7 @@ CONF_APPS = "apps"
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
CONF_GET_SOURCES = "get_sources"
CONF_SCREENCAP = "screencap"
CONF_SCREENCAP_INTERVAL = "screencap_interval"
CONF_STATE_DETECTION_RULES = "state_detection_rules"
CONF_TURN_OFF_COMMAND = "turn_off_command"
CONF_TURN_ON_COMMAND = "turn_on_command"
@@ -18,7 +19,7 @@ DEFAULT_DEVICE_CLASS = "auto"
DEFAULT_EXCLUDE_UNNAMED_APPS = False
DEFAULT_GET_SOURCES = True
DEFAULT_PORT = 5555
DEFAULT_SCREENCAP = True
DEFAULT_SCREENCAP_INTERVAL = 5
DEVICE_ANDROIDTV = "androidtv"
DEVICE_FIRETV = "firetv"
@@ -2,10 +2,9 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import hashlib
import logging
from typing import Any
from androidtv.constants import APPS, KEYS
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
@@ -23,19 +22,19 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle
from homeassistant.util.dt import utcnow
from . import AndroidTVConfigEntry
from .const import (
CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES,
CONF_SCREENCAP,
CONF_SCREENCAP_INTERVAL,
CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND,
DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES,
DEFAULT_SCREENCAP,
DEFAULT_SCREENCAP_INTERVAL,
DEVICE_ANDROIDTV,
SIGNAL_CONFIG_ENTITY,
)
@@ -48,8 +47,6 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path"
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
@@ -125,7 +122,8 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._app_name_to_id: dict[str, str] = {}
self._get_sources = DEFAULT_GET_SOURCES
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
self._screencap = DEFAULT_SCREENCAP
self._screencap_delta: timedelta | None = None
self._last_screencap: datetime | None = None
self.turn_on_command: str | None = None
self.turn_off_command: str | None = None
@@ -159,7 +157,13 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._exclude_unnamed_apps = options.get(
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
)
self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
screencap_interval: int = options.get(
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
)
if screencap_interval > 0:
self._screencap_delta = timedelta(minutes=screencap_interval)
else:
self._screencap_delta = None
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
@@ -183,7 +187,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
"""Take a screen capture from the device when enabled."""
if (
not self._screencap
not self._screencap_delta
or self.state in {MediaPlayerState.OFF, None}
or not self.available
):
@@ -193,11 +197,18 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
force: bool = prev_app_id is not None
if force:
force = prev_app_id != self._attr_app_id
await self._adb_get_screencap(no_throttle=force)
await self._adb_get_screencap(force)
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
async def _adb_get_screencap(self, **kwargs: Any) -> None:
"""Take a screen capture from the device every 60 seconds."""
async def _adb_get_screencap(self, force: bool = False) -> None:
"""Take a screen capture from the device every configured minutes."""
time_elapsed = self._screencap_delta is not None and (
self._last_screencap is None
or (utcnow() - self._last_screencap) >= self._screencap_delta
)
if not (force or time_elapsed):
return
self._last_screencap = utcnow()
if media_data := await self._adb_screencap():
self._media_image = media_data, "image/png"
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
@@ -31,7 +31,7 @@
"apps": "Configure applications list",
"get_sources": "Retrieve the running apps as the list of sources",
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
"screencap": "Use screen capture for album art",
"screencap_interval": "Interval in minutes between screen capture for album art (set 0 to disable)",
"state_detection_rules": "Configure state detection rules",
"turn_off_command": "ADB shell turn off command (leave empty for default)",
"turn_on_command": "ADB shell turn on command (leave empty for default)"
+16 -19
View File
@@ -15,12 +15,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
type AranetConfigEntry = ConfigEntry[
PassiveBluetoothProcessorCoordinator[Aranet4Advertisement]
]
def _service_info_to_adv(
service_info: BluetoothServiceInfoBleak,
@@ -28,30 +30,25 @@ def _service_info_to_adv(
return Aranet4Advertisement(service_info.device, service_info.advertisement)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
"""Set up Aranet from a config entry."""
address = entry.unique_id
assert address is not None
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=_service_info_to_adv,
)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=_service_info_to_adv,
)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
# only start after all platforms have had a chance to subscribe
entry.async_on_unload(coordinator.async_start())
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+4 -8
View File
@@ -8,12 +8,10 @@ from typing import Any
from aranet4.client import Aranet4Advertisement
from bleak.backends.device import BLEDevice
from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothEntityKey,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
@@ -38,7 +36,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ARANET_MANUFACTURER_NAME, DOMAIN
from . import AranetConfigEntry
from .const import ARANET_MANUFACTURER_NAME
@dataclass(frozen=True)
@@ -174,20 +173,17 @@ def sensor_update_to_bluetooth_data_update(
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
entry: AranetConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aranet sensors."""
coordinator: PassiveBluetoothProcessorCoordinator[Aranet4Advertisement] = hass.data[
DOMAIN
][entry.entry_id]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
Aranet4BluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
class Aranet4BluetoothSensorEntity(
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco",
"iot_class": "cloud_polling",
"requirements": ["autarco==3.0.0"]
"requirements": ["autarco==3.1.0"]
}
@@ -124,7 +124,9 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_CONN_STRING,
data_schema=CONN_STRING_SCHEMA,
errors=errors,
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
description_placeholders={
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
},
last_step=True,
)
@@ -144,7 +146,9 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_SAS,
data_schema=SAS_SCHEMA,
errors=errors,
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
description_placeholders={
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
},
last_step=True,
)
+1 -1
View File
@@ -1,8 +1,8 @@
"""The Backup integration."""
from homeassistant.components.hassio import is_hassio
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
from .const import DATA_MANAGER, DOMAIN, LOGGER
+83 -26
View File
@@ -7,20 +7,72 @@ from typing import Final
from mozart_api.models import Source, SourceArray, SourceTypeEnum
from homeassistant.components.media_player import MediaPlayerState, MediaType
from homeassistant.components.media_player import (
MediaPlayerState,
MediaType,
RepeatMode,
)
class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth")
CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast")
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
URI_STREAMER: Final[Source] = Source(
name="Audio Streamer",
id="uriStreamer",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
BLUETOOTH: Final[Source] = Source(
name="Bluetooth",
id="bluetooth",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
CHROMECAST: Final[Source] = Source(
name="Chromecast built-in",
id="chromeCast",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
LINE_IN: Final[Source] = Source(
name="Line-In",
id="lineIn",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
SPDIF: Final[Source] = Source(
name="Optical",
id="spdif",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
NET_RADIO: Final[Source] = Source(
name="B&O Radio",
id="netRadio",
is_seekable=False,
is_enabled=True,
is_playable=True,
)
DEEZER: Final[Source] = Source(
name="Deezer",
id="deezer",
is_seekable=True,
is_enabled=True,
is_playable=True,
)
TIDAL: Final[Source] = Source(
name="Tidal",
id="tidal",
is_seekable=True,
is_enabled=True,
is_playable=True,
)
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
@@ -36,6 +88,17 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
"unknown": MediaPlayerState.IDLE,
}
# Dict used for translating Home Assistant settings to device repeat settings.
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
}
# Media types for play_media
class BangOlufsenMediaType(StrEnum):
@@ -123,20 +186,6 @@ VALID_MEDIA_TYPES: Final[tuple] = (
MediaType.CHANNEL,
)
# Sources on the device that should not be selectable by the user
HIDDEN_SOURCE_IDS: Final[tuple] = (
"airPlay",
"bluetooth",
"chromeCast",
"generator",
"local",
"dlna",
"qplay",
"wpl",
"pl",
"beolink",
"usbIn",
)
# Fallback sources to use in case of API failure.
FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
@@ -144,23 +193,26 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
Source(
id="uriStreamer",
is_enabled=True,
is_playable=False,
is_playable=True,
name="Audio Streamer",
type=SourceTypeEnum(value="uriStreamer"),
is_seekable=False,
),
Source(
id="bluetooth",
is_enabled=True,
is_playable=False,
is_playable=True,
name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"),
is_seekable=False,
),
Source(
id="spotify",
is_enabled=True,
is_playable=False,
is_playable=True,
name="Spotify Connect",
type=SourceTypeEnum(value="spotify"),
is_seekable=True,
),
Source(
id="lineIn",
@@ -168,6 +220,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Line-In",
type=SourceTypeEnum(value="lineIn"),
is_seekable=False,
),
Source(
id="spdif",
@@ -175,6 +228,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Optical",
type=SourceTypeEnum(value="spdif"),
is_seekable=False,
),
Source(
id="netRadio",
@@ -182,6 +236,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="B&O Radio",
type=SourceTypeEnum(value="netRadio"),
is_seekable=False,
),
Source(
id="deezer",
@@ -189,6 +244,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Deezer",
type=SourceTypeEnum(value="deezer"),
is_seekable=True,
),
Source(
id="tidalConnect",
@@ -196,6 +252,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True,
name="Tidal Connect",
type=SourceTypeEnum(value="tidalConnect"),
is_seekable=True,
),
]
)
@@ -3,10 +3,13 @@
from __future__ import annotations
from collections.abc import Callable
import contextlib
from datetime import timedelta
import json
import logging
from typing import TYPE_CHECKING, Any, cast
from aiohttp import ClientConnectorError
from mozart_api import __version__ as MOZART_API_VERSION
from mozart_api.exceptions import ApiException
from mozart_api.models import (
@@ -22,6 +25,7 @@ from mozart_api.models import (
PlaybackProgress,
PlayQueueItem,
PlayQueueItemType,
PlayQueueSettings,
RenderingState,
SceneProperties,
SoftwareUpdateState,
@@ -44,6 +48,7 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
RepeatMode,
async_process_play_media_url,
)
from homeassistant.config_entries import ConfigEntry
@@ -58,12 +63,13 @@ from homeassistant.util.dt import utcnow
from . import BangOlufsenConfigEntry
from .const import (
BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
CONF_BEOLINK_JID,
CONNECTION_STATUS,
DOMAIN,
FALLBACK_SOURCES,
HIDDEN_SOURCE_IDS,
VALID_MEDIA_TYPES,
BangOlufsenMediaType,
BangOlufsenSource,
@@ -72,6 +78,8 @@ from .const import (
from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BANG_OLUFSEN_FEATURES = (
@@ -84,8 +92,9 @@ BANG_OLUFSEN_FEATURES = (
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.REPEAT_SET
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SHUFFLE_SET
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_MUTE
@@ -114,7 +123,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
_attr_icon = "mdi:speaker-wireless"
_attr_name = None
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_supported_features = BANG_OLUFSEN_FEATURES
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the media player."""
@@ -131,6 +139,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
serial_number=self._unique_id,
)
self._attr_unique_id = self._unique_id
self._attr_should_poll = True
# Misc. variables.
self._audio_sources: dict[str, str] = {}
@@ -159,6 +168,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error,
WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata_and_beolink,
WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress,
WebsocketNotification.PLAYBACK_SOURCE: self._async_update_sources,
WebsocketNotification.PLAYBACK_STATE: self._async_update_playback_state,
WebsocketNotification.REMOTE_MENU_CHANGED: self._async_update_sources,
WebsocketNotification.SOURCE_CHANGE: self._async_update_source_change,
@@ -220,7 +230,20 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
await self._async_update_sound_modes()
async def _async_update_sources(self) -> None:
async def async_update(self) -> None:
"""Update queue settings."""
# The WebSocket event listener is the main handler for connection state.
# The polling updates do therefore not set the device as available or unavailable
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None:
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
async def _async_update_sources(self, _: Source | None = None) -> None:
"""Get sources for the specific product."""
# Audio sources
@@ -247,10 +270,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self._audio_sources = {
source.id: source.name
for source in cast(list[Source], sources.items)
if source.is_enabled
and source.id
and source.name
and source.id not in HIDDEN_SOURCE_IDS
if source.is_enabled and source.id and source.name and source.is_playable
}
# Some sources are not Beolink expandable, meaning that they can't be joined by
@@ -464,6 +484,17 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BANG_OLUFSEN_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
features |= MediaPlayerEntityFeature.SEEK
return features
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
@@ -610,17 +641,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_media_seek(self, position: float) -> None:
"""Seek to position in ms."""
if self._source_change.id == BangOlufsenSource.DEEZER.id:
await self._client.seek_to_position(position_ms=int(position * 1000))
# Try to prevent the playback progress from bouncing in the UI.
self._attr_media_position_updated_at = utcnow()
self._playback_progress = PlaybackProgress(progress=int(position))
await self._client.seek_to_position(position_ms=int(position * 1000))
# Try to prevent the playback progress from bouncing in the UI.
self._attr_media_position_updated_at = utcnow()
self._playback_progress = PlaybackProgress(progress=int(position))
self.async_write_ha_state()
else:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
)
self.async_write_ha_state()
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
@@ -630,6 +656,20 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Clear the current playback queue."""
await self._client.post_clear_queue()
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
)
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set playback queues to shuffle."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(shuffle=shuffle),
)
async def async_select_source(self, source: str) -> None:
"""Select an input source."""
if source not in self._sources.values():
@@ -29,9 +29,6 @@
"m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported."
},
"non_deezer_seeking": {
"message": "Seeking is currently only supported when using Deezer"
},
"invalid_source": {
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
},
@@ -63,6 +63,9 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self._client.get_playback_progress_notifications(
self.on_playback_progress_notification
)
self._client.get_playback_source_notifications(
self.on_playback_source_notification
)
self._client.get_playback_state_notifications(
self.on_playback_state_notification
)
@@ -157,6 +160,14 @@ class BangOlufsenWebsocket(BangOlufsenBase):
notification,
)
def on_playback_source_notification(self, notification: Source) -> None:
"""Send playback_source dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_SOURCE}",
notification,
)
def on_source_change_notification(self, notification: Source) -> None:
"""Send source_change dispatch."""
async_dispatcher_send(
+6 -9
View File
@@ -10,7 +10,6 @@ from blinkpy.blinkpy import Blink
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import (
CONF_FILE_PATH,
CONF_FILENAME,
@@ -41,13 +40,11 @@ SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def _reauth_flow_wrapper(hass: HomeAssistant, data: dict[str, Any]) -> None:
async def _reauth_flow_wrapper(
hass: HomeAssistant, entry: BlinkConfigEntry, data: dict[str, Any]
) -> None:
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_REAUTH}, data=data
)
)
entry.async_start_reauth(hass, data=data)
persistent_notification.async_create(
hass,
(
@@ -64,10 +61,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
await _reauth_flow_wrapper(hass, data)
await _reauth_flow_wrapper(hass, entry, data)
return False
if entry.version == 2:
await _reauth_flow_wrapper(hass, data)
await _reauth_flow_wrapper(hass, entry, data)
return False
return True
@@ -364,12 +364,13 @@ class BluesoundPlayer(MediaPlayerEntity):
if self.is_grouped and not self.is_master:
return MediaPlayerState.IDLE
status = self._status.state
if status in ("pause", "stop"):
return MediaPlayerState.PAUSED
if status in ("stream", "play"):
return MediaPlayerState.PLAYING
return MediaPlayerState.IDLE
match self._status.state:
case "pause":
return MediaPlayerState.PAUSED
case "stream" | "play":
return MediaPlayerState.PLAYING
case _:
return MediaPlayerState.IDLE
@property
def media_title(self) -> str | None:
@@ -7,7 +7,11 @@ from typing import Any
from bimmer_connected.api.authentication import MyBMWAuthentication
from bimmer_connected.api.regions import get_region_from_name
from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
from bimmer_connected.models import (
MyBMWAPIError,
MyBMWAuthError,
MyBMWCaptchaMissingError,
)
from httpx import RequestError
import voluptuous as vol
@@ -54,6 +58,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
try:
await auth.login()
except MyBMWCaptchaMissingError as ex:
raise MissingCaptcha from ex
except MyBMWAuthError as ex:
raise InvalidAuth from ex
except (MyBMWAPIError, RequestError) as ex:
@@ -98,6 +104,8 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
CONF_GCID: info.get(CONF_GCID),
}
except MissingCaptcha:
errors["base"] = "missing_captcha"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -192,3 +200,7 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class MissingCaptcha(HomeAssistantError):
"""Error to indicate the captcha token is missing."""
@@ -7,7 +7,12 @@ import logging
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.regions import get_region_from_name
from bimmer_connected.models import GPSPosition, MyBMWAPIError, MyBMWAuthError
from bimmer_connected.models import (
GPSPosition,
MyBMWAPIError,
MyBMWAuthError,
MyBMWCaptchaMissingError,
)
from httpx import RequestError
from homeassistant.config_entries import ConfigEntry
@@ -61,6 +66,12 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
try:
await self.account.get_vehicles()
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"quality_scale": "platinum",
"requirements": ["bimmer-connected[china]==0.16.3"]
"requirements": ["bimmer-connected[china]==0.16.4"]
}
@@ -11,7 +11,8 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"missing_captcha": "Captcha validation missing"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
@@ -200,6 +201,9 @@
"exceptions": {
"invalid_poi": {
"message": "Invalid data for point of interest: {poi_exception}"
},
"missing_captcha": {
"message": "Login requires captcha validation"
}
}
}
@@ -39,16 +39,21 @@ HOST_SCHEMA = vol.Schema(
)
def write_tls_asset(hass: HomeAssistant, filename: str, asset: bytes) -> None:
def write_tls_asset(
hass: HomeAssistant, folder: str, filename: str, asset: bytes
) -> None:
"""Write the tls assets to disk."""
makedirs(hass.config.path(DOMAIN), exist_ok=True)
with open(hass.config.path(DOMAIN, filename), "w", encoding="utf8") as file_handle:
makedirs(hass.config.path(DOMAIN, folder), exist_ok=True)
with open(
hass.config.path(DOMAIN, folder, filename), "w", encoding="utf8"
) as file_handle:
file_handle.write(asset.decode("utf-8"))
def create_credentials_and_validate(
hass: HomeAssistant,
host: str,
unique_id: str,
user_input: dict[str, Any],
zeroconf_instance: zeroconf.HaZeroconf,
) -> dict[str, Any] | None:
@@ -57,13 +62,15 @@ def create_credentials_and_validate(
result = helper.register(host, "HomeAssistant")
if result is not None:
write_tls_asset(hass, CONF_SHC_CERT, result["cert"])
write_tls_asset(hass, CONF_SHC_KEY, result["key"])
# Save key/certificate pair for each registered host separately
# otherwise only the last registered host is accessible.
write_tls_asset(hass, unique_id, CONF_SHC_CERT, result["cert"])
write_tls_asset(hass, unique_id, CONF_SHC_KEY, result["key"])
session = SHCSession(
host,
hass.config.path(DOMAIN, CONF_SHC_CERT),
hass.config.path(DOMAIN, CONF_SHC_KEY),
hass.config.path(DOMAIN, unique_id, CONF_SHC_CERT),
hass.config.path(DOMAIN, unique_id, CONF_SHC_KEY),
True,
zeroconf_instance,
)
@@ -143,11 +150,16 @@ class BoschSHCConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
# unique_id uniquely identifies the registered controller and is used
# to save the key/certificate pair for each controller separately
unique_id = self.info["unique_id"]
assert unique_id
try:
result = await self.hass.async_add_executor_job(
create_credentials_and_validate,
self.hass,
self.host,
unique_id,
user_input,
zeroconf_instance,
)
@@ -167,13 +179,18 @@ class BoschSHCConfigFlow(ConfigFlow, domain=DOMAIN):
else:
assert result
entry_data = {
CONF_SSL_CERTIFICATE: self.hass.config.path(DOMAIN, CONF_SHC_CERT),
CONF_SSL_KEY: self.hass.config.path(DOMAIN, CONF_SHC_KEY),
# Each host has its own key/certificate pair
CONF_SSL_CERTIFICATE: self.hass.config.path(
DOMAIN, unique_id, CONF_SHC_CERT
),
CONF_SSL_KEY: self.hass.config.path(
DOMAIN, unique_id, CONF_SHC_KEY
),
CONF_HOST: self.host,
CONF_TOKEN: result["token"],
CONF_HOSTNAME: result["token"].split(":", 1)[1],
}
existing_entry = await self.async_set_unique_id(self.info["unique_id"])
existing_entry = await self.async_set_unique_id(unique_id)
if existing_entry:
return self.async_update_reload_and_abort(
existing_entry,
@@ -12,6 +12,13 @@
},
"list_language": {
"default": "mdi:earth"
},
"list_access": {
"default": "mdi:account-lock",
"state": {
"shared": "mdi:account-group",
"invitation": "mdi:account-multiple-plus"
}
}
},
"todo": {
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["bring-api==0.9.0"]
"requirements": ["bring-api==0.9.1"]
}
+9
View File
@@ -40,6 +40,7 @@ class BringSensor(StrEnum):
CONVENIENT = "convenient"
DISCOUNTED = "discounted"
LIST_LANGUAGE = "list_language"
LIST_ACCESS = "list_access"
SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
@@ -73,6 +74,14 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
options=[x.lower() for x in BRING_SUPPORTED_LOCALES],
device_class=SensorDeviceClass.ENUM,
),
BringSensorEntityDescription(
key=BringSensor.LIST_ACCESS,
translation_key=BringSensor.LIST_ACCESS,
value_fn=lambda lst, _: lst["status"].lower(),
entity_category=EntityCategory.DIAGNOSTIC,
options=["registered", "shared", "invitation"],
device_class=SensorDeviceClass.ENUM,
),
)
@@ -61,6 +61,14 @@
"sv-se": "Sweden",
"tr-tr": "Türkiye"
}
},
"list_access": {
"name": "List access",
"state": {
"registered": "Private",
"shared": "Shared",
"invitation": "Invitation pending"
}
}
}
},
+2 -8
View File
@@ -15,7 +15,7 @@ from broadlink.exceptions import (
)
from typing_extensions import TypeVar
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -200,10 +200,4 @@ class BroadlinkDevice(Generic[_ApiT]):
self.api.host[0],
)
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH},
data={CONF_NAME: self.name, **self.config.data},
)
)
self.config.async_start_reauth(self.hass, data={CONF_NAME: self.name})
+7 -64
View File
@@ -2,79 +2,22 @@
from __future__ import annotations
from asyncio import timeout
import logging
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
from brunt import BruntClientAsync, Thing
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL
_LOGGER = logging.getLogger(__name__)
from .const import PLATFORMS
from .coordinator import BruntConfigEntry, BruntCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BruntConfigEntry) -> bool:
"""Set up Brunt using config flow."""
session = async_get_clientsession(hass)
bapi = BruntClientAsync(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
await bapi.async_login()
except ServerDisconnectedError as exc:
raise ConfigEntryNotReady("Brunt not ready to connect.") from exc
except ClientResponseError as exc:
raise ConfigEntryAuthFailed(
f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}."
) from exc
async def async_update_data() -> dict[str | None, Thing]:
"""Fetch data from the Brunt endpoint for all Things.
Error 403 is the API response for any kind of authentication error (failed password or email)
Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
"""
try:
async with timeout(10):
things = await bapi.async_get_things(force=True)
return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed from err
if err.status == 401:
_LOGGER.warning("Device not found, will reload Brunt integration")
await hass.config_entries.async_reload(entry.entry_id)
raise UpdateFailed from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="brunt",
update_method=async_update_data,
update_interval=REGULAR_INTERVAL,
)
coordinator = BruntCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator}
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: BruntConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-2
View File
@@ -10,8 +10,6 @@ NOTIFICATION_ID = "brunt_notification"
NOTIFICATION_TITLE = "Brunt Cover Setup"
ATTRIBUTION = "Based on an unofficial Brunt SDK."
PLATFORMS = [Platform.COVER]
DATA_BAPI = "bapi"
DATA_COOR = "coordinator"
CLOSED_POSITION = 0
OPEN_POSITION = 100
@@ -0,0 +1,80 @@
"""The brunt component."""
from __future__ import annotations
from asyncio import timeout
import logging
from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError
from brunt import BruntClientAsync, Thing
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import REGULAR_INTERVAL
_LOGGER = logging.getLogger(__name__)
type BruntConfigEntry = ConfigEntry[BruntCoordinator]
class BruntCoordinator(DataUpdateCoordinator[dict[str | None, Thing]]):
"""Config entry data."""
bapi: BruntClientAsync
config_entry: BruntConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: BruntConfigEntry,
) -> None:
"""Initialize the Brunt coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="brunt",
update_interval=REGULAR_INTERVAL,
)
async def _async_setup(self) -> None:
session = async_get_clientsession(self.hass)
self.bapi = BruntClientAsync(
username=self.config_entry.data[CONF_USERNAME],
password=self.config_entry.data[CONF_PASSWORD],
session=session,
)
try:
await self.bapi.async_login()
except ServerDisconnectedError as exc:
raise ConfigEntryNotReady("Brunt not ready to connect.") from exc
except ClientResponseError as exc:
raise ConfigEntryAuthFailed(
f"Brunt could not connect with username: {self.config_entry.data[CONF_USERNAME]}."
) from exc
async def _async_update_data(self) -> dict[str | None, Thing]:
"""Fetch data from the Brunt endpoint for all Things.
Error 403 is the API response for any kind of authentication error (failed password or email)
Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account.
"""
try:
async with timeout(10):
things = await self.bapi.async_get_things(force=True)
return {thing.serial: thing for thing in things}
except ServerDisconnectedError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
except ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed from err
if err.status == 401:
_LOGGER.warning("Device not found, will reload Brunt integration")
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
raise UpdateFailed from err
+10 -22
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any
from aiohttp.client_exceptions import ClientResponseError
from brunt import BruntClientAsync, Thing
from brunt import Thing
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -13,49 +13,39 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_REQUEST_POSITION,
ATTRIBUTION,
CLOSED_POSITION,
DATA_BAPI,
DATA_COOR,
DOMAIN,
FAST_INTERVAL,
OPEN_POSITION,
REGULAR_INTERVAL,
)
from .coordinator import BruntConfigEntry, BruntCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: BruntConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the brunt platform."""
bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI]
coordinator: DataUpdateCoordinator[dict[str | None, Thing]] = hass.data[DOMAIN][
entry.entry_id
][DATA_COOR]
coordinator = entry.runtime_data
async_add_entities(
BruntDevice(coordinator, serial, thing, bapi, entry.entry_id)
BruntDevice(coordinator, serial, thing, entry.entry_id)
for serial, thing in coordinator.data.items()
)
class BruntDevice(
CoordinatorEntity[DataUpdateCoordinator[dict[str | None, Thing]]], CoverEntity
):
class BruntDevice(CoordinatorEntity[BruntCoordinator], CoverEntity):
"""Representation of a Brunt cover device.
Contains the common logic for all Brunt devices.
@@ -73,16 +63,14 @@ class BruntDevice(
def __init__(
self,
coordinator: DataUpdateCoordinator[dict[str | None, Thing]],
coordinator: BruntCoordinator,
serial: str | None,
thing: Thing,
bapi: BruntClientAsync,
entry_id: str,
) -> None:
"""Init the Brunt device."""
super().__init__(coordinator)
self._attr_unique_id = serial
self._bapi = bapi
self._thing = thing
self._entry_id = entry_id
@@ -167,7 +155,7 @@ class BruntDevice(
async def _async_update_cover(self, position: int) -> None:
"""Set the cover to the new position and wait for the update to be reflected."""
try:
await self._bapi.async_change_request_position(
await self.coordinator.bapi.async_change_request_position(
position, thing_uri=self._thing.thing_uri
)
except ClientResponseError as exc:
@@ -182,7 +170,7 @@ class BruntDevice(
"""Update the update interval after each refresh."""
if (
self.request_cover_position
== self._bapi.last_requested_positions[self._thing.thing_uri]
== self.coordinator.bapi.last_requested_positions[self._thing.thing_uri]
and self.move_state == 0
):
self.coordinator.update_interval = REGULAR_INTERVAL
+1 -1
View File
@@ -364,7 +364,7 @@ SENSOR_DESCRIPTIONS = {
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.CONDUCTIVITY}_{Units.CONDUCTIVITY}",
device_class=SensorDeviceClass.CONDUCTIVITY,
native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS,
native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS_PER_CM,
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -14,9 +14,6 @@
},
"get_events": {
"service": "mdi:calendar-month"
},
"list_events": {
"service": "mdi:calendar-month"
}
}
}
@@ -36,22 +36,6 @@ create_event:
example: "Conference Room - F123, Bldg. 002"
selector:
text:
list_events:
target:
entity:
domain: calendar
fields:
start_date_time:
example: "2022-03-22 20:00:00"
selector:
datetime:
end_date_time:
example: "2022-03-22 22:00:00"
selector:
datetime:
duration:
selector:
duration:
get_events:
target:
entity:
@@ -89,24 +89,6 @@
"description": "Returns active events from start_date_time until the specified duration."
}
}
},
"list_events": {
"name": "List event",
"description": "Lists events on a calendar within a time range.",
"fields": {
"start_date_time": {
"name": "[%key:component::calendar::services::get_events::fields::start_date_time::name%]",
"description": "[%key:component::calendar::services::get_events::fields::start_date_time::description%]"
},
"end_date_time": {
"name": "[%key:component::calendar::services::get_events::fields::end_date_time::name%]",
"description": "[%key:component::calendar::services::get_events::fields::end_date_time::description%]"
},
"duration": {
"name": "[%key:component::calendar::services::get_events::fields::duration::name%]",
"description": "[%key:component::calendar::services::get_events::fields::duration::description%]"
}
}
}
},
"issues": {
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONNECT_TIMEOUT, STREAM_MAGIC_EXCEPTIONS
from .const import CONNECT_TIMEOUT, DOMAIN, STREAM_MAGIC_EXCEPTIONS
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.SELECT, Platform.SWITCH]
@@ -45,7 +45,13 @@ async def async_setup_entry(
async with asyncio.timeout(CONNECT_TIMEOUT):
await client.connect()
except STREAM_MAGIC_EXCEPTIONS as err:
raise ConfigEntryNotReady(f"Error while connecting to {client.host}") from err
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="entry_cannot_connect",
translation_placeholders={
"host": client.host,
},
) from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -2,20 +2,22 @@
from typing import Any
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.redact import async_redact_data
from . import CambridgeAudioConfigEntry
TO_REDACT = {CONF_HOST}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: CambridgeAudioConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for the provided config entry."""
client = entry.runtime_data
return async_redact_data(
{"info": client.info, "sources": client.sources}, TO_REDACT
)
return {
"display": client.display.to_dict(),
"info": client.info.to_dict(),
"now_playing": client.now_playing.to_dict(),
"play_state": client.play_state.to_dict(),
"presets_list": client.preset_list.to_dict(),
"sources": [s.to_dict() for s in client.sources],
"update": client.update.to_dict(),
}
@@ -26,7 +26,12 @@ def command[_EntityT: CambridgeAudioEntity, **_P](
await func(self, *args, **kwargs)
except STREAM_MAGIC_EXCEPTIONS as exc:
raise HomeAssistantError(
f"Error executing {func.__name__} on entity {self.entity_id},"
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={
"function_name": func.__name__,
"entity_id": self.entity_id,
},
) from exc
return decorator
@@ -62,4 +67,4 @@ class CambridgeAudioEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Remove callbacks."""
await self.client.unregister_state_update_callbacks(self._state_update_callback)
self.client.unregister_state_update_callbacks(self._state_update_callback)
@@ -8,6 +8,9 @@
"dim": "mdi:brightness-6",
"off": "mdi:brightness-3"
}
},
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"
}
},
"switch": {
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"requirements": ["aiostreammagic==2.8.1"],
"requirements": ["aiostreammagic==2.8.4"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
@@ -177,12 +177,9 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
return volume / 100
@property
def shuffle(self) -> bool | None:
def shuffle(self) -> bool:
"""Current shuffle configuration."""
mode_shuffle = self.client.play_state.mode_shuffle
if not mode_shuffle:
return False
return mode_shuffle != ShuffleMode.OFF
return self.client.play_state.mode_shuffle != ShuffleMode.OFF
@property
def repeat(self) -> RepeatMode | None:
@@ -1,7 +1,7 @@
"""Support for Cambridge Audio select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from dataclasses import dataclass, field
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import DisplayBrightness
@@ -19,10 +19,34 @@ from .entity import CambridgeAudioEntity
class CambridgeAudioSelectEntityDescription(SelectEntityDescription):
"""Describes Cambridge Audio select entity."""
options_fn: Callable[[StreamMagicClient], list[str]] = field(default=lambda _: [])
load_fn: Callable[[StreamMagicClient], bool] = field(default=lambda _: True)
value_fn: Callable[[StreamMagicClient], str | None]
set_value_fn: Callable[[StreamMagicClient, str], Awaitable[None]]
async def _audio_output_set_value_fn(client: StreamMagicClient, value: str) -> None:
"""Set the audio output using the display name."""
audio_output_id = next(
(output.id for output in client.audio_output.outputs if value == output.name),
None,
)
assert audio_output_id is not None
await client.set_audio_output(audio_output_id)
def _audio_output_value_fn(client: StreamMagicClient) -> str | None:
"""Convert the current audio output id to name."""
return next(
(
output.name
for output in client.audio_output.outputs
if client.state.audio_output == output.id
),
None,
)
CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
CambridgeAudioSelectEntityDescription(
key="display_brightness",
@@ -34,6 +58,17 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
DisplayBrightness(value)
),
),
CambridgeAudioSelectEntityDescription(
key="audio_output",
translation_key="audio_output",
entity_category=EntityCategory.CONFIG,
options_fn=lambda client: [
output.name for output in client.audio_output.outputs
],
load_fn=lambda client: len(client.audio_output.outputs) > 0,
value_fn=_audio_output_value_fn,
set_value_fn=_audio_output_set_value_fn,
),
)
@@ -46,7 +81,9 @@ async def async_setup_entry(
client: StreamMagicClient = entry.runtime_data
entities: list[CambridgeAudioSelect] = [
CambridgeAudioSelect(client, description) for description in CONTROL_ENTITIES
CambridgeAudioSelect(client, description)
for description in CONTROL_ENTITIES
if description.load_fn(client)
]
async_add_entities(entities)
@@ -65,6 +102,9 @@ class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
super().__init__(client)
self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
options_fn = description.options_fn(client)
if options_fn:
self._attr_options = options_fn
@property
def current_option(self) -> str | None:
@@ -32,6 +32,9 @@
"dim": "Dim",
"off": "[%key:common::state::off%]"
}
},
"audio_output": {
"name": "Audio output"
}
},
"switch": {
@@ -52,6 +55,12 @@
},
"preset_non_integer": {
"message": "Preset must be an integer, got: {preset_id}"
},
"entry_cannot_connect": {
"message": "Error while connecting to {host}"
},
"command_error": {
"message": "Error executing {function_name} on entity {entity_id}"
}
}
}
+226 -96
View File
@@ -4,9 +4,9 @@ from __future__ import annotations
import asyncio
import collections
from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Coroutine
from contextlib import suppress
from dataclasses import asdict
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
from enum import IntFlag
from functools import partial
@@ -18,9 +18,9 @@ from typing import Any, Final, final
from aiohttp import hdrs, web
import attr
from propcache import cached_property
from propcache import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceServer
from webrtc_models import RTCIceCandidate, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -86,12 +86,20 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration,
async_get_supported_providers,
WebRTCError,
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_legacy_provider,
async_get_supported_provider,
async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
ws_get_client_config,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
_LOGGER = logging.getLogger(__name__)
@@ -169,6 +177,13 @@ class Image:
content: bytes = attr.ib()
@dataclass(frozen=True)
class CameraCapabilities:
"""Camera capabilities."""
frontend_stream_types: set[StreamType]
@bind_hass
async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) -> str:
"""Request a stream for a camera entity."""
@@ -342,10 +357,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.register_view(CameraMjpegStream(component))
websocket_api.async_register_command(hass, ws_camera_stream)
websocket_api.async_register_command(hass, ws_camera_web_rtc_offer)
websocket_api.async_register_command(hass, websocket_get_prefs)
websocket_api.async_register_command(hass, websocket_update_prefs)
websocket_api.async_register_command(hass, ws_get_client_config)
websocket_api.async_register_command(hass, ws_camera_capabilities)
async_register_ws(hass)
await component.async_setup(config)
@@ -405,7 +420,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
def get_ice_servers() -> list[RTCIceServer]:
if hass.config.webrtc.ice_servers:
return hass.config.webrtc.ice_servers
return [RTCIceServer(urls="stun:stun.home-assistant.io:80")]
return [
RTCIceServer(urls="stun:stun.home-assistant.io:80"),
RTCIceServer(urls="stun:stun.home-assistant.io:3478"),
]
async_register_ice_servers(hass, get_ice_servers)
return True
@@ -454,8 +472,11 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_state: None = None # State is determined by is_on
_attr_supported_features: CameraEntityFeature = CameraEntityFeature(0)
__supports_stream: CameraEntityFeature | None = None
def __init__(self) -> None:
"""Initialize a camera."""
self._cache: dict[str, Any] = {}
self.stream: Stream | None = None
self.stream_options: dict[str, str | bool | float] = {}
self.content_type: str = DEFAULT_CONTENT_TYPE
@@ -463,7 +484,15 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self._warned_old_signature = False
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_providers: list[CameraWebRTCProvider] = []
self._webrtc_provider: CameraWebRTCProvider | None = None
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
self._supports_native_async_webrtc = (
type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
)
@cached_property
def entity_picture(self) -> str:
@@ -537,7 +566,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
if self._webrtc_providers:
if self._webrtc_provider or self._legacy_webrtc_provider:
return StreamType.WEB_RTC
return StreamType.HLS
@@ -587,12 +616,66 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
for provider in self._webrtc_providers:
if answer := await provider.async_handle_web_rtc_offer(self, offer_sdp):
return answer
raise HomeAssistantError(
"WebRTC offer was not accepted by the supported providers"
)
async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
"""Handle the async WebRTC offer.
Async means that it could take some time to process the offer and responses/message
will be sent with the send_message callback.
This method is used by cameras with CameraEntityFeature.STREAM and StreamType.WEB_RTC.
An integration overriding this method must also implement async_on_webrtc_candidate.
Integrations can override with a native WebRTC implementation.
"""
if self._supports_native_sync_webrtc:
try:
answer = await self.async_handle_web_rtc_offer(offer_sdp)
except ValueError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
except TimeoutError:
# This catch was already here and should stay through the deprecation
_LOGGER.error("Timeout handling WebRTC offer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"Timeout handling WebRTC offer",
)
)
else:
if answer:
send_message(WebRTCAnswer(answer))
else:
_LOGGER.error("Error handling WebRTC offer: No answer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"No answer on WebRTC offer",
)
)
return
if self._webrtc_provider:
await self._webrtc_provider.async_handle_async_webrtc_offer(
self, offer_sdp, session_id, send_message
)
return
if self._legacy_webrtc_provider and (
answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer(
self, offer_sdp
)
):
send_message(WebRTCAnswer(answer))
else:
raise HomeAssistantError("Camera does not support WebRTC")
def camera_image(
self, width: int | None = None, height: int | None = None
@@ -702,57 +785,133 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async def async_internal_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_internal_added_to_hass()
# Avoid calling async_refresh_providers() in here because it
# it will write state a second time since state is always
# written when an entity is added to hass.
self._webrtc_providers = await self._async_get_supported_webrtc_providers()
self.__supports_stream = (
self.supported_features_compat & CameraEntityFeature.STREAM
)
await self.async_refresh_providers(write_state=False)
async def async_refresh_providers(self) -> None:
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
"""Determine if any of the registered providers are suitable for this entity.
This affects state attributes, so it should be invoked any time the registered
providers or inputs to the state attributes change.
Returns True if any state was updated (and needs to be written)
"""
old_providers = self._webrtc_providers
new_providers = await self._async_get_supported_webrtc_providers()
self._webrtc_providers = new_providers
if old_providers != new_providers:
self.async_write_ha_state()
old_provider = self._webrtc_provider
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None
new_legacy_provider = None
async def _async_get_supported_webrtc_providers(
self,
) -> list[CameraWebRTCProvider]:
"""Get the all providers that supports this camera."""
# Skip all providers if the camera has a native WebRTC implementation
if not (
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
):
# Camera doesn't have a native WebRTC implementation
new_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_provider
)
if new_provider is None:
# Only add the legacy provider if the new provider is not available
new_legacy_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_legacy_provider
)
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
self._webrtc_provider = new_provider
self._legacy_webrtc_provider = new_legacy_provider
self._invalidate_camera_capabilities_cache()
if write_state:
self.async_write_ha_state()
async def _async_get_supported_webrtc_provider[_T](
self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
) -> _T | None:
"""Get first provider that supports this camera."""
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return []
return None
return await async_get_supported_providers(self.hass, self)
return await fn(self.hass, self)
@property
def webrtc_providers(self) -> list[CameraWebRTCProvider]:
"""Return the WebRTC providers."""
return self._webrtc_providers
async def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
@callback
def _async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
"""Return the WebRTC client configuration adjustable per integration."""
return WebRTCClientConfiguration()
@final
async def async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
@callback
def async_get_webrtc_client_configuration(self) -> WebRTCClientConfiguration:
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = await self._async_get_webrtc_client_configuration()
config = self._async_get_webrtc_client_configuration()
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
if not self._supports_native_sync_webrtc:
# Until 2024.11, the frontend was not resolving any ice servers
# The async approach was added 2024.11 and new integrations need to use it
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = (
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
return config
async def async_on_webrtc_candidate(
self, session_id: str, candidate: RTCIceCandidate
) -> None:
"""Handle a WebRTC candidate."""
if self._webrtc_provider:
await self._webrtc_provider.async_on_webrtc_candidate(session_id, candidate)
else:
raise HomeAssistantError("Cannot handle WebRTC candidate")
@callback
def close_webrtc_session(self, session_id: str) -> None:
"""Close a WebRTC session."""
if self._webrtc_provider:
self._webrtc_provider.async_close_session(session_id)
@callback
def _invalidate_camera_capabilities_cache(self) -> None:
"""Invalidate the camera capabilities cache."""
self._cache.pop("camera_capabilities", None)
@final
@under_cached_property
def camera_capabilities(self) -> CameraCapabilities:
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
else:
frontend_stream_types.add(StreamType.HLS)
if self._webrtc_provider:
frontend_stream_types.add(StreamType.WEB_RTC)
return CameraCapabilities(frontend_stream_types)
@callback
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
Schedules async_refresh_providers if support of streams have changed.
"""
super().async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features_compat
& CameraEntityFeature.STREAM
):
self.__supports_stream = supports_stream
self._invalidate_camera_capabilities_cache()
self.hass.async_create_task(self.async_refresh_providers())
class CameraView(HomeAssistantView):
"""Base CameraView."""
@@ -843,6 +1002,24 @@ class CameraMjpegStream(CameraView):
raise web.HTTPBadRequest from err
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/capabilities",
vol.Required("entity_id"): cv.entity_id,
}
)
@websocket_api.async_response
async def ws_camera_capabilities(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle get camera capabilities websocket command.
Async friendly.
"""
camera = get_camera_from_entity_id(hass, msg["entity_id"])
connection.send_result(msg["id"], asdict(camera.camera_capabilities))
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/stream",
@@ -873,53 +1050,6 @@ async def ws_camera_stream(
)
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/web_rtc_offer",
vol.Required("entity_id"): cv.entity_id,
vol.Required("offer"): str,
}
)
@websocket_api.async_response
async def ws_camera_web_rtc_offer(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle the signal path for a WebRTC stream.
This signal path is used to route the offer created by the client to the
camera device through the integration for negotiation on initial setup,
which returns an answer. The actual streaming is handled entirely between
the client and camera device.
Async friendly.
"""
entity_id = msg["entity_id"]
offer = msg["offer"]
camera = get_camera_from_entity_id(hass, entity_id)
if camera.frontend_stream_type != StreamType.WEB_RTC:
connection.send_error(
msg["id"],
"web_rtc_offer_failed",
(
"Camera does not support WebRTC,"
f" frontend_stream_type={camera.frontend_stream_type}"
),
)
return
try:
answer = await camera.async_handle_web_rtc_offer(offer)
except (HomeAssistantError, ValueError) as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex))
except TimeoutError:
_LOGGER.error("Timeout handling WebRTC offer")
connection.send_error(
msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer"
)
else:
connection.send_result(msg["id"], {"answer": answer})
@websocket_api.websocket_command(
{vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id}
)
@@ -46,6 +46,10 @@
}
}
}
},
"legacy_webrtc_provider": {
"title": "Detected use of legacy WebRTC provider registered by {legacy_integration}",
"description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant."
}
},
"services": {
+294 -22
View File
@@ -2,18 +2,23 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
from functools import cache, partial
import logging
from typing import TYPE_CHECKING, Any, Protocol
import voluptuous as vol
from webrtc_models import RTCConfiguration, RTCIceServer
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceServer
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid
from .const import DATA_COMPONENT, DOMAIN, StreamType
from .helper import get_camera_from_entity_id
@@ -21,15 +26,79 @@ from .helper import get_camera_from_entity_id
if TYPE_CHECKING:
from . import Camera
_LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_web_rtc_providers"
"camera_webrtc_providers"
)
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
"camera_webrtc_legacy_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_web_rtc_ice_servers"
"camera_webrtc_ice_servers"
)
_WEBRTC = "WebRTC"
@dataclass(frozen=True)
class WebRTCMessage:
"""Base class for WebRTC messages."""
@classmethod
@cache
def _get_type(cls) -> str:
_, _, name = cls.__name__.partition(_WEBRTC)
return name.lower()
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the message."""
data = asdict(self)
data["type"] = self._get_type()
return data
@dataclass(frozen=True)
class WebRTCSession(WebRTCMessage):
"""WebRTC session."""
session_id: str
@dataclass(frozen=True)
class WebRTCAnswer(WebRTCMessage):
"""WebRTC answer."""
answer: str
@dataclass(frozen=True)
class WebRTCCandidate(WebRTCMessage):
"""WebRTC candidate."""
candidate: RTCIceCandidate
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the message."""
return {
"type": self._get_type(),
"candidate": self.candidate.candidate,
}
@dataclass(frozen=True)
class WebRTCError(WebRTCMessage):
"""WebRTC error."""
code: str
message: str
type WebRTCSendMessage = Callable[[WebRTCMessage], None]
@dataclass(kw_only=True)
class WebRTCClientConfiguration:
"""WebRTC configuration for the client.
@@ -39,18 +108,55 @@ class WebRTCClientConfiguration:
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
data_channel: str | None = None
get_candidates_upfront: bool = False
def to_frontend_dict(self) -> dict[str, Any]:
"""Return a dict that can be used by the frontend."""
data: dict[str, Any] = {
"configuration": self.configuration.to_dict(),
"getCandidatesUpfront": self.get_candidates_upfront,
}
if self.data_channel is not None:
data["dataChannel"] = self.data_channel
return data
class CameraWebRTCProvider(Protocol):
class CameraWebRTCProvider(ABC):
"""WebRTC provider."""
@property
@abstractmethod
def domain(self) -> str:
"""Return the integration domain of the provider."""
@callback
@abstractmethod
def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""
@abstractmethod
async def async_handle_async_webrtc_offer(
self,
camera: Camera,
offer_sdp: str,
session_id: str,
send_message: WebRTCSendMessage,
) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback."""
@abstractmethod
async def async_on_webrtc_candidate(
self, session_id: str, candidate: RTCIceCandidate
) -> None:
"""Handle the WebRTC candidate."""
@callback
def async_close_session(self, session_id: str) -> None:
"""Close the session."""
return ## This is an optional method so we need a default here.
class CameraWebRTCLegacyProvider(Protocol):
"""WebRTC provider."""
async def async_is_supported(self, stream_source: str) -> bool:
@@ -62,6 +168,7 @@ class CameraWebRTCProvider(Protocol):
"""Handle the WebRTC offer and return an answer."""
@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
provider: CameraWebRTCProvider,
@@ -73,9 +180,7 @@ def async_register_webrtc_provider(
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
providers: set[CameraWebRTCProvider] = hass.data.setdefault(
DATA_WEBRTC_PROVIDERS, set()
)
providers = hass.data.setdefault(DATA_WEBRTC_PROVIDERS, set())
@callback
def remove_provider() -> None:
@@ -92,6 +197,7 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers."""
_async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT]
await asyncio.gather(
@@ -99,6 +205,72 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None:
)
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/webrtc/offer",
vol.Required("entity_id"): cv.entity_id,
vol.Required("offer"): str,
}
)
@websocket_api.async_response
async def ws_webrtc_offer(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle the signal path for a WebRTC stream.
This signal path is used to route the offer created by the client to the
camera device through the integration for negotiation on initial setup.
The ws endpoint returns a subscription id, where ice candidates and the
final answer will be returned.
The actual streaming is handled entirely between the client and camera device.
Async friendly.
"""
entity_id = msg["entity_id"]
offer = msg["offer"]
camera = get_camera_from_entity_id(hass, entity_id)
if camera.frontend_stream_type != StreamType.WEB_RTC:
connection.send_error(
msg["id"],
"webrtc_offer_failed",
(
"Camera does not support WebRTC,"
f" frontend_stream_type={camera.frontend_stream_type}"
),
)
return
session_id = ulid()
connection.subscriptions[msg["id"]] = partial(
camera.close_webrtc_session, session_id
)
connection.send_message(websocket_api.result_message(msg["id"]))
@callback
def send_message(message: WebRTCMessage) -> None:
"""Push a value to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"],
message.as_dict(),
)
)
send_message(WebRTCSession(session_id))
try:
await camera.async_handle_async_webrtc_offer(offer, session_id, send_message)
except HomeAssistantError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/webrtc/get_client_config",
@@ -115,7 +287,7 @@ async def ws_get_client_config(
if camera.frontend_stream_type != StreamType.WEB_RTC:
connection.send_error(
msg["id"],
"web_rtc_offer_failed",
"webrtc_get_client_config_failed",
(
"Camera does not support WebRTC,"
f" frontend_stream_type={camera.frontend_stream_type}"
@@ -123,26 +295,82 @@ async def ws_get_client_config(
)
return
config = (await camera.async_get_webrtc_client_configuration()).to_frontend_dict()
config = camera.async_get_webrtc_client_configuration().to_frontend_dict()
connection.send_result(
msg["id"],
config,
)
async def async_get_supported_providers(
@websocket_api.websocket_command(
{
vol.Required("type"): "camera/webrtc/candidate",
vol.Required("entity_id"): cv.entity_id,
vol.Required("session_id"): str,
vol.Required("candidate"): str,
}
)
@websocket_api.async_response
async def ws_candidate(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle WebRTC candidate websocket command."""
entity_id = msg["entity_id"]
camera = get_camera_from_entity_id(hass, entity_id)
if camera.frontend_stream_type != StreamType.WEB_RTC:
connection.send_error(
msg["id"],
"webrtc_candidate_failed",
(
"Camera does not support WebRTC,"
f" frontend_stream_type={camera.frontend_stream_type}"
),
)
return
await camera.async_on_webrtc_candidate(
msg["session_id"], RTCIceCandidate(msg["candidate"])
)
connection.send_message(websocket_api.result_message(msg["id"]))
@callback
def async_register_ws(hass: HomeAssistant) -> None:
"""Register camera webrtc ws endpoints."""
websocket_api.async_register_command(hass, ws_webrtc_offer)
websocket_api.async_register_command(hass, ws_get_client_config)
websocket_api.async_register_command(hass, ws_candidate)
async def async_get_supported_provider(
hass: HomeAssistant, camera: Camera
) -> list[CameraWebRTCProvider]:
"""Return a list of supported providers for the camera."""
) -> CameraWebRTCProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(DATA_WEBRTC_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return []
return None
return [
provider
for provider in providers
if await provider.async_is_supported(stream_source)
]
for provider in providers:
if provider.async_is_supported(stream_source):
return provider
return None
async def async_get_supported_legacy_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCLegacyProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return None
for provider in providers.values():
if await provider.async_is_supported(stream_source):
return provider
return None
@callback
@@ -177,7 +405,7 @@ _RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}
type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]]
class _CameraRtspToWebRTCProvider(CameraWebRTCProvider):
class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider):
def __init__(self, fn: RtspToWebRtcProviderType) -> None:
"""Initialize the RTSP to WebRTC provider."""
self._fn = fn
@@ -205,5 +433,49 @@ def async_register_rtsp_to_web_rtc_provider(
The first provider to satisfy the offer will be used.
"""
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {})
if domain in legacy_providers:
raise ValueError("Provider already registered")
provider_instance = _CameraRtspToWebRTCProvider(provider)
return async_register_webrtc_provider(hass, provider_instance)
@callback
def remove_provider() -> None:
legacy_providers.pop(domain)
hass.async_create_task(_async_refresh_providers(hass))
legacy_providers[domain] = provider_instance
hass.async_create_task(_async_refresh_providers(hass))
return remove_provider
@callback
def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None:
"""Check if a legacy provider is registered together with the builtin provider."""
builtin_provider_domain = "go2rtc"
if (
(legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS))
and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS))
and any(provider.domain == builtin_provider_domain for provider in providers)
):
for domain in legacy_providers:
ir.async_create_issue(
hass,
DOMAIN,
f"legacy_webrtc_provider_{domain}",
is_fixable=False,
is_persistent=False,
issue_domain=domain,
learn_more_url="https://www.home-assistant.io/integrations/go2rtc/",
severity=ir.IssueSeverity.WARNING,
translation_key="legacy_webrtc_provider",
translation_placeholders={
"legacy_integration": domain,
"builtin_integration": builtin_provider_domain,
},
)
+1 -1
View File
@@ -14,7 +14,7 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.4"],
"requirements": ["PyChromecast==14.0.5"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}
+54 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import datetime
from http import HTTPStatus
import logging
@@ -11,12 +12,14 @@ from typing import Any, Literal
import aiohttp
from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed
from webrtc_models import RTCIceServer
from homeassistant.components import google_assistant, persistent_notification, webhook
from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
@@ -27,7 +30,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
from homeassistant.util.aiohttp import MockRequest, serialize_response
from . import alexa_config, google_config
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN, PREF_ENABLE_CLOUD_ICE_SERVERS
from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__)
@@ -60,6 +63,7 @@ class CloudClient(Interface):
self._alexa_config_init_lock = asyncio.Lock()
self._google_config_init_lock = asyncio.Lock()
self._relayer_region: str | None = None
self._cloud_ice_servers_listener: Callable[[], None] | None = None
@property
def base_path(self) -> Path:
@@ -187,6 +191,49 @@ class CloudClient(Interface):
if is_new_user:
await gconf.async_sync_entities(gconf.agent_user_id)
async def setup_cloud_ice_servers(_: datetime) -> None:
async def register_cloud_ice_server(
ice_servers: list[RTCIceServer],
) -> Callable[[], None]:
"""Register cloud ice server."""
def get_ice_servers() -> list[RTCIceServer]:
return ice_servers
return async_register_ice_servers(self._hass, get_ice_servers)
async def async_register_cloud_ice_servers_listener(
prefs: CloudPreferences,
) -> None:
is_cloud_ice_servers_enabled = (
self.cloud.is_logged_in
and not self.cloud.subscription_expired
and prefs.cloud_ice_servers_enabled
)
if is_cloud_ice_servers_enabled:
if self._cloud_ice_servers_listener is None:
self._cloud_ice_servers_listener = await self.cloud.ice_servers.async_register_ice_servers_listener(
register_cloud_ice_server
)
elif self._cloud_ice_servers_listener:
self._cloud_ice_servers_listener()
self._cloud_ice_servers_listener = None
async def async_prefs_updated(prefs: CloudPreferences) -> None:
updated_prefs = prefs.last_updated
if (
updated_prefs is None
or PREF_ENABLE_CLOUD_ICE_SERVERS not in updated_prefs
):
return
await async_register_cloud_ice_servers_listener(prefs)
await async_register_cloud_ice_servers_listener(self._prefs)
self._prefs.async_listen_updates(async_prefs_updated)
tasks = []
if self._prefs.alexa_enabled and self._prefs.alexa_report_state:
@@ -195,6 +242,8 @@ class CloudClient(Interface):
if self._prefs.google_enabled:
tasks.append(enable_google)
tasks.append(setup_cloud_ice_servers)
if tasks:
await asyncio.gather(*(task(None) for task in tasks))
@@ -222,6 +271,10 @@ class CloudClient(Interface):
self._google_config.async_deinitialize()
self._google_config = None
if self._cloud_ice_servers_listener:
self._cloud_ice_servers_listener()
self._cloud_ice_servers_listener = None
@callback
def user_message(self, identifier: str, title: str, message: str) -> None:
"""Create a message for user to UI."""
+1
View File
@@ -43,6 +43,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
PREF_GOOGLE_CONNECTED = "google_connected"
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
PREF_ENABLE_CLOUD_ICE_SERVERS = "cloud_ice_servers_enabled"
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural")
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = True
@@ -42,6 +42,7 @@ from .const import (
PREF_ALEXA_REPORT_STATE,
PREF_DISABLE_2FA,
PREF_ENABLE_ALEXA,
PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_GOOGLE_REPORT_STATE,
PREF_GOOGLE_SECURE_DEVICES_PIN,
@@ -448,6 +449,7 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
vol.Coerce(tuple), validate_language_voice
),
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool,
}
)
@websocket_api.async_response
+1 -1
View File
@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.81.1"],
"requirements": ["hass-nabucasa==0.83.0"],
"single_config_entry": true
}
+13
View File
@@ -32,6 +32,7 @@ from .const import (
PREF_CLOUD_USER,
PREF_CLOUDHOOKS,
PREF_ENABLE_ALEXA,
PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_ENABLE_REMOTE,
PREF_GOOGLE_CONNECTED,
@@ -176,6 +177,7 @@ class CloudPreferences:
google_settings_version: int | UndefinedType = UNDEFINED,
google_connected: bool | UndefinedType = UNDEFINED,
remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED,
) -> None:
"""Update user preferences."""
prefs = {**self._prefs}
@@ -198,6 +200,7 @@ class CloudPreferences:
(PREF_REMOTE_DOMAIN, remote_domain),
(PREF_GOOGLE_CONNECTED, google_connected),
(PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
(PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled),
)
if value is not UNDEFINED
}
@@ -246,6 +249,7 @@ class CloudPreferences:
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable,
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled,
}
@property
@@ -362,6 +366,14 @@ class CloudPreferences:
"""
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
@property
def cloud_ice_servers_enabled(self) -> bool:
"""Return if cloud ICE servers are enabled."""
cloud_ice_servers_enabled: bool = self._prefs.get(
PREF_ENABLE_CLOUD_ICE_SERVERS, True
)
return cloud_ice_servers_enabled
async def get_cloud_user(self) -> str:
"""Return ID of Home Assistant Cloud system user."""
user = await self._load_cloud_user()
@@ -409,6 +421,7 @@ class CloudPreferences:
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_ENABLE_CLOUD_ICE_SERVERS: True,
PREF_GOOGLE_CONNECTED: False,
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
PREF_GOOGLE_ENTITY_CONFIGS: {},
@@ -33,6 +33,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
data["remote_connected"] = cloud.remote.is_connected
data["alexa_enabled"] = client.prefs.alexa_enabled
data["google_enabled"] = client.prefs.google_enabled
data["cloud_ice_servers_enabled"] = client.prefs.cloud_ice_servers_enabled
data["remote_server"] = cloud.remote.snitun_server
data["certificate_status"] = cloud.remote.certificate_status
data["instance_id"] = client.prefs.instance_id
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.10.2"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.4"]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/deako",
"iot_class": "local_polling",
"loggers": ["pydeako"],
"requirements": ["pydeako==0.4.0"],
"requirements": ["pydeako==0.5.4"],
"single_config_entry": true,
"zeroconf": ["_deako._tcp.local."]
}
@@ -20,7 +20,6 @@ from pydeconz.utils import (
import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import (
SOURCE_HASSIO,
ConfigEntry,
@@ -31,6 +30,7 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import (
CONF_ALLOW_CLIP_SENSOR,
@@ -9,6 +9,7 @@
"conversation",
"dhcp",
"energy",
"go2rtc",
"history",
"homeassistant_alerts",
"logbook",
+23 -2
View File
@@ -75,6 +75,21 @@ async def async_setup_entry(
support_release_notes=True,
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
update_steps=10,
),
DemoUpdate(
unique_id="update_support_decimal_progress",
device_name="Demo Update with Decimal Progress",
title="Philips Lamps Firmware",
installed_version="1.93.3",
latest_version="1.94.2",
support_progress=True,
release_summary="Added support for effects",
support_release_notes=True,
release_url="https://www.example.com/release/1.93.3",
device_class=UpdateDeviceClass.FIRMWARE,
display_precision=2,
update_steps=1000,
),
]
)
@@ -106,10 +121,13 @@ class DemoUpdate(UpdateEntity):
support_install: bool = True,
support_release_notes: bool = False,
device_class: UpdateDeviceClass | None = None,
display_precision: int = 0,
update_steps: int = 100,
) -> None:
"""Initialize the Demo select entity."""
self._attr_installed_version = installed_version
self._attr_device_class = device_class
self._attr_display_precision = display_precision
self._attr_latest_version = latest_version
self._attr_release_summary = release_summary
self._attr_release_url = release_url
@@ -119,6 +137,7 @@ class DemoUpdate(UpdateEntity):
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
self._update_steps = update_steps
if support_install:
self._attr_supported_features |= (
UpdateEntityFeature.INSTALL
@@ -136,12 +155,14 @@ class DemoUpdate(UpdateEntity):
) -> None:
"""Install an update."""
if self.supported_features & UpdateEntityFeature.PROGRESS:
for progress in range(0, 100, 10):
self._attr_in_progress = progress
self._attr_in_progress = True
for progress in range(0, self._update_steps, 1):
self._attr_update_percentage = progress / (self._update_steps / 100)
self.async_write_ha_state()
await _fake_install()
self._attr_in_progress = False
self._attr_update_percentage = None
self._attr_installed_version = (
version if version is not None else self.latest_version
)
@@ -2,6 +2,7 @@
from __future__ import annotations
from asyncio import Semaphore
from dataclasses import dataclass
import logging
from typing import Any
@@ -32,7 +33,7 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import (
CONNECTED_PLC_DEVICES,
@@ -47,6 +48,7 @@ from .const import (
SWITCH_GUEST_WIFI,
SWITCH_LEDS,
)
from .coordinator import DevoloDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -58,7 +60,7 @@ class DevoloHomeNetworkData:
"""The devolo Home Network data."""
device: Device
coordinators: dict[str, DataUpdateCoordinator[Any]]
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
async def async_setup_entry(
@@ -68,6 +70,7 @@ async def async_setup_entry(
zeroconf_instance = await zeroconf.async_get_async_instance(hass)
async_client = get_async_client(hass)
device_registry = dr.async_get(hass)
semaphore = Semaphore(1)
try:
device = Device(
@@ -163,58 +166,72 @@ async def async_setup_entry(
"""Disconnect from device."""
await device.async_disconnect()
coordinators: dict[str, DataUpdateCoordinator[Any]] = {}
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {}
if device.plcnet:
coordinators[CONNECTED_PLC_DEVICES] = DataUpdateCoordinator(
coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=CONNECTED_PLC_DEVICES,
semaphore=semaphore,
update_method=async_update_connected_plc_devices,
update_interval=LONG_UPDATE_INTERVAL,
)
if device.device and "led" in device.device.features:
coordinators[SWITCH_LEDS] = DataUpdateCoordinator(
coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=SWITCH_LEDS,
semaphore=semaphore,
update_method=async_update_led_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "restart" in device.device.features:
coordinators[LAST_RESTART] = DataUpdateCoordinator(
coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=LAST_RESTART,
semaphore=semaphore,
update_method=async_update_last_restart,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "update" in device.device.features:
coordinators[REGULAR_FIRMWARE] = DataUpdateCoordinator(
coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=REGULAR_FIRMWARE,
semaphore=semaphore,
update_method=async_update_firmware_available,
update_interval=FIRMWARE_UPDATE_INTERVAL,
)
if device.device and "wifi1" in device.device.features:
coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=CONNECTED_WIFI_CLIENTS,
semaphore=semaphore,
update_method=async_update_wifi_connected_station,
update_interval=SHORT_UPDATE_INTERVAL,
)
coordinators[NEIGHBORING_WIFI_NETWORKS] = DataUpdateCoordinator(
coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=NEIGHBORING_WIFI_NETWORKS,
semaphore=semaphore,
update_method=async_update_wifi_neighbor_access_points,
update_interval=LONG_UPDATE_INTERVAL,
)
coordinators[SWITCH_GUEST_WIFI] = DataUpdateCoordinator(
coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=SWITCH_GUEST_WIFI,
semaphore=semaphore,
update_method=async_update_guest_wifi_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
@@ -15,13 +15,13 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool:
@@ -78,7 +78,7 @@ class DevoloBinarySensorEntity(
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[LogicalNetwork],
coordinator: DevoloDataUpdateCoordinator[LogicalNetwork],
description: DevoloBinarySensorEntityDescription,
) -> None:
"""Initialize entity."""
@@ -22,7 +22,7 @@ from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS
from .entity import DevoloEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
@@ -0,0 +1,41 @@
"""Base coordinator."""
from asyncio import Semaphore
from collections.abc import Awaitable, Callable
from datetime import timedelta
from logging import Logger
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Class to manage fetching data from devolo Home Network devices."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str,
semaphore: Semaphore,
update_interval: timedelta,
update_method: Callable[[], Awaitable[_DataT]],
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
update_method=update_method,
)
self._semaphore = semaphore
async def _async_update_data(self) -> _DataT:
"""Fetch the latest data from the source."""
async with self._semaphore:
return await super()._async_update_data()
@@ -13,15 +13,13 @@ from homeassistant.const import STATE_UNKNOWN, UnitOfFrequency
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS
from .coordinator import DevoloDataUpdateCoordinator
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
async def async_setup_entry(
@@ -31,7 +29,7 @@ async def async_setup_entry(
) -> None:
"""Get all devices and sensors and setup them via config entry."""
device = entry.runtime_data.device
coordinators: dict[str, DataUpdateCoordinator[list[ConnectedStationInfo]]] = (
coordinators: dict[str, DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]] = (
entry.runtime_data.coordinators
)
registry = er.async_get(hass)
@@ -83,14 +81,16 @@ async def async_setup_entry(
)
class DevoloScannerEntity(
CoordinatorEntity[DataUpdateCoordinator[list[ConnectedStationInfo]]], ScannerEntity
# The pylint disable is needed because of https://github.com/pylint-dev/pylint/issues/9138
class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
CoordinatorEntity[DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]],
ScannerEntity,
):
"""Representation of a devolo device tracker."""
def __init__(
self,
coordinator: DataUpdateCoordinator[list[ConnectedStationInfo]],
coordinator: DevoloDataUpdateCoordinator[list[ConnectedStationInfo]],
device: Device,
mac: str,
) -> None:
@@ -12,13 +12,11 @@ from devolo_plc_api.plcnet_api import DataRate, LogicalNetwork
from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN
from .coordinator import DevoloDataUpdateCoordinator
type _DataType = (
LogicalNetwork
@@ -64,14 +62,14 @@ class DevoloEntity(Entity):
class DevoloCoordinatorEntity[_DataT: _DataType](
CoordinatorEntity[DataUpdateCoordinator[_DataT]], DevoloEntity
CoordinatorEntity[DevoloDataUpdateCoordinator[_DataT]], DevoloEntity
):
"""Representation of a coordinated devolo home network device."""
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[_DataT],
coordinator: DevoloDataUpdateCoordinator[_DataT],
) -> None:
"""Initialize a devolo home network device."""
super().__init__(coordinator)
@@ -13,14 +13,14 @@ from homeassistant.components.image import ImageEntity, ImageEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import homeassistant.util.dt as dt_util
from . import DevoloHomeNetworkConfigEntry
from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
@@ -66,7 +66,7 @@ class DevoloImageEntity(DevoloCoordinatorEntity[WifiGuestAccessGet], ImageEntity
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[WifiGuestAccessGet],
coordinator: DevoloDataUpdateCoordinator[WifiGuestAccessGet],
description: DevoloImageEntityDescription,
) -> None:
"""Initialize entity."""
@@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import StrEnum
from typing import Any, Generic, TypeVar
from typing import Any
from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo
from devolo_plc_api.plcnet_api import REMOTE, DataRate, LogicalNetwork
@@ -20,7 +20,6 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory, UnitOfDataRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import utcnow
from . import DevoloHomeNetworkConfigEntry
@@ -32,9 +31,10 @@ from .const import (
PLC_RX_RATE,
PLC_TX_RATE,
)
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
def _last_restart(runtime: int) -> datetime:
@@ -47,26 +47,10 @@ def _last_restart(runtime: int) -> datetime:
)
_CoordinatorDataT = TypeVar(
"_CoordinatorDataT",
bound=LogicalNetwork
| DataRate
| list[ConnectedStationInfo]
| list[NeighborAPInfo]
| int,
)
_ValueDataT = TypeVar(
"_ValueDataT",
bound=LogicalNetwork
| DataRate
| list[ConnectedStationInfo]
| list[NeighborAPInfo]
| int,
)
_SensorDataT = TypeVar(
"_SensorDataT",
bound=int | float | datetime,
type _CoordinatorDataType = (
LogicalNetwork | DataRate | list[ConnectedStationInfo] | list[NeighborAPInfo] | int
)
type _SensorDataType = int | float | datetime
class DataRateDirection(StrEnum):
@@ -77,9 +61,10 @@ class DataRateDirection(StrEnum):
@dataclass(frozen=True, kw_only=True)
class DevoloSensorEntityDescription(
SensorEntityDescription, Generic[_CoordinatorDataT, _SensorDataT]
):
class DevoloSensorEntityDescription[
_CoordinatorDataT: _CoordinatorDataType,
_SensorDataT: _SensorDataType,
](SensorEntityDescription):
"""Describes devolo sensor entity."""
value_func: Callable[[_CoordinatorDataT], _SensorDataT]
@@ -200,8 +185,11 @@ async def async_setup_entry(
async_add_entities(entities)
class BaseDevoloSensorEntity(
Generic[_CoordinatorDataT, _ValueDataT, _SensorDataT],
class BaseDevoloSensorEntity[
_CoordinatorDataT: _CoordinatorDataType,
_ValueDataT: _CoordinatorDataType,
_SensorDataT: _SensorDataType,
](
DevoloCoordinatorEntity[_CoordinatorDataT],
SensorEntity,
):
@@ -210,7 +198,7 @@ class BaseDevoloSensorEntity(
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[_CoordinatorDataT],
coordinator: DevoloDataUpdateCoordinator[_CoordinatorDataT],
description: DevoloSensorEntityDescription[_ValueDataT, _SensorDataT],
) -> None:
"""Initialize entity."""
@@ -218,9 +206,11 @@ class BaseDevoloSensorEntity(
super().__init__(entry, coordinator)
class DevoloSensorEntity(
BaseDevoloSensorEntity[_CoordinatorDataT, _CoordinatorDataT, _SensorDataT]
):
class DevoloSensorEntity[
_CoordinatorDataT: _CoordinatorDataType,
_ValueDataT: _CoordinatorDataType,
_SensorDataT: _SensorDataType,
](BaseDevoloSensorEntity[_CoordinatorDataT, _ValueDataT, _SensorDataT]):
"""Representation of a generic devolo sensor."""
entity_description: DevoloSensorEntityDescription[_CoordinatorDataT, _SensorDataT]
@@ -241,7 +231,7 @@ class DevoloPlcDataRateSensorEntity(
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[LogicalNetwork],
coordinator: DevoloDataUpdateCoordinator[LogicalNetwork],
description: DevoloSensorEntityDescription[DataRate, float],
peer: str,
) -> None:
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from typing import Any
from devolo_plc_api.device import Device
from devolo_plc_api.device_api import WifiGuestAccessGet
@@ -15,19 +15,19 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
_DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool)
type _DataType = WifiGuestAccessGet | bool
@dataclass(frozen=True, kw_only=True)
class DevoloSwitchEntityDescription(SwitchEntityDescription, Generic[_DataT]):
class DevoloSwitchEntityDescription[_DataT: _DataType](SwitchEntityDescription):
"""Describes devolo switch entity."""
is_on_func: Callable[[_DataT], bool]
@@ -81,7 +81,9 @@ async def async_setup_entry(
async_add_entities(entities)
class DevoloSwitchEntity(DevoloCoordinatorEntity[_DataT], SwitchEntity):
class DevoloSwitchEntity[_DataT: _DataType](
DevoloCoordinatorEntity[_DataT], SwitchEntity
):
"""Representation of a devolo switch."""
entity_description: DevoloSwitchEntityDescription[_DataT]
@@ -89,7 +91,7 @@ class DevoloSwitchEntity(DevoloCoordinatorEntity[_DataT], SwitchEntity):
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator[_DataT],
coordinator: DevoloDataUpdateCoordinator[_DataT],
description: DevoloSwitchEntityDescription[_DataT],
) -> None:
"""Initialize entity."""
@@ -20,13 +20,13 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, REGULAR_FIRMWARE
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 1
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
@@ -79,7 +79,7 @@ class DevoloUpdateEntity(DevoloCoordinatorEntity, UpdateEntity):
def __init__(
self,
entry: DevoloHomeNetworkConfigEntry,
coordinator: DataUpdateCoordinator,
coordinator: DevoloDataUpdateCoordinator,
description: DevoloUpdateEntityDescription,
) -> None:
"""Initialize entity."""
@@ -46,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator[GlucoseReading](
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/doorbird",
"iot_class": "local_push",
"loggers": ["doorbirdpy"],
"requirements": ["DoorBirdPy==3.0.4"],
"requirements": ["DoorBirdPy==3.0.8"],
"zeroconf": [
{
"type": "_axis-video._tcp.local.",
@@ -69,6 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=lock.name,
update_method=_async_update,
update_interval=timedelta(seconds=UPDATE_SECONDS),
@@ -37,8 +37,8 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
self._device_tracker = None
self._previous_position = None
async def async_config_entry_first_refresh(self) -> None:
"""Perform first refresh."""
async def _async_setup(self) -> None:
"""Set up coordinator."""
if region_identifier := self.config_entry.data.get(CONF_REGION_IDENTIFIER):
self.api = await self.hass.async_add_executor_job(
DwdWeatherWarningsAPI, region_identifier
@@ -48,8 +48,6 @@ class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
CONF_REGION_DEVICE_TRACKER
)
await super().async_config_entry_first_refresh()
async def _async_update_data(self) -> None:
"""Get the latest data from the DWD Weather Warnings API."""
if self._device_tracker:

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