Compare commits

...

205 Commits

Author SHA1 Message Date
Franck Nijhof 0644d782cd 2024.11.3 (#131248) 2024-11-22 11:55:45 +01:00
Franck Nijhof 4ef50ffd88 Bump version to 2024.11.3 2024-11-22 11:05:59 +01:00
starkillerOG bfcd4194f3 Bump reolink_aio to 0.11.2 (#131237) 2024-11-22 11:05:37 +01:00
rappenze 2f05240e4c Fix fibaro cover state is not always correct (#131206) 2024-11-22 11:05:34 +01:00
starkillerOG 44ad8081a3 Reolink log fast poll errors once (#131203) 2024-11-22 11:05:30 +01:00
Jesse Hills 780eaa8379 Fix typo in ESPHome repair text (#131200) 2024-11-22 11:05:26 +01:00
Norbert Rittel 75dcdfb087 Fix cast translation string (#131156) 2024-11-22 11:05:23 +01:00
Norbert Rittel c88ff2ca44 Fix typo in name of "Alarm arm home instant" action (#131151) 2024-11-22 11:05:19 +01:00
Norbert Rittel 402c668f05 Replace "service" with "action" in zha:reconfigure_device (#131111)
Replace "service" with "action" in one description

As services are now actions in HA this needs to be fixed.
2024-11-22 11:05:14 +01:00
Álvaro Fernández Rojas 93b4570c04 Update aioairzone to v0.9.7 (#131033) 2024-11-22 10:57:08 +01:00
Álvaro Fernández Rojas 50a610914b Bump aioairzone to 0.9.6 (#130559)
* Update aioairzone to v0.9.6

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Remove _async_migrator_mac_empty and improve tests

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Remove WebServer empty mac fixes as requested by @epenet

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

---------

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-11-22 10:57:04 +01:00
G Johansson 8db18181d0 Bump holidays to 0.61 (#130984) 2024-11-22 10:52:43 +01:00
G Johansson 335124acc6 Add missing catholic category in workday (#130983) 2024-11-22 10:52:40 +01:00
Norbert Rittel 24ccb9b894 Add more UI user-friendly description to six Supervisor actions (#130971) 2024-11-22 10:52:36 +01:00
Jan-Philipp Benecke a75ce850b8 Strip whitespaces from host in ping config flow (#130970) 2024-11-22 10:52:33 +01:00
Renat Sibgatulin 4753510ace Bump aioairq to 0.4.3 (#130963) 2024-11-22 10:52:30 +01:00
ElmaxSrl fc607ea7e5 Update elmax_api to v0.0.6.1 (#130917)
Co-authored-by: Alberto Geniola <albertogeniola@gmail.com>
2024-11-22 10:52:27 +01:00
Sergio Conde Gómez 477141c22a Unscape HTML Entities from RSS feeds (#130915)
* Unscape HTML Entities from RSS feeds

* Improve tests
2024-11-22 10:52:23 +01:00
Charles Yuan aaa36adbcc Fixed Small Inaccuracy in Description String for myUplink (#130900) 2024-11-22 10:52:20 +01:00
J. Nick Koston 9447180c04 Bump bluetooth-adapters to 0.20.2 (#130877) 2024-11-22 10:52:17 +01:00
epenet 6853234f9d Pass config_entry explicitly in rachio (#130865) 2024-11-22 10:52:14 +01:00
G Johansson 6944ba0333 Use default device sensors also for AirQ devices in Sensibo (#130841) 2024-11-22 10:52:10 +01:00
starkillerOG 04bc041174 Reolink fix dev/entity id migration (#130836) 2024-11-22 10:52:07 +01:00
Glenn Waters a024acf096 UPB integration: Change unique ID from int to string. (#130832) 2024-11-22 10:52:04 +01:00
hahn-th 5b1aca53ac Bump homematicip to 1.1.3 (#130824) 2024-11-22 10:52:00 +01:00
Michael a588ced2e3 Fix unexpected stop of media playback via ffmpeg proxy for ESPhome devices (#130788)
disable writing progress stats to stderr in ffmpeg command
2024-11-22 10:51:57 +01:00
Franck Nijhof 876112ff54 Update twentemilieu to 2.1.0 (#130752) 2024-11-22 10:51:54 +01:00
Jan Bouwhuis a48f88033d Fix file uploads in MQTT config flow not processed in executor (#130746)
Process file uploads in MQTT config flow in executor
2024-11-22 10:51:49 +01:00
Patrick 5deba1766e Fix and bump apsystems-ez1 to 2.4.0 (#130740) 2024-11-22 10:51:45 +01:00
Davin Kevin 4863243f5a Prevent endless loop in recorder when using a filter and there are no more states to purge (#126149)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-11-22 10:51:35 +01:00
Franck Nijhof 847afabed1 2024.11.2 (#130713) 2024-11-15 20:16:10 +01:00
Franck Nijhof ac270e19be Bump version to 2024.11.2 2024-11-15 19:35:42 +01:00
Matt Zimmerman ca40b96a89 Bump python-smarttub to 0.0.38 (#130679) 2024-11-15 19:35:14 +01:00
epenet 045e285bfe Fix missing translations in onewire (#130673) 2024-11-15 19:35:11 +01:00
epenet 8d6f2e78f5 Fix missing translations in generic (#130672) 2024-11-15 19:35:07 +01:00
epenet 9e4d26137e Fix missing translations in madvr (#130656) 2024-11-15 19:35:04 +01:00
epenet f74bfdc974 Fix missing translations in toon (#130655) 2024-11-15 19:35:00 +01:00
epenet 1cabcdf257 Fix missing translations in tradfri (#130654)
* Fix missing translations in tradfri

* Simplify
2024-11-15 19:34:57 +01:00
epenet c6931d656e Fix missing translations in utility_meter (#130652) 2024-11-15 19:34:54 +01:00
epenet 942830505a Fix missing translations in vilfo (#130650) 2024-11-15 19:34:51 +01:00
Jan-Philipp Benecke 880f28e28a Remove dumping config entry to log in setup of roborock (#130648) 2024-11-15 19:34:48 +01:00
Johan Nenzén f406ffa75a Bump pyplaato to 0.0.19 (#130641)
Bumps version of pyplaato to 0.0.19
2024-11-15 19:34:44 +01:00
epenet 0d695c843f Add missing translation string to philips_js (#130637) 2024-11-15 19:34:41 +01:00
epenet 5f09eb97e1 Add missing translation string to lg_netcast (#130635) 2024-11-15 19:34:38 +01:00
epenet 6d561ca373 Add missing translation string to hvv_departures (#130634) 2024-11-15 19:34:34 +01:00
Alistair Galbraith 663ebe199d Fix scene loading issue (#130627) 2024-11-15 19:34:31 +01:00
Keilin Bickar 8b9c4db2b3 Bump sense-energy to 0.13.4 (#130625) 2024-11-15 19:34:27 +01:00
epenet e478b9b599 Add missing translation string to smarty (#130624) 2024-11-15 19:34:23 +01:00
Robert Resch 5acdf58976 Fix hassfest by adding go2rtc reqs (#130602) 2024-11-15 19:33:09 +01:00
starkillerOG 6d861e7f47 Bump reolink-aio to 0.11.1 (#130600) 2024-11-15 19:32:30 +01:00
Johan Nenzén 281a8eda31 Fixes webhook schema for different temp and volume units (#130578) 2024-11-15 19:32:26 +01:00
Simone Chemelli 1bc005d0d4 Update uptime deviation for Vodafone Station (#130571)
Update sensor.py
2024-11-15 19:32:23 +01:00
puddly 95d60987ab Bump ZHA dependencies (#130563) 2024-11-15 19:32:19 +01:00
J. Nick Koston 53e38454b2 Fix non-thread-safe operation in powerview number (#130557) 2024-11-15 19:32:16 +01:00
Brig Lamoreaux 876b86cd3d fix translation in srp_energy (#130540) 2024-11-15 19:32:13 +01:00
Robert Resch cb104935ea Add go2rtc recommended version (#130508) 2024-11-15 19:32:10 +01:00
Joost Lekkerkerker 4c24e26926 Bump aiowithings to 3.1.3 (#130504) 2024-11-15 19:32:06 +01:00
Robert Resch 4b13d8bc47 Bump go2rtc-client to 0.1.1 (#130498) 2024-11-15 19:30:50 +01:00
Tony 433e3718f8 Bump aioruckus to 0.42 (#130487) 2024-11-15 19:28:38 +01:00
Sheldon Ip 1e3c2c0631 Fix translations in subaru (#130486) 2024-11-15 19:28:34 +01:00
starkillerOG 3a2f996c13 Bump reolink_aio to 0.11.0 (#130481) 2024-11-15 19:28:30 +01:00
G Johansson e4cb3c67d9 Fix legacy _attr_state handling in AlarmControlPanel (#130479) 2024-11-15 19:28:27 +01:00
puddly 8a22433168 Ensure ZHA setup works with container installs (#130470) 2024-11-15 19:28:23 +01:00
Joost Lekkerkerker 0976476d16 Bump aiowithings to 3.1.2 (#130469) 2024-11-15 19:28:19 +01:00
Kelvin Dekker 28f46a0f88 Fix typo in file strings (#130465) 2024-11-15 19:28:16 +01:00
G Johansson 8b173656e7 Fix translation in statistics (#130455)
* Fix translation in statistics

* Update homeassistant/components/statistics/strings.json
2024-11-15 19:28:12 +01:00
Joost Lekkerkerker 08f6f2759b Add title to water heater component (#130446) 2024-11-15 19:28:09 +01:00
Steven B. f4798d27c7 Do not trigger events for updated ring events (#130430) 2024-11-15 19:28:05 +01:00
Steven B. 103a84b4bd Bump ring-doorbell to 0.9.12 (#130419) 2024-11-15 19:28:01 +01:00
Steven B. 4d3502e061 Bump ring library ring-doorbell to 0.9.9 (#129966) 2024-11-15 19:26:59 +01:00
J. Nick Koston 79329e16cf Fix missing title placeholders in powerwall reauth (#130389) 2024-11-15 19:24:37 +01:00
Daniel Hjelseth Høyer 929164251a Bump Tibber 0.30.8 (#130388) 2024-11-15 19:24:34 +01:00
Joost Lekkerkerker 300724443a Bump spotifyaio to 0.8.8 (#130372) 2024-11-15 19:24:30 +01:00
Robert Resch 70ef3a355c Go2rtc bump and set ffmpeg logs to debug (#130371) 2024-11-15 19:24:26 +01:00
Jan Bouwhuis 83162c1461 Fix typo in go2rtc (#130165)
Fix typo in original
2024-11-15 19:24:20 +01:00
Jan Bouwhuis a12c76dbdd Use f-strings in go2rtc code and test and do not use abbreviation (#130158) 2024-11-15 19:22:09 +01:00
Noah Husby 9292b6da3d Disable brightness from devices with no display in Cambridge Audio (#130369) 2024-11-15 19:03:04 +01:00
Simon Lamon 8d05183de2 Add Spotify and Tidal to playingmode mapping (#130351) 2024-11-15 19:03:01 +01:00
Simon Lamon a86ff41bbc Add seek support to LinkPlay (#130349) 2024-11-15 19:02:58 +01:00
Simon Lamon ce92f3de44 Bump python-linkplay to 0.0.20 (#130348) 2024-11-15 19:02:54 +01:00
LG-ThinQ-Integration 465d8b2ee2 Fix fan's warning TURN_ON, TURN_OFF (#130327)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2024-11-15 19:02:51 +01:00
G Johansson 218eedfd93 Fix Homekit error handling alarm state unknown or unavailable (#130311) 2024-11-15 19:02:47 +01:00
Simone Chemelli afec354b84 Avoid Shelly data update during shutdown (#130301) 2024-11-15 19:02:44 +01:00
Allen Porter 282f92e5f3 Ignore WebRTC candidates for nest cameras (#130294) 2024-11-15 19:02:41 +01:00
David Knowles f6cd74e2d7 Make Hydrawise poll non-critical data less frequently (#130289) 2024-11-15 19:02:37 +01:00
Åke Strandberg f821ddeab8 Add more f-series models to myuplink (#130283) 2024-11-15 19:02:34 +01:00
Allen Porter d408b7ac62 Improve nest camera stream expiration to be defensive against errors (#130265) 2024-11-15 19:02:31 +01:00
Michael 83baa1a788 Fix translation key for done response in conversation (#130247) 2024-11-15 19:02:27 +01:00
Max Shcherbina 07a8cf14cd Update generic thermostat strings for clarity and accuracy (#130243) 2024-11-15 19:02:24 +01:00
Olivier Corradi 9f447af468 Rename "CO2 Signal" display name to Electricity Maps for consistency (#130242)
* Update strings.json for Electricity Maps

* Update strings.json

* Update config_flow.py

* Update test_config_flow.py

* Fix test
2024-11-15 19:02:20 +01:00
Allen Porter c399d8f571 Bump google-nest-sdm to 6.1.5 (#130229) 2024-11-15 19:02:17 +01:00
jjlawren 4ea9574229 Bump SoCo to 0.30.6 (#130223) 2024-11-15 19:02:14 +01:00
Daniel Hjelseth Høyer 592b8ed0a0 Bump pyTibber (#130216) 2024-11-15 19:02:10 +01:00
Simone Chemelli 6b91c0810a Fix uptime sensor for Vodafone Station (#130215) 2024-11-15 19:02:06 +01:00
Max Shcherbina 9579e4a9c1 Fix wording in Google Calendar create_event strings for consistency (#130183) 2024-11-15 19:00:06 +01:00
IceBotYT 7f4f90f06d Bump nice-go to 0.3.10 (#130173)
Bump Nice G.O. to 0.3.10
2024-11-15 19:00:02 +01:00
Sheldon Ip 701a901fe4 Fix translations in ollama (#130164) 2024-11-15 18:59:59 +01:00
Simon Lamon f914642e31 No longer thrown an error when device is offline in linkplay (#130161) 2024-11-15 18:59:55 +01:00
Simon Lamon 32dc9fc238 Allow dynamic max preset in linkplay play preset (#130160) 2024-11-15 18:59:52 +01:00
Simon Lamon b27e0f9fe7 Bump python-linkplay to v0.0.18 (#130159) 2024-11-15 18:59:47 +01:00
Thomas55555 f040060b3c Fix RecursionError in Husqvarna Automower coordinator (#123085)
* reach maximum recursion depth exceeded in tests

* second background task

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* test

* modify test

* tests

* use correct exception

* reset mock

* use recursion_limit

* remove unneeded ticks

* test TimeoutException

* set lower recursionlimit

* remove not that important comment and move the other

* test that we connect and listen successfully

* Simulate hass shutting down

* skip testing against the recursion limit

* Update homeassistant/components/husqvarna_automower/coordinator.py

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

* mock

* Remove comment

* Revert "mock"

This reverts commit e8ddaea3d79ed1aceb696a055cc42ad08b4febca.

* Move patch to decorator

* Make execution of patched methods predictable

* Parametrize test, make mocked start_listening block

* Apply suggestions from code review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
2024-11-15 18:47:59 +01:00
J. Nick Koston cc45793896 Bump aiohttp to 3.10.11 (#130483)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-15 09:42:20 +01:00
Franck Nijhof ab0556227c 2024.11.1 (#130156) 2024-11-08 19:42:10 +01:00
Franck Nijhof c16fb9c93d Bump version to 2024.11.1 2024-11-08 18:58:21 +01:00
Jan Bouwhuis da8fc7a2fc Refrase imap fetch service description string (#130152) 2024-11-08 18:58:07 +01:00
Allen Porter 864b4d86f2 Fix bugs in nest stream expiration handling (#130150) 2024-11-08 18:58:04 +01:00
Louis Christ 1bb0ced7c0 Fix volume_up not working in some cases in bluesound integration (#130146) 2024-11-08 18:58:00 +01:00
Martin Hjelmare 2fe4fc908b Bump ha-ffmpeg to 3.2.2 (#130142) 2024-11-08 18:57:25 +01:00
Joost Lekkerkerker aa2c3b046f Bump spotifyaio to 0.8.7 (#130140) 2024-11-08 18:56:15 +01:00
Robert Resch 22822cb8aa Add go2rtc workaround for HA managed one until upstream fixes it (#130139) 2024-11-08 18:56:12 +01:00
Shai Ungar b71383c997 Fix issue when timestamp is None (#130133)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:56:09 +01:00
Bram Kragten b0b163df48 Update frontend to 20241106.2 (#130128) 2024-11-08 18:56:06 +01:00
Luke Lashley 35539dbf60 Bump python-roborock to 2.7.2 (#130100) 2024-11-08 18:56:02 +01:00
Bram Kragten 09d03e8edf Update frontend to 20241106.1 (#130086) 2024-11-08 18:55:59 +01:00
Kelvin Dekker 46e37f3bdd Fix typo in insteon strings (#130085) 2024-11-08 18:55:55 +01:00
Klaas Schoute 0206c149cf Force int value on port in P1Monitor (#130084) 2024-11-08 18:55:52 +01:00
Josef Zweck 29620ef977 Add missing string to tedee plus test (#130081) 2024-11-08 18:55:49 +01:00
Erik Montnemery 9012b113ad Don't create repairs asking user to remove duplicate flipr config entries (#130058)
* Don't create repairs asking user to remove duplicate flipr config entries

* Improve comments
2024-11-08 18:55:46 +01:00
Allen Porter 5f5f6cc3d5 Fix KeyError in nest integration when the old key format does not exist (#130057)
* Fix bug in nest setup when the old key format does not exist

* Further simplify the entry.data check

* Update homeassistant/components/nest/api.py

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

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-11-08 18:55:42 +01:00
Erik Montnemery 7ff501f3ec Don't create repairs asking user to remove duplicate ignored config entries (#130056) 2024-11-08 18:55:39 +01:00
sean t b0f110b9ab Bump agent-py to 0.0.24 (#130018)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:36 +01:00
epenet 2692bc23a5 Add missing placeholder description to twitch (#130013) 2024-11-08 18:55:33 +01:00
Allen Porter 1beac5f0f8 Bump google-nest-sdm to 6.1.4 (#130005)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-11-08 18:55:29 +01:00
Keilin Bickar ec7ba1b7fd Update sense energy library to 0.13.3 (#129998) 2024-11-08 18:55:25 +01:00
Brett Adams 5bd1b0dd9c Fix Trunks in Teslemetry and Tesla Fleet (#129986) 2024-11-08 18:55:21 +01:00
Michael Hansen a2ad4c9cfd Bump intents to 2024.11.6 (#129982) 2024-11-08 18:52:43 +01:00
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
290 changed files with 4733 additions and 1290 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
- id: hassfest-mypy-config
name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
+3 -2
View File
@@ -7,7 +7,8 @@ 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
@@ -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.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
+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,
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling",
"loggers": ["agent"],
"requirements": ["agent-py==0.0.23"]
"requirements": ["agent-py==0.0.24"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.3.2"]
"requirements": ["aioairq==0.4.3"]
}
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.5"]
"requirements": ["aioairzone==0.9.7"]
}
@@ -6,7 +6,7 @@ import asyncio
from datetime import timedelta
from functools import partial
import logging
from typing import Any, Final, final
from typing import TYPE_CHECKING, Any, Final, final
from propcache import cached_property
import voluptuous as vol
@@ -221,9 +221,15 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
@property
def state(self) -> str | None:
"""Return the current state."""
if (alarm_state := self.alarm_state) is None:
return None
return alarm_state
if (alarm_state := self.alarm_state) is not None:
return alarm_state
if self._attr_state is not None:
# Backwards compatibility for integrations that set state directly
# Should be removed in 2025.11
if TYPE_CHECKING:
assert isinstance(self._attr_state, str)
return self._attr_state
return None
@cached_property
def alarm_state(self) -> AlarmControlPanelState | None:
@@ -38,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ApSystemsConfigEntry) ->
ip_address=entry.data[CONF_IP_ADDRESS],
port=entry.data.get(CONF_PORT, DEFAULT_PORT),
timeout=8,
enable_debounce=True,
)
coordinator = ApSystemsDataCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==2.2.1"]
"requirements": ["apsystems-ez1==2.4.0"]
}
+2 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any
from aiohttp.client_exceptions import ClientConnectionError
from APsystemsEZ1 import InverterReturnedError
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.core import HomeAssistant
@@ -40,7 +41,7 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
"""Update switch status and availability."""
try:
status = await self._api.get_device_power_status()
except (TimeoutError, ClientConnectionError):
except (TimeoutError, ClientConnectionError, InverterReturnedError):
self._attr_available = False
else:
self._attr_available = True
@@ -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,
)
+19 -17
View File
@@ -21,41 +21,57 @@ class BangOlufsenSource:
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,
)
@@ -170,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(
@@ -191,7 +193,7 @@ 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,
@@ -199,7 +201,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
Source(
id="bluetooth",
is_enabled=True,
is_playable=False,
is_playable=True,
name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"),
is_seekable=False,
@@ -207,7 +209,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
Source(
id="spotify",
is_enabled=True,
is_playable=False,
is_playable=True,
name="Spotify Connect",
type=SourceTypeEnum(value="spotify"),
is_seekable=True,
@@ -70,7 +70,6 @@ from .const import (
CONNECTION_STATUS,
DOMAIN,
FALLBACK_SOURCES,
HIDDEN_SOURCE_IDS,
VALID_MEDIA_TYPES,
BangOlufsenMediaType,
BangOlufsenSource,
@@ -169,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,
@@ -243,7 +243,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
async def _async_update_sources(self) -> None:
async def _async_update_sources(self, _: Source | None = None) -> None:
"""Get sources for the specific product."""
# Audio sources
@@ -270,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
@@ -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(
@@ -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:
@@ -769,7 +770,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None:
"""Send volume_up command to media player."""
volume = int(volume * 100)
volume = int(round(volume * 100))
volume = min(100, volume)
volume = max(0, volume)
@@ -16,7 +16,7 @@
"requirements": [
"bleak==0.22.3",
"bleak-retry-connector==3.6.0",
"bluetooth-adapters==0.20.0",
"bluetooth-adapters==0.20.2",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.20.0",
"dbus-fast==2.24.3",
@@ -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"
}
}
}
+2 -1
View File
@@ -16,7 +16,8 @@
"list_access": {
"default": "mdi:account-lock",
"state": {
"shared": "mdi:account-group"
"shared": "mdi:account-group",
"invitation": "mdi:account-multiple-plus"
}
}
},
+1 -1
View File
@@ -79,7 +79,7 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
translation_key=BringSensor.LIST_ACCESS,
value_fn=lambda lst, _: lst["status"].lower(),
entity_category=EntityCategory.DIAGNOSTIC,
options=["registered", "shared"],
options=["registered", "shared", "invitation"],
device_class=SensorDeviceClass.ENUM,
),
)
+2 -1
View File
@@ -66,7 +66,8 @@
"name": "List access",
"state": {
"registered": "Private",
"shared": "Shared"
"shared": "Shared",
"invitation": "Invitation pending"
}
}
}
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"requirements": ["aiostreammagic==2.8.4"],
"requirements": ["aiostreammagic==2.8.5"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
@@ -51,8 +51,13 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
CambridgeAudioSelectEntityDescription(
key="display_brightness",
translation_key="display_brightness",
options=[x.value for x in DisplayBrightness],
options=[
DisplayBrightness.BRIGHT.value,
DisplayBrightness.DIM.value,
DisplayBrightness.OFF.value,
],
entity_category=EntityCategory.CONFIG,
load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE,
value_fn=lambda client: client.display.brightness,
set_value_fn=lambda client, value: client.set_display_brightness(
DisplayBrightness(value)
+57 -25
View File
@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
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
@@ -472,6 +472,8 @@ 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] = {}
@@ -484,9 +486,13 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._webrtc_sync_offer = (
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:
@@ -623,7 +629,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
if self._webrtc_sync_offer:
if self._supports_native_sync_webrtc:
try:
answer = await self.async_handle_web_rtc_offer(offer_sdp)
except ValueError as ex:
@@ -779,6 +785,9 @@ 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()
self.__supports_stream = (
self.supported_features_compat & CameraEntityFeature.STREAM
)
await self.async_refresh_providers(write_state=False)
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
@@ -788,18 +797,25 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
providers or inputs to the state attributes change.
"""
old_provider = self._webrtc_provider
new_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_provider
)
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None
new_legacy_provider = None
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
# 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
@@ -827,20 +843,26 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
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._webrtc_sync_offer or self._legacy_webrtc_provider is not None
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: str) -> None:
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)
@@ -864,12 +886,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat:
if (
type(self).async_handle_web_rtc_offer
!= Camera.async_handle_web_rtc_offer
or type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
):
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:
@@ -880,6 +897,21 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
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."""
+15 -4
View File
@@ -11,7 +11,7 @@ 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
@@ -78,7 +78,14 @@ class WebRTCAnswer(WebRTCMessage):
class WebRTCCandidate(WebRTCMessage):
"""WebRTC candidate."""
candidate: str
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)
@@ -138,7 +145,9 @@ class CameraWebRTCProvider(ABC):
"""Handle the WebRTC offer and return the answer via the provided callback."""
@abstractmethod
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
async def async_on_webrtc_candidate(
self, session_id: str, candidate: RTCIceCandidate
) -> None:
"""Handle the WebRTC candidate."""
@callback
@@ -319,7 +328,9 @@ async def ws_candidate(
)
return
await camera.async_on_webrtc_candidate(msg["session_id"], msg["candidate"])
await camera.async_on_webrtc_candidate(
msg["session_id"], RTCIceCandidate(msg["candidate"])
)
connection.send_message(websocket_api.result_message(msg["id"]))
+1 -1
View File
@@ -53,7 +53,7 @@
},
"view_path": {
"name": "View path",
"description": "The path of the dashboard view to show."
"description": "The URL path of the dashboard view to show."
}
}
}
@@ -168,7 +168,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_create_entry(
title=get_extra_name(data) or "CO2 Signal",
title=get_extra_name(data) or "Electricity Maps",
data=data,
)
@@ -294,7 +294,7 @@ class DefaultAgent(ConversationEntity):
self.hass, language, DOMAIN, [DOMAIN]
)
response_text = translations.get(
f"component.{DOMAIN}.agent.done", "Done"
f"component.{DOMAIN}.conversation.agent.done", "Done"
)
response.async_set_speech(response_text)
@@ -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.30"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"]
}
+1 -1
View File
@@ -68,7 +68,7 @@
}
},
"alarm_arm_home_instant": {
"name": "Alarm are home instant",
"name": "Alarm arm home instant",
"description": "Arms the ElkM1 in home instant mode.",
"fields": {
"code": {
+1 -1
View File
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/elmax",
"iot_class": "cloud_polling",
"loggers": ["elmax_api"],
"requirements": ["elmax-api==0.0.5"],
"requirements": ["elmax-api==0.0.6.1"],
"zeroconf": [
{
"type": "_elmax-ssl._tcp.local."
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.2"]
"requirements": ["sense-energy==0.13.4"]
}
@@ -179,6 +179,9 @@ class FFmpegConvertResponse(web.StreamResponse):
# Remove metadata and cover art
command_args.extend(["-map_metadata", "-1", "-vn"])
# disable progress stats on stderr
command_args.append("-nostats")
# Output to stdout
command_args.append("pipe:")
+4 -2
View File
@@ -570,8 +570,10 @@ def _async_setup_device_registry(
configuration_url = None
if device_info.webserver_port > 0:
configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}"
elif (dashboard := async_get_dashboard(hass)) and dashboard.data.get(
device_info.name
elif (
(dashboard := async_get_dashboard(hass))
and dashboard.data
and dashboard.data.get(device_info.name)
):
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
@@ -118,7 +118,7 @@
},
"service_calls_not_allowed": {
"title": "{name} is not permitted to perform Home Assistant actions",
"description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perfom Home Assistant action, you can enable this functionality in the options flow."
"description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perform Home Assistant action, you can enable this functionality in the options flow."
}
}
}
@@ -2,6 +2,7 @@
from __future__ import annotations
import html
import logging
from typing import Any
import urllib.error
@@ -106,7 +107,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
return self.abort_on_import_error(user_input[CONF_URL], "url_error")
return self.show_user_form(user_input, {"base": "url_error"})
feed_title = feed["feed"]["title"]
feed_title = html.unescape(feed["feed"]["title"])
return self.async_create_entry(
title=feed_title,
@@ -4,6 +4,7 @@ from __future__ import annotations
from calendar import timegm
from datetime import datetime
import html
from logging import getLogger
from time import gmtime, struct_time
from typing import TYPE_CHECKING
@@ -102,7 +103,8 @@ class FeedReaderCoordinator(
"""Set up the feed manager."""
feed = await self._async_fetch_feed()
self.logger.debug("Feed data fetched from %s : %s", self.url, feed["feed"])
self.feed_author = feed["feed"].get("author")
if feed_author := feed["feed"].get("author"):
self.feed_author = html.unescape(feed_author)
self.feed_version = feedparser.api.SUPPORTED_VERSIONS.get(feed["version"])
self._feed = feed
+10 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import html
import logging
from feedparser import FeedParserDict
@@ -76,15 +77,22 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity):
# so we always take the first entry in list, since we only care about the latest entry
feed_data: FeedParserDict = data[0]
if description := feed_data.get("description"):
description = html.unescape(description)
if title := feed_data.get("title"):
title = html.unescape(title)
if content := feed_data.get("content"):
if isinstance(content, list) and isinstance(content[0], dict):
content = content[0].get("value")
content = html.unescape(content)
self._trigger_event(
EVENT_FEEDREADER,
{
ATTR_DESCRIPTION: feed_data.get("description"),
ATTR_TITLE: feed_data.get("title"),
ATTR_DESCRIPTION: description,
ATTR_TITLE: title,
ATTR_LINK: feed_data.get("link"),
ATTR_CONTENT: content,
},
@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ffmpeg",
"integration_type": "system",
"requirements": ["ha-ffmpeg==3.2.1"]
"requirements": ["ha-ffmpeg==3.2.2"]
}
+19 -40
View File
@@ -69,37 +69,29 @@ class FibaroCover(FibaroEntity, CoverEntity):
# so if it is missing we have a device which supports open / close only
return not self.fibaro_device.value.has_value
@property
def current_cover_position(self) -> int | None:
"""Return current position of cover. 0 is closed, 100 is open."""
return self.bound(self.level)
def update(self) -> None:
"""Update the state."""
super().update()
@property
def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt position for venetian blinds."""
return self.bound(self.level2)
self._attr_current_cover_position = self.bound(self.level)
self._attr_current_cover_tilt_position = self.bound(self.level2)
@property
def is_opening(self) -> bool | None:
"""Return if the cover is opening or not.
device_state = self.fibaro_device.state
Be aware that this property is only available for some modern devices.
For example the Fibaro Roller Shutter 4 reports this correctly.
"""
if self.fibaro_device.state.has_value:
return self.fibaro_device.state.str_value().lower() == "opening"
return None
# Be aware that opening and closing is only available for some modern
# devices.
# For example the Fibaro Roller Shutter 4 reports this correctly.
if device_state.has_value:
self._attr_is_opening = device_state.str_value().lower() == "opening"
self._attr_is_closing = device_state.str_value().lower() == "closing"
@property
def is_closing(self) -> bool | None:
"""Return if the cover is closing or not.
Be aware that this property is only available for some modern devices.
For example the Fibaro Roller Shutter 4 reports this correctly.
"""
if self.fibaro_device.state.has_value:
return self.fibaro_device.state.str_value().lower() == "closing"
return None
closed: bool | None = None
if self._is_open_close_only():
if device_state.has_value and device_state.str_value().lower() != "unknown":
closed = device_state.str_value().lower() == "closed"
elif self.current_cover_position is not None:
closed = self.current_cover_position == 0
self._attr_is_closed = closed
def set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
@@ -109,19 +101,6 @@ class FibaroCover(FibaroEntity, CoverEntity):
"""Move the cover to a specific position."""
self.set_level2(cast(int, kwargs.get(ATTR_TILT_POSITION)))
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._is_open_close_only():
state = self.fibaro_device.state
if not state.has_value or state.str_value().lower() == "unknown":
return None
return state.str_value().lower() == "closed"
if self.current_cover_position is None:
return None
return self.current_cover_position == 0
def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self.action("open")
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyfibaro"],
"requirements": ["pyfibaro==0.7.8"]
"requirements": ["pyfibaro==0.8.0"]
}
+1 -1
View File
@@ -18,7 +18,7 @@
},
"data_description": {
"file_path": "The local file path to retrieve the sensor value from",
"value_template": "A template to render the the sensors value based on the file content",
"value_template": "A template to render the sensors value based on the file content",
"unit_of_measurement": "Unit of measurement for the sensor"
}
},
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20241031.0"]
"requirements": ["home-assistant-frontend==20241106.2"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
"requirements": ["ayla-iot-unofficial==1.4.2"]
"requirements": ["ayla-iot-unofficial==1.4.3"]
}
@@ -282,7 +282,7 @@ async def async_test_stream(
return {CONF_STREAM_SOURCE: "timeout"}
await stream.stop()
except StreamWorkerError as err:
return {CONF_STREAM_SOURCE: str(err)}
return {CONF_STREAM_SOURCE: "unknown_with_details", "error_details": str(err)}
except PermissionError:
return {CONF_STREAM_SOURCE: "stream_not_permitted"}
except OSError as err:
@@ -339,6 +339,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the start of the config flow."""
errors = {}
description_placeholders = {}
hass = self.hass
if user_input:
# Secondary validation because serialised vol can't seem to handle this complexity:
@@ -372,6 +373,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
# temporary preview for user to check the image
self.preview_cam = user_input
return await self.async_step_user_confirm_still()
if "error_details" in errors:
description_placeholders["error"] = errors.pop("error_details")
elif self.user_input:
user_input = self.user_input
else:
@@ -379,6 +382,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
data_schema=build_schema(user_input),
description_placeholders=description_placeholders,
errors=errors,
)
@@ -3,6 +3,7 @@
"config": {
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]",
"unknown_with_details": "An unknown error occurred: {error}",
"already_exists": "A camera with these URL settings already exists.",
"unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.",
"unable_still_load_auth": "Unable to load valid image from still image URL: The camera may require a user name and password, or they are not correct.",
@@ -3,7 +3,7 @@
"config": {
"step": {
"user": {
"title": "Add generic thermostat helper",
"title": "Add generic thermostat",
"description": "Create a climate entity that controls the temperature via a switch and sensor.",
"data": {
"ac_mode": "Cooling mode",
@@ -17,8 +17,8 @@
"data_description": {
"ac_mode": "Set the actuator specified to be treated as a cooling device instead of a heating device.",
"heater": "Switch entity used to cool or heat depending on A/C mode.",
"target_sensor": "Temperature sensor that reflect the current temperature.",
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on. This option will be ignored if the keep alive option is set.",
"target_sensor": "Temperature sensor that reflects the current temperature.",
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.",
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5."
}
+65 -18
View File
@@ -4,8 +4,9 @@ import logging
import shutil
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
from awesomeversion import AwesomeVersion
from go2rtc_client import Go2RtcRestClient
from go2rtc_client.exceptions import Go2RtcClientError
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
from go2rtc_client.ws import (
Go2RtcWsClient,
ReceiveMessages,
@@ -15,6 +16,7 @@ from go2rtc_client.ws import (
WsError,
)
import voluptuous as vol
from webrtc_models import RTCIceCandidate
from homeassistant.components.camera import (
Camera,
@@ -31,13 +33,23 @@ from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers import (
config_validation as cv,
discovery_flow,
issue_registry as ir,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.package import is_docker_env
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN
from .const import (
CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE,
DOMAIN,
HA_MANAGED_URL,
RECOMMENDED_VERSION,
)
from .server import Server
_LOGGER = logging.getLogger(__name__)
@@ -113,14 +125,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
server = Server(
hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
)
await server.start()
try:
await server.start()
except Exception: # noqa: BLE001
_LOGGER.warning("Could not start go2rtc server", exc_info=True)
return False
async def on_stop(event: Event) -> None:
await server.stop()
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
url = "http://localhost:1984/"
url = HA_MANAGED_URL
hass.data[_DATA_GO2RTC] = url
discovery_flow.async_create_flow(
@@ -142,7 +158,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Validate the server URL
try:
client = Go2RtcRestClient(async_get_clientsession(hass), url)
await client.streams.list()
version = await client.validate_server_version()
if version < AwesomeVersion(RECOMMENDED_VERSION):
ir.async_create_issue(
hass,
DOMAIN,
"recommended_version",
is_fixable=False,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key="recommended_version",
translation_placeholders={
"recommended_version": RECOMMENDED_VERSION,
"current_version": str(version),
},
)
except Go2RtcClientError as err:
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
raise ConfigEntryNotReady(
@@ -150,6 +180,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) from err
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
return False
except Go2RtcVersionError as err:
raise ConfigEntryNotReady(
f"The go2rtc server version is not supported, {err}"
) from err
except Exception as err: # noqa: BLE001
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
return False
@@ -202,16 +236,27 @@ class WebRTCProvider(CameraWebRTCProvider):
self._session, self._url, source=camera.entity_id
)
if not (stream_source := await camera.stream_source()):
send_message(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
)
return
streams = await self._rest_client.streams.list()
if camera.entity_id not in streams:
if not (stream_source := await camera.stream_source()):
send_message(
WebRTCError(
"go2rtc_webrtc_offer_failed", "Camera has no stream source"
)
)
return
await self._rest_client.streams.add(camera.entity_id, stream_source)
if (stream := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream.producers
):
await self._rest_client.streams.add(
camera.entity_id,
[
stream_source,
# We are setting any ffmpeg rtsp related logs to debug
# Connection problems to the camera will be logged by the first stream
# Therefore setting it to debug will not hide any important logs
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
],
)
@callback
def on_messages(message: ReceiveMessages) -> None:
@@ -219,7 +264,7 @@ class WebRTCProvider(CameraWebRTCProvider):
value: WebRTCMessage
match message:
case WebRTCCandidate():
value = HAWebRTCCandidate(message.candidate)
value = HAWebRTCCandidate(RTCIceCandidate(message.candidate))
case WebRTCAnswer():
value = HAWebRTCAnswer(message.sdp)
case WsError():
@@ -231,11 +276,13 @@ class WebRTCProvider(CameraWebRTCProvider):
config = camera.async_get_webrtc_client_configuration()
await ws_client.send(WebRTCOffer(offer_sdp, config.configuration.ice_servers))
async def async_on_webrtc_candidate(self, session_id: str, candidate: str) -> None:
async def async_on_webrtc_candidate(
self, session_id: str, candidate: RTCIceCandidate
) -> None:
"""Handle the WebRTC candidate."""
if ws_client := self._sessions.get(session_id):
await ws_client.send(WebRTCCandidate(candidate))
await ws_client.send(WebRTCCandidate(candidate.candidate))
else:
_LOGGER.debug("Unknown session %s. Ignoring candidate", session_id)
+3
View File
@@ -4,3 +4,6 @@ DOMAIN = "go2rtc"
CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
RECOMMENDED_VERSION = "1.9.7"
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
"integration_type": "system",
"iot_class": "local_polling",
"requirements": ["go2rtc-client==0.0.1b3"],
"requirements": ["go2rtc-client==0.1.1"],
"single_config_entry": true
}
+152 -11
View File
@@ -1,40 +1,76 @@
"""Go2rtc server."""
import asyncio
from collections import deque
from contextlib import suppress
import logging
from tempfile import NamedTemporaryFile
from go2rtc_client import Go2RtcRestClient
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
_LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5
_SETUP_TIMEOUT = 30
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
_LOCALHOST_IP = "127.0.0.1"
_LOG_BUFFER_SIZE = 512
_RESPAWN_COOLDOWN = 1
# Default configuration for HA
# - Api is listening only on localhost
# - Disable rtsp listener
# - Enable rtsp for localhost only as ffmpeg needs it
# - Clear default ice servers
_GO2RTC_CONFIG_FORMAT = r"""
_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
# Do not edit it manually
api:
listen: "{api_ip}:1984"
listen: "{api_ip}:{api_port}"
rtsp:
# ffmpeg needs rtsp for opus audio transcoding
listen: "127.0.0.1:8554"
listen: "127.0.0.1:18554"
webrtc:
listen: ":18555/tcp"
ice_servers: []
"""
_LOG_LEVEL_MAP = {
"TRC": logging.DEBUG,
"DBG": logging.DEBUG,
"INF": logging.DEBUG,
"WRN": logging.WARNING,
"ERR": logging.WARNING,
"FTL": logging.ERROR,
"PNC": logging.ERROR,
}
class Go2RTCServerStartError(HomeAssistantError):
"""Raised when server does not start."""
_message = "Go2rtc server didn't start correctly"
class Go2RTCWatchdogError(HomeAssistantError):
"""Raised on watchdog error."""
def _create_temp_file(api_ip: str) -> str:
"""Create temporary config file."""
# Set delete=False to prevent the file from being deleted when the file is closed
# Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write(_GO2RTC_CONFIG_FORMAT.format(api_ip=api_ip).encode())
file.write(
_GO2RTC_CONFIG_FORMAT.format(
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
).encode()
)
return file.name
@@ -47,14 +83,24 @@ class Server:
"""Initialize the server."""
self._hass = hass
self._binary = binary
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event()
self._api_ip = _LOCALHOST_IP
if enable_ui:
# Listen on all interfaces for allowing access from all ips
self._api_ip = ""
self._watchdog_task: asyncio.Task | None = None
self._watchdog_tasks: list[asyncio.Task] = []
async def start(self) -> None:
"""Start the server."""
await self._start()
self._watchdog_task = asyncio.create_task(
self._watchdog(), name="Go2rtc respawn"
)
async def _start(self) -> None:
"""Start the server."""
_LOGGER.debug("Starting go2rtc server")
config_file = await self._hass.async_add_executor_job(
@@ -82,8 +128,13 @@ class Server:
except TimeoutError as err:
msg = "Go2rtc server didn't start correctly"
_LOGGER.exception(msg)
await self.stop()
raise HomeAssistantError("Go2rtc server didn't start correctly") from err
self._log_server_output(logging.WARNING)
await self._stop()
raise Go2RTCServerStartError from err
# Check the server version
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
await client.validate_server_version()
async def _log_output(self, process: asyncio.subprocess.Process) -> None:
"""Log the output of the process."""
@@ -91,21 +142,111 @@ class Server:
async for line in process.stdout:
msg = line[:-1].decode().strip()
_LOGGER.debug(msg)
self._log_buffer.append(msg)
loglevel = logging.WARNING
if len(split_msg := msg.split(" ", 2)) == 3:
loglevel = _LOG_LEVEL_MAP.get(split_msg[1], loglevel)
_LOGGER.log(loglevel, msg)
if not self._startup_complete.is_set() and _SUCCESSFUL_BOOT_MESSAGE in msg:
self._startup_complete.set()
def _log_server_output(self, loglevel: int) -> None:
"""Log captured process output, then clear the log buffer."""
for line in list(self._log_buffer): # Copy the deque to avoid mutation error
_LOGGER.log(loglevel, line)
self._log_buffer.clear()
async def _watchdog(self) -> None:
"""Keep respawning go2rtc servers.
A new go2rtc server is spawned if the process terminates or the API
stops responding.
"""
while True:
try:
monitor_process_task = asyncio.create_task(self._monitor_process())
self._watchdog_tasks.append(monitor_process_task)
monitor_process_task.add_done_callback(self._watchdog_tasks.remove)
monitor_api_task = asyncio.create_task(self._monitor_api())
self._watchdog_tasks.append(monitor_api_task)
monitor_api_task.add_done_callback(self._watchdog_tasks.remove)
try:
await asyncio.gather(monitor_process_task, monitor_api_task)
except Go2RTCWatchdogError:
_LOGGER.debug("Caught Go2RTCWatchdogError")
for task in self._watchdog_tasks:
if task.done():
if not task.cancelled():
task.exception()
continue
task.cancel()
await asyncio.sleep(_RESPAWN_COOLDOWN)
try:
await self._stop()
_LOGGER.warning("Go2rtc unexpectedly stopped, server log:")
self._log_server_output(logging.WARNING)
_LOGGER.debug("Spawning new go2rtc server")
with suppress(Go2RTCServerStartError):
await self._start()
except Exception:
_LOGGER.exception(
"Unexpected error when restarting go2rtc server"
)
except Exception:
_LOGGER.exception("Unexpected error in go2rtc server watchdog")
async def _monitor_process(self) -> None:
"""Raise if the go2rtc process terminates."""
_LOGGER.debug("Monitoring go2rtc server process")
if self._process:
await self._process.wait()
_LOGGER.debug("go2rtc server terminated")
raise Go2RTCWatchdogError("Process ended")
async def _monitor_api(self) -> None:
"""Raise if the go2rtc process terminates."""
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
_LOGGER.debug("Monitoring go2rtc API")
try:
while True:
await client.validate_server_version()
await asyncio.sleep(10)
except Exception as err:
_LOGGER.debug("go2rtc API did not reply", exc_info=True)
raise Go2RTCWatchdogError("API error") from err
async def _stop_watchdog(self) -> None:
"""Handle watchdog stop request."""
tasks: list[asyncio.Task] = []
if watchdog_task := self._watchdog_task:
self._watchdog_task = None
tasks.append(watchdog_task)
watchdog_task.cancel()
for task in self._watchdog_tasks:
tasks.append(task)
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
async def stop(self) -> None:
"""Stop the server and abort the watchdog task."""
_LOGGER.debug("Server stop requested")
await self._stop_watchdog()
await self._stop()
async def _stop(self) -> None:
"""Stop the server."""
if self._process:
_LOGGER.debug("Stopping go2rtc server")
process = self._process
self._process = None
process.terminate()
with suppress(ProcessLookupError):
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=_TERMINATE_TIMEOUT)
except TimeoutError:
_LOGGER.warning("Go2rtc server didn't terminate gracefully. Killing it")
process.kill()
with suppress(ProcessLookupError):
process.kill()
else:
_LOGGER.debug("Go2rtc server has been stopped")
@@ -0,0 +1,8 @@
{
"issues": {
"recommended_version": {
"title": "Outdated go2rtc server detected",
"description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`."
}
}
}
+2 -2
View File
@@ -87,8 +87,8 @@
}
},
"create_event": {
"name": "Creates event",
"description": "Add a new calendar event.",
"name": "Create event",
"description": "Adds a new calendar event.",
"fields": {
"summary": {
"name": "Summary",
@@ -59,9 +59,9 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
tasks_response.extend(await self.api.tasks.user.get(type="completedTodos"))
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.debug("Currently rate limited, skipping update")
_LOGGER.debug("Rate limit exceeded, will try again later")
return self.data
raise UpdateFailed(f"Error communicating with API: {error}") from error
raise UpdateFailed(f"Unable to connect to Habitica: {error}") from error
return HabiticaData(user=user_response, tasks=tasks_response)
@@ -204,10 +204,10 @@
"message": "Unable to create new to-do `{name}` for Habitica, please try again"
},
"setup_rate_limit_exception": {
"message": "Currently rate limited, try again later"
"message": "Rate limit exceeded, try again later"
},
"service_call_unallowed": {
"message": "Unable to carry out this action, because the required conditions are not met"
"message": "Unable to complete action, the required conditions are not met"
},
"service_call_exception": {
"message": "Unable to connect to Habitica, try again later"
+1
View File
@@ -103,6 +103,7 @@ PLACEHOLDER_KEY_ADDON_URL = "addon_url"
PLACEHOLDER_KEY_REFERENCE = "reference"
PLACEHOLDER_KEY_COMPONENTS = "components"
ISSUE_KEY_ADDON_BOOT_FAIL = "issue_addon_boot_fail"
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
+2 -2
View File
@@ -131,11 +131,11 @@ class HassIODiscovery(HomeAssistantView):
config=data.config,
name=addon_info.name,
slug=data.addon,
uuid=str(data.uuid),
uuid=data.uuid.hex,
),
discovery_key=discovery_flow.DiscoveryKey(
domain=DOMAIN,
key=str(data.uuid),
key=data.uuid.hex,
version=1,
),
)
@@ -36,6 +36,7 @@ from .const import (
EVENT_SUPERVISOR_EVENT,
EVENT_SUPERVISOR_UPDATE,
EVENT_SUPPORTED_CHANGED,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
@@ -94,6 +95,7 @@ UNHEALTHY_REASONS = {
# Keys (type + context) of issues that when found should be made into a repair
ISSUE_KEYS_FOR_REPAIRS = {
ISSUE_KEY_ADDON_BOOT_FAIL,
"issue_mount_mount_failed",
"issue_system_multiple_data_disks",
"issue_system_reboot_required",
+8 -4
View File
@@ -14,6 +14,7 @@ from homeassistant.data_entry_flow import FlowResult
from . import get_addons_info, get_issues_info
from .const import (
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
PLACEHOLDER_KEY_ADDON,
@@ -181,8 +182,8 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
return placeholders
class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
"""Handler for detached addon issue fixing flows."""
class AddonIssueRepairFlow(SupervisorIssueRepairFlow):
"""Handler for addon issue fixing flows."""
@property
def description_placeholders(self) -> dict[str, str] | None:
@@ -210,7 +211,10 @@ async def async_create_fix_flow(
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
return DockerConfigIssueRepairFlow(issue_id)
if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
return DetachedAddonIssueRepairFlow(issue_id)
if issue and issue.key in {
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_BOOT_FAIL,
}:
return AddonIssueRepairFlow(issue_id)
return SupervisorIssueRepairFlow(issue_id)
+23 -6
View File
@@ -17,6 +17,23 @@
}
},
"issues": {
"issue_addon_boot_fail": {
"title": "Add-on failed to start at boot",
"fix_flow": {
"step": {
"fix_menu": {
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
"menu_options": {
"addon_execute_start": "Start",
"addon_disable_boot": "Disable"
}
}
},
"abort": {
"apply_suggestion_fail": "Could not apply the fix. Check the Supervisor logs for more details."
}
}
},
"issue_addon_detached_addon_missing": {
"title": "Missing repository for an installed add-on",
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
@@ -257,7 +274,7 @@
"fields": {
"addon": {
"name": "Add-on",
"description": "The add-on slug."
"description": "The add-on to start."
}
}
},
@@ -267,17 +284,17 @@
"fields": {
"addon": {
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
"description": "[%key:component::hassio::services::addon_start::fields::addon::description%]"
"description": "The add-on to restart."
}
}
},
"addon_stdin": {
"name": "Write data to add-on stdin.",
"description": "Writes data to add-on stdin.",
"description": "Writes data to the add-on's standard input.",
"fields": {
"addon": {
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
"description": "[%key:component::hassio::services::addon_start::fields::addon::description%]"
"description": "The add-on to write to."
}
}
},
@@ -287,7 +304,7 @@
"fields": {
"addon": {
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
"description": "[%key:component::hassio::services::addon_start::fields::addon::description%]"
"description": "The add-on to stop."
}
}
},
@@ -297,7 +314,7 @@
"fields": {
"addon": {
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
"description": "[%key:component::hassio::services::addon_start::fields::addon::description%]"
"description": "The add-on to update."
}
}
},
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.59", "babel==2.15.0"]
"requirements": ["holidays==0.61", "babel==2.15.0"]
}
@@ -13,7 +13,11 @@ from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .api import HomeConnectDevice
from .const import (
@@ -206,3 +210,9 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
"items": "\n".join([f"- {item}" for item in items]),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
)
@@ -37,11 +37,8 @@
"set_light_color": {
"message": "Error while trying to set color of {entity_id}: {description}"
},
"set_light_effect": {
"message": "Error while trying to set effect of {entity_id}: {description}"
},
"set_setting": {
"message": "Error while trying to set \"{value}\" to \"{key}\" setting for {entity_id}: {description}"
"message": "Error while trying to assign the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
},
"turn_on": {
"message": "Error while trying to turn on {entity_id} ({key}): {description}"
@@ -18,6 +18,8 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_DISARM,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import State, callback
@@ -152,12 +154,12 @@ class SecuritySystem(HomeAccessory):
@callback
def async_update_state(self, new_state: State) -> None:
"""Update security state after state changed."""
hass_state = None
if new_state and new_state.state == "None":
# Bail out early for no state
hass_state: str | AlarmControlPanelState = new_state.state
if hass_state in {"None", STATE_UNKNOWN, STATE_UNAVAILABLE}:
# Bail out early for no state, unknown or unavailable
return
if new_state and new_state.state is not None:
hass_state = AlarmControlPanelState(new_state.state)
if hass_state is not None:
hass_state = AlarmControlPanelState(hass_state)
if (
hass_state
and (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None
@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"quality_scale": "silver",
"requirements": ["homematicip==1.1.2"]
"requirements": ["homematicip==1.1.3"]
}
@@ -1,5 +1,8 @@
{
"config": {
"abort": {
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"connection_error": "Could not connect to the controller.",
"credentials_needed": "The controller needs credentials.",
+7 -2
View File
@@ -130,10 +130,15 @@ class HueSceneEntity(HueSceneEntityBase):
@property
def is_dynamic(self) -> bool:
"""Return if this scene has a dynamic color palette."""
if self.resource.palette.color and len(self.resource.palette.color) > 1:
if (
self.resource.palette
and self.resource.palette.color
and len(self.resource.palette.color) > 1
):
return True
if (
self.resource.palette.color_temperature
self.resource.palette
and self.resource.palette.color_temperature
and len(self.resource.palette.color_temperature) > 1
):
return True
@@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber):
self.entity_description = description
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
def set_native_value(self, value: float) -> None:
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
self._attr_native_value = value
self.entity_description.store_value_fn(self.coordinator, self._shade.id, value)
@@ -8,6 +8,7 @@ from aioautomower.exceptions import (
ApiException,
AuthException,
HusqvarnaWSServerHandshakeError,
TimeoutException,
)
from aioautomower.model import MowerAttributes
from aioautomower.session import AutomowerSession
@@ -22,6 +23,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
SCAN_INTERVAL = timedelta(minutes=8)
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
@@ -40,8 +42,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
update_interval=SCAN_INTERVAL,
)
self.api = api
self.ws_connected: bool = False
self.reconnect_time = DEFAULT_RECONNECT_TIME
async def _async_update_data(self) -> dict[str, MowerAttributes]:
"""Subscribe for websocket and poll data from the API."""
@@ -66,24 +68,28 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
hass: HomeAssistant,
entry: ConfigEntry,
automower_client: AutomowerSession,
reconnect_time: int = 2,
) -> None:
"""Listen with the client."""
try:
await automower_client.auth.websocket_connect()
reconnect_time = 2
# Reset reconnect time after successful connection
self.reconnect_time = DEFAULT_RECONNECT_TIME
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s", err
"Failed to connect to websocket. Trying to reconnect: %s",
err,
)
except TimeoutException as err:
_LOGGER.debug(
"Failed to listen to websocket. Trying to reconnect: %s",
err,
)
if not hass.is_stopping:
await asyncio.sleep(reconnect_time)
reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
await self.client_listen(
hass=hass,
entry=entry,
automower_client=automower_client,
reconnect_time=reconnect_time,
await asyncio.sleep(self.reconnect_time)
self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME)
entry.async_create_background_task(
hass,
self.client_listen(hass, entry, automower_client),
"reconnect_task",
)
@@ -32,6 +32,10 @@
}
},
"options": {
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"step": {
"init": {
"title": "Options",
+18 -5
View File
@@ -7,8 +7,12 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN, SCAN_INTERVAL
from .coordinator import HydrawiseDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import (
HydrawiseMainDataUpdateCoordinator,
HydrawiseUpdateCoordinators,
HydrawiseWaterUseDataUpdateCoordinator,
)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
@@ -29,9 +33,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD])
)
coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
main_coordinator = HydrawiseMainDataUpdateCoordinator(hass, hydrawise)
await main_coordinator.async_config_entry_first_refresh()
water_use_coordinator = HydrawiseWaterUseDataUpdateCoordinator(
hass, hydrawise, main_coordinator
)
await water_use_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = (
HydrawiseUpdateCoordinators(
main=main_coordinator,
water_use=water_use_coordinator,
)
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import VolDictType
from .const import DOMAIN, SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND
from .coordinator import HydrawiseDataUpdateCoordinator
from .coordinator import HydrawiseUpdateCoordinators
from .entity import HydrawiseEntity
@@ -81,18 +81,16 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Hydrawise binary_sensor platform."""
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id]
entities: list[HydrawiseBinarySensor] = []
for controller in coordinator.data.controllers.values():
for controller in coordinators.main.data.controllers.values():
entities.extend(
HydrawiseBinarySensor(coordinator, description, controller)
HydrawiseBinarySensor(coordinators.main, description, controller)
for description in CONTROLLER_BINARY_SENSORS
)
entities.extend(
HydrawiseBinarySensor(
coordinator,
coordinators.main,
description,
controller,
sensor_id=sensor.id,
@@ -103,7 +101,7 @@ async def async_setup_entry(
)
entities.extend(
HydrawiseZoneBinarySensor(
coordinator, description, controller, zone_id=zone.id
coordinators.main, description, controller, zone_id=zone.id
)
for zone in controller.zones
for description in ZONE_BINARY_SENSORS
+2 -1
View File
@@ -10,7 +10,8 @@ DEFAULT_WATERING_TIME = timedelta(minutes=15)
MANUFACTURER = "Hydrawise"
SCAN_INTERVAL = timedelta(seconds=60)
MAIN_SCAN_INTERVAL = timedelta(seconds=60)
WATER_USE_SCAN_INTERVAL = timedelta(minutes=60)
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
@@ -2,8 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from dataclasses import dataclass, field
from pydrawise import Hydrawise
from pydrawise.schema import Controller, ControllerWaterUseSummary, Sensor, User, Zone
@@ -12,7 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.dt import now
from .const import DOMAIN, LOGGER
from .const import DOMAIN, LOGGER, MAIN_SCAN_INTERVAL, WATER_USE_SCAN_INTERVAL
@dataclass
@@ -20,22 +19,39 @@ class HydrawiseData:
"""Container for data fetched from the Hydrawise API."""
user: User
controllers: dict[int, Controller]
zones: dict[int, Zone]
sensors: dict[int, Sensor]
daily_water_summary: dict[int, ControllerWaterUseSummary]
controllers: dict[int, Controller] = field(default_factory=dict)
zones: dict[int, Zone] = field(default_factory=dict)
sensors: dict[int, Sensor] = field(default_factory=dict)
daily_water_summary: dict[int, ControllerWaterUseSummary] = field(
default_factory=dict
)
@dataclass
class HydrawiseUpdateCoordinators:
"""Container for all Hydrawise DataUpdateCoordinator instances."""
main: HydrawiseMainDataUpdateCoordinator
water_use: HydrawiseWaterUseDataUpdateCoordinator
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
"""The Hydrawise Data Update Coordinator."""
"""Base class for Hydrawise Data Update Coordinators."""
api: Hydrawise
def __init__(
self, hass: HomeAssistant, api: Hydrawise, scan_interval: timedelta
) -> None:
class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator):
"""The main Hydrawise Data Update Coordinator.
This fetches the primary state data for Hydrawise controllers and zones
at a relatively frequent interval so that the primary functions of the
integration are updated in a timely manner.
"""
def __init__(self, hass: HomeAssistant, api: Hydrawise) -> None:
"""Initialize HydrawiseDataUpdateCoordinator."""
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval)
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=MAIN_SCAN_INTERVAL)
self.api = api
async def _async_update_data(self) -> HydrawiseData:
@@ -43,28 +59,56 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
# Don't fetch zones. We'll fetch them for each controller later.
# This is to prevent 502 errors in some cases.
# See: https://github.com/home-assistant/core/issues/120128
user = await self.api.get_user(fetch_zones=False)
controllers = {}
zones = {}
sensors = {}
daily_water_summary: dict[int, ControllerWaterUseSummary] = {}
for controller in user.controllers:
controllers[controller.id] = controller
data = HydrawiseData(user=await self.api.get_user(fetch_zones=False))
for controller in data.user.controllers:
data.controllers[controller.id] = controller
controller.zones = await self.api.get_zones(controller)
for zone in controller.zones:
zones[zone.id] = zone
data.zones[zone.id] = zone
for sensor in controller.sensors:
sensors[sensor.id] = sensor
data.sensors[sensor.id] = sensor
return data
class HydrawiseWaterUseDataUpdateCoordinator(HydrawiseDataUpdateCoordinator):
"""Data Update Coordinator for Hydrawise Water Use.
This fetches data that is more expensive for the Hydrawise API to compute
at a less frequent interval as to not overload the Hydrawise servers.
"""
_main_coordinator: HydrawiseMainDataUpdateCoordinator
def __init__(
self,
hass: HomeAssistant,
api: Hydrawise,
main_coordinator: HydrawiseMainDataUpdateCoordinator,
) -> None:
"""Initialize HydrawiseWaterUseDataUpdateCoordinator."""
super().__init__(
hass,
LOGGER,
name=f"{DOMAIN} water use",
update_interval=WATER_USE_SCAN_INTERVAL,
)
self.api = api
self._main_coordinator = main_coordinator
async def _async_update_data(self) -> HydrawiseData:
"""Fetch the latest data from Hydrawise."""
daily_water_summary: dict[int, ControllerWaterUseSummary] = {}
for controller in self._main_coordinator.data.controllers.values():
daily_water_summary[controller.id] = await self.api.get_water_use_summary(
controller,
now().replace(hour=0, minute=0, second=0, microsecond=0),
now(),
)
main_data = self._main_coordinator.data
return HydrawiseData(
user=user,
controllers=controllers,
zones=zones,
sensors=sensors,
user=main_data.user,
controllers=main_data.controllers,
zones=main_data.zones,
sensors=main_data.sensors,
daily_water_summary=daily_water_summary,
)
+30 -19
View File
@@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator
from .coordinator import HydrawiseUpdateCoordinators
from .entity import HydrawiseEntity
@@ -92,7 +92,7 @@ def _get_controller_daily_total_water_use(sensor: HydrawiseSensor) -> float | No
return daily_water_summary.total_use
CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
WATER_USE_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
HydrawiseSensorEntityDescription(
key="daily_active_water_time",
translation_key="daily_active_water_time",
@@ -103,6 +103,16 @@ CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
)
WATER_USE_ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
HydrawiseSensorEntityDescription(
key="daily_active_water_time",
translation_key="daily_active_water_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
value_fn=_get_zone_daily_active_water_time,
),
)
FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
HydrawiseSensorEntityDescription(
key="daily_total_water_use",
@@ -150,13 +160,6 @@ ZONE_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfTime.MINUTES,
value_fn=_get_zone_watering_time,
),
HydrawiseSensorEntityDescription(
key="daily_active_water_time",
translation_key="daily_active_water_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
value_fn=_get_zone_daily_active_water_time,
),
)
FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS]
@@ -168,29 +171,37 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Hydrawise sensor platform."""
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id]
entities: list[HydrawiseSensor] = []
for controller in coordinator.data.controllers.values():
for controller in coordinators.main.data.controllers.values():
entities.extend(
HydrawiseSensor(coordinator, description, controller)
for description in CONTROLLER_SENSORS
HydrawiseSensor(coordinators.water_use, description, controller)
for description in WATER_USE_CONTROLLER_SENSORS
)
entities.extend(
HydrawiseSensor(coordinator, description, controller, zone_id=zone.id)
HydrawiseSensor(
coordinators.water_use, description, controller, zone_id=zone.id
)
for zone in controller.zones
for description in WATER_USE_ZONE_SENSORS
)
entities.extend(
HydrawiseSensor(coordinators.main, description, controller, zone_id=zone.id)
for zone in controller.zones
for description in ZONE_SENSORS
)
if coordinator.data.daily_water_summary[controller.id].total_use is not None:
if (
coordinators.water_use.data.daily_water_summary[controller.id].total_use
is not None
):
# we have a flow sensor for this controller
entities.extend(
HydrawiseSensor(coordinator, description, controller)
HydrawiseSensor(coordinators.water_use, description, controller)
for description in FLOW_CONTROLLER_SENSORS
)
entities.extend(
HydrawiseSensor(
coordinator,
coordinators.water_use,
description,
controller,
zone_id=zone.id,
@@ -13,7 +13,8 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
+4 -6
View File
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DEFAULT_WATERING_TIME, DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator
from .coordinator import HydrawiseUpdateCoordinators
from .entity import HydrawiseEntity
@@ -66,12 +66,10 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Hydrawise switch platform."""
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
HydrawiseSwitch(coordinator, description, controller, zone_id=zone.id)
for controller in coordinator.data.controllers.values()
HydrawiseSwitch(coordinators.main, description, controller, zone_id=zone.id)
for controller in coordinators.main.data.controllers.values()
for zone in controller.zones
for description in SWITCH_TYPES
)
+4 -6
View File
@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator
from .coordinator import HydrawiseUpdateCoordinators
from .entity import HydrawiseEntity
VALVE_TYPES: tuple[ValveEntityDescription, ...] = (
@@ -34,12 +34,10 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Hydrawise valve platform."""
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
HydrawiseValve(coordinator, description, controller, zone_id=zone.id)
for controller in coordinator.data.controllers.values()
HydrawiseValve(coordinators.main, description, controller, zone_id=zone.id)
for controller in coordinators.main.data.controllers.values()
for zone in controller.zones
for description in VALVE_TYPES
)
+1 -1
View File
@@ -104,7 +104,7 @@
"services": {
"fetch": {
"name": "Fetch message",
"description": "Fetch the email message from the server.",
"description": "Fetch an email message from the server.",
"fields": {
"entry": {
"name": "Entry",
@@ -120,12 +120,22 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN):
assert self._discovery_info is not None
service_data = self._discovery_info.service_data
improv_service_data = ImprovServiceData.from_bytes(
service_data[SERVICE_DATA_UUID]
)
try:
improv_service_data = ImprovServiceData.from_bytes(
service_data[SERVICE_DATA_UUID]
)
except improv_ble_errors.InvalidCommand as err:
_LOGGER.warning(
"Aborting improv flow, device %s sent invalid improv data: '%s'",
self._discovery_info.address,
service_data[SERVICE_DATA_UUID].hex(),
)
raise AbortFlow("invalid_improv_data") from err
if improv_service_data.state in (State.PROVISIONING, State.PROVISIONED):
_LOGGER.debug(
"Aborting improv flow, device is already provisioned: %s",
"Aborting improv flow, device %s is already provisioned: %s",
self._discovery_info.address,
improv_service_data.state,
)
raise AbortFlow("already_provisioned")
@@ -112,7 +112,7 @@
"services": {
"add_all_link": {
"name": "Add all link",
"description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"description": "Tells the Insteon Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.",
"fields": {
"group": {
"name": "Group",
+13 -1
View File
@@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
intent.async_register(hass, TimerStatusIntentHandler())
intent.async_register(hass, GetCurrentDateIntentHandler())
intent.async_register(hass, GetCurrentTimeIntentHandler())
intent.async_register(hass, HelloIntentHandler())
return True
@@ -364,7 +365,7 @@ class NevermindIntentHandler(intent.IntentHandler):
description = "Cancels the current request and does nothing"
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Doe not do anything, and produces an empty response."""
"""Do nothing and produces an empty response."""
return intent_obj.create_response()
@@ -420,6 +421,17 @@ class GetCurrentTimeIntentHandler(intent.IntentHandler):
return response
class HelloIntentHandler(intent.IntentHandler):
"""Responds with no action."""
intent_type = intent.INTENT_RESPOND
description = "Returns the provided response with no action."
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Return the provided response, but take no action."""
return intent_obj.create_response()
async def _async_process_intent(
hass: HomeAssistant, domain: str, platform: IntentPlatformProtocol
) -> None:
+4 -1
View File
@@ -92,4 +92,7 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.firmware_update.last_update_success
return (
self.installed_version is not None
and self.firmware_update.last_update_success
)
@@ -12,6 +12,9 @@
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
+11 -1
View File
@@ -15,7 +15,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -115,6 +119,9 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
await self.device_connection.cancel_status_request_handler(
self.setpoint_variable
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_sensor_{self.entity_id}"
)
def input_received(self, input_obj: InputType) -> None:
"""Set sensor value when LCN input object (command) is received."""
@@ -201,6 +208,9 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity):
await super().async_will_remove_from_hass()
if not self.device_connection.is_group:
await self.device_connection.cancel_status_request_handler(self.source)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_sensor_{self.entity_id}"
)
def input_received(self, input_obj: InputType) -> None:
"""Set sensor value when LCN input object (command) is received."""
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.24", "lcn-frontend==0.2.0"]
"requirements": ["pypck==0.7.24", "lcn-frontend==0.2.1"]
}
@@ -25,7 +25,8 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]"
}
},
"device_automation": {
+7 -4
View File
@@ -72,8 +72,11 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
super().__init__(coordinator, entity_description, property_id)
self._ordered_named_fan_speeds = []
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_ON
| FanEntityFeature.TURN_OFF
)
if (fan_modes := self.data.fan_modes) is not None:
self._attr_speed_count = len(fan_modes)
if self.speed_count == 4:
@@ -98,7 +101,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
self._attr_percentage = 0
_LOGGER.debug(
"[%s:%s] update status: %s -> %s (percntage=%s)",
"[%s:%s] update status: %s -> %s (percentage=%s)",
self.coordinator.device_name,
self.property_id,
self.data.is_on,
@@ -120,7 +123,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
return
_LOGGER.debug(
"[%s:%s] async_set_percentage. percntage=%s, value=%s",
"[%s:%s] async_set_percentage. percentage=%s, value=%s",
self.coordinator.device_name,
self.property_id,
percentage,
+1 -86
View File
@@ -255,73 +255,9 @@ WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
translation_key=ThinQProperty.WATER_TYPE,
),
}
TIMER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
TimerProperty.RELATIVE_TO_START: SensorEntityDescription(
key=TimerProperty.RELATIVE_TO_START,
translation_key=TimerProperty.RELATIVE_TO_START,
),
TimerProperty.RELATIVE_TO_START_WM: SensorEntityDescription(
key=TimerProperty.RELATIVE_TO_START,
translation_key=TimerProperty.RELATIVE_TO_START_WM,
),
TimerProperty.RELATIVE_TO_STOP: SensorEntityDescription(
key=TimerProperty.RELATIVE_TO_STOP,
translation_key=TimerProperty.RELATIVE_TO_STOP,
),
TimerProperty.RELATIVE_TO_STOP_WM: SensorEntityDescription(
key=TimerProperty.RELATIVE_TO_STOP,
translation_key=TimerProperty.RELATIVE_TO_STOP_WM,
),
TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP: SensorEntityDescription(
key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP,
translation_key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP,
),
TimerProperty.ABSOLUTE_TO_START: SensorEntityDescription(
key=TimerProperty.ABSOLUTE_TO_START,
translation_key=TimerProperty.ABSOLUTE_TO_START,
),
TimerProperty.ABSOLUTE_TO_STOP: SensorEntityDescription(
key=TimerProperty.ABSOLUTE_TO_STOP,
translation_key=TimerProperty.ABSOLUTE_TO_STOP,
),
TimerProperty.REMAIN: SensorEntityDescription(
key=TimerProperty.REMAIN,
translation_key=TimerProperty.REMAIN,
),
TimerProperty.TARGET: SensorEntityDescription(
key=TimerProperty.TARGET,
translation_key=TimerProperty.TARGET,
),
TimerProperty.RUNNING: SensorEntityDescription(
key=TimerProperty.RUNNING,
translation_key=TimerProperty.RUNNING,
),
TimerProperty.TOTAL: SensorEntityDescription(
key=TimerProperty.TOTAL,
translation_key=TimerProperty.TOTAL,
),
TimerProperty.LIGHT_START: SensorEntityDescription(
key=TimerProperty.LIGHT_START,
translation_key=TimerProperty.LIGHT_START,
),
ThinQProperty.ELAPSED_DAY_STATE: SensorEntityDescription(
key=ThinQProperty.ELAPSED_DAY_STATE,
native_unit_of_measurement=UnitOfTime.DAYS,
translation_key=ThinQProperty.ELAPSED_DAY_STATE,
),
ThinQProperty.ELAPSED_DAY_TOTAL: SensorEntityDescription(
key=ThinQProperty.ELAPSED_DAY_TOTAL,
native_unit_of_measurement=UnitOfTime.DAYS,
translation_key=ThinQProperty.ELAPSED_DAY_TOTAL,
),
}
WASHER_SENSORS: tuple[SensorEntityDescription, ...] = (
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM],
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP_WM],
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
TIMER_SENSOR_DESC[TimerProperty.TOTAL],
)
DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = {
DeviceType.AIR_CONDITIONER: (
@@ -332,9 +268,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_LIFETIME],
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START],
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP],
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
),
DeviceType.AIR_PURIFIER_FAN: (
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
@@ -345,7 +278,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
),
DeviceType.AIR_PURIFIER: (
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
@@ -361,7 +293,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
DeviceType.COOKTOP: (
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
POWER_SENSOR_DESC[ThinQProperty.POWER_LEVEL],
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
),
DeviceType.DEHUMIDIFIER: (
JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
@@ -372,9 +303,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
PREFERENCE_SENSOR_DESC[ThinQProperty.RINSE_LEVEL],
PREFERENCE_SENSOR_DESC[ThinQProperty.SOFTENING_LEVEL],
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM],
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
TIMER_SENSOR_DESC[TimerProperty.TOTAL],
),
DeviceType.DRYER: WASHER_SENSORS,
DeviceType.HOME_BREW: (
@@ -385,10 +313,7 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_INFO],
RECIPE_SENSOR_DESC[ThinQProperty.BEER_REMAIN],
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_STATE],
TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_TOTAL],
),
DeviceType.HOOD: (TIMER_SENSOR_DESC[TimerProperty.REMAIN],),
DeviceType.HUMIDIFIER: (
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
@@ -397,9 +322,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START],
TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP],
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
),
DeviceType.KIMCHI_REFRIGERATOR: (
REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
@@ -408,15 +330,10 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
translation_key=ThinQProperty.TARGET_TEMPERATURE,
),
),
DeviceType.MICROWAVE_OVEN: (
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
),
DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],),
DeviceType.OVEN: (
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE],
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
TIMER_SENSOR_DESC[TimerProperty.TARGET],
),
DeviceType.PLANT_CULTIVATOR: (
LIGHT_SENSOR_DESC[ThinQProperty.BRIGHTNESS],
@@ -427,7 +344,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE],
TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE],
TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE],
TIMER_SENSOR_DESC[TimerProperty.LIGHT_START],
),
DeviceType.REFRIGERATOR: (
REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
@@ -436,7 +352,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
DeviceType.ROBOT_CLEANER: (
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
TIMER_SENSOR_DESC[TimerProperty.RUNNING],
),
DeviceType.STICK_CLEANER: (
BATTERY_SENSOR_DESC[ThinQProperty.BATTERY_PERCENT],
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.0.17"],
"requirements": ["python-linkplay==0.0.20"],
"zeroconf": ["_linkplay._tcp.local."]
}
@@ -9,7 +9,7 @@ from typing import Any, Concatenate
from linkplay.bridge import LinkPlayBridge
from linkplay.consts import EqualizerMode, LoopMode, PlayingMode, PlayingStatus
from linkplay.controller import LinkPlayController, LinkPlayMultiroom
from linkplay.exceptions import LinkPlayException, LinkPlayRequestException
from linkplay.exceptions import LinkPlayRequestException
import voluptuous as vol
from homeassistant.components import media_source
@@ -69,6 +69,8 @@ SOURCE_MAP: dict[PlayingMode, str] = {
PlayingMode.FM: "FM Radio",
PlayingMode.RCA: "RCA",
PlayingMode.UDISK: "USB",
PlayingMode.SPOTIFY: "Spotify",
PlayingMode.TIDAL: "Tidal",
PlayingMode.FOLLOWER: "Follower",
}
@@ -201,9 +203,8 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
try:
await self._bridge.player.update_status()
self._update_properties()
except LinkPlayException:
except LinkPlayRequestException:
self._attr_available = False
raise
@exception_wrap
async def async_select_source(self, source: str) -> None:
@@ -292,7 +293,15 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
@exception_wrap
async def async_play_preset(self, preset_number: int) -> None:
"""Play preset number."""
await self._bridge.player.play_preset(preset_number)
try:
await self._bridge.player.play_preset(preset_number)
except ValueError as err:
raise HomeAssistantError(err) from err
@exception_wrap
async def async_media_seek(self, position: float) -> None:
"""Seek to a position."""
await self._bridge.player.seek(round(position))
@exception_wrap
async def async_join_players(self, group_members: list[str]) -> None:
@@ -379,9 +388,9 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
)
self._attr_source = SOURCE_MAP.get(self._bridge.player.play_mode, "other")
self._attr_media_position = self._bridge.player.current_position / 1000
self._attr_media_position = self._bridge.player.current_position_in_seconds
self._attr_media_position_updated_at = utcnow()
self._attr_media_duration = self._bridge.player.total_length / 1000
self._attr_media_duration = self._bridge.player.total_length_in_seconds
self._attr_media_artist = self._bridge.player.artist
self._attr_media_title = self._bridge.player.title
self._attr_media_album_name = self._bridge.player.album
@@ -11,5 +11,4 @@ play_preset:
selector:
number:
min: 1
max: 10
mode: box
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/lutron",
"iot_class": "local_polling",
"loggers": ["pylutron"],
"requirements": ["pylutron==0.2.15"],
"requirements": ["pylutron==0.2.16"],
"single_config_entry": true
}
+3 -3
View File
@@ -28,12 +28,12 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"set_up_new_device": "A new device was detected. Please set it up as a new entity instead of reconfiguring."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_mac": "A MAC address was not found. It required to identify the device. Please ensure your device is connectable.",
"set_up_new_device": "A new device was detected. Please set it up as a new entity instead of reconfiguring."
"no_mac": "A MAC address was not found. It required to identify the device. Please ensure your device is connectable."
}
},
"entity": {
@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp==2024.10.22"],
"requirements": ["yt-dlp[default]==2024.11.04"],
"single_config_entry": true
}
+14 -11
View File
@@ -33,7 +33,7 @@ from homeassistant.const import (
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio
@@ -737,6 +737,16 @@ class MQTTOptionsFlowHandler(OptionsFlow):
)
async def _get_uploaded_file(hass: HomeAssistant, id: str) -> str:
"""Get file content from uploaded file."""
def _proces_uploaded_file() -> str:
with process_uploaded_file(hass, id) as file_path:
return file_path.read_text(encoding=DEFAULT_ENCODING)
return await hass.async_add_executor_job(_proces_uploaded_file)
async def async_get_broker_settings(
flow: ConfigFlow | OptionsFlow,
fields: OrderedDict[Any, Any],
@@ -795,8 +805,7 @@ async def async_get_broker_settings(
return False
certificate_id: str | None = user_input.get(CONF_CERTIFICATE)
if certificate_id:
with process_uploaded_file(hass, certificate_id) as certificate_file:
certificate = certificate_file.read_text(encoding=DEFAULT_ENCODING)
certificate = await _get_uploaded_file(hass, certificate_id)
# Return to form for file upload CA cert or client cert and key
if (
@@ -812,15 +821,9 @@ async def async_get_broker_settings(
return False
if client_certificate_id:
with process_uploaded_file(
hass, client_certificate_id
) as client_certificate_file:
client_certificate = client_certificate_file.read_text(
encoding=DEFAULT_ENCODING
)
client_certificate = await _get_uploaded_file(hass, client_certificate_id)
if client_key_id:
with process_uploaded_file(hass, client_key_id) as key_file:
client_key = key_file.read_text(encoding=DEFAULT_ENCODING)
client_key = await _get_uploaded_file(hass, client_key_id)
certificate_data: dict[str, Any] = {}
if certificate:
@@ -12,11 +12,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MyUplinkConfigEntry, MyUplinkDataCoordinator
from .const import F_SERIES
from .entity import MyUplinkEntity, MyUplinkSystemEntity
from .helpers import find_matching_platform
from .helpers import find_matching_platform, transform_model_series
CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, BinarySensorEntityDescription]] = {
"F730": {
F_SERIES: {
"43161": BinarySensorEntityDescription(
key="elect_add",
translation_key="elect_add",
@@ -50,6 +51,7 @@ def get_description(device_point: DevicePoint) -> BinarySensorEntityDescription
2. Default to None
"""
prefix, _, _ = device_point.category.partition(" ")
prefix = transform_model_series(prefix)
return CATEGORY_BASED_DESCRIPTIONS.get(prefix, {}).get(device_point.parameter_id)
@@ -6,3 +6,5 @@ API_ENDPOINT = "https://api.myuplink.com"
OAUTH2_AUTHORIZE = "https://api.myuplink.com/oauth/authorize"
OAUTH2_TOKEN = "https://api.myuplink.com/oauth/token"
OAUTH2_SCOPES = ["WRITESYSTEM", "READSYSTEM", "offline_access"]
F_SERIES = "f-series"

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