Compare commits

...

223 Commits

Author SHA1 Message Date
abmantis
bea72a49d7 Add device_tracker home enter/leave triggers 2025-12-06 00:30:33 +00:00
Paul Bottein
2d13a92496 Update frontend to 20251203.1 (#158069) 2025-12-05 21:25:01 +01:00
Artur Pragacz
b06bffa815 Add ai_task to core files (#158058) 2025-12-05 21:14:49 +01:00
Joost Lekkerkerker
b8f4b9515b Prevent entsoe from loading (#158036)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-05 21:08:57 +01:00
Petro31
3c10e9f1c0 Fix inverted kelvin issue (#158054) 2025-12-05 19:12:12 +00:00
Artur Pragacz
2dec3befcd Assign hass in Condition init (#158062) 2025-12-05 19:04:11 +00:00
J. Nick Koston
7d065bf314 Bump aiodns to 3.6.0 (#158063) 2025-12-05 20:00:09 +01:00
Raphael Hehl
3315680d0b Bump uiprotect to 7.33.2 (#158057) 2025-12-05 19:43:44 +01:00
Markus Jacobsen
ce48c89a26 Fix button event entity creation in Bang & Olufsen (#157982)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-05 18:39:58 +00:00
Jan Bouwhuis
f67a926f56 Move lametric URLs out of strings.json (#158051) 2025-12-05 19:35:07 +01:00
Paulus Schoutsen
e0a9d305b2 Use multiple selector for validation in AI task (#158056) 2025-12-05 18:51:18 +01:00
Jan Bouwhuis
4ff141d35e Move example image path out of translatable strings (#158053) 2025-12-05 18:05:09 +01:00
Artur Pragacz
f12a43b2b7 Mark reauthentication in music assistant quality scale (#158055) 2025-12-05 18:02:16 +01:00
Paul Tarjan
35e6f504a3 Fix doorbird duplicate unique ID generation (#158013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 10:17:43 -06:00
Denis Shulyaka
1f68809cf9 Replace deprecated preview image model (#158048) 2025-12-05 07:55:05 -08:00
Paul Bottein
66bddebca1 Add subscribe preview feature endpoint to labs (#157976) 2025-12-05 16:36:56 +01:00
TheJulianJES
2280d779a8 Change ZHA strings for incorrect adapter state (#158021)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-05 16:35:34 +01:00
Maciej Bieniek
ebc608845c Do not create restart button for sleeping gen2+ Shelly devices (#158047) 2025-12-05 16:33:11 +01:00
Max Michels
5d13a41926 Move telegram-bot URLs out of strings.json (#155130)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-12-05 16:33:01 +01:00
Quentin Ulmer
630b40fbba Fix Rituals Perfume Genie (#151537)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 16:16:51 +01:00
Manu
7fd440c4a0 Add coordinator to Duck DNS integration (#158041) 2025-12-05 15:49:48 +01:00
Petro31
2a116a2a11 Fix missing template key in deprecation repair (#158033) 2025-12-05 15:30:39 +01:00
David Bonnes
f189e3b5ca Bump evohome-async to 1.0.6 (#158005) 2025-12-05 13:27:38 +01:00
wollew
4cd460351d Add Squeezebox binary sensors for player alarm status (#154491)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-05 11:43:19 +01:00
Johnny Willemsen
afea571c2c Enhance migration logging for home_connect (#158027) 2025-12-05 11:30:10 +01:00
Manu
e4aadd675e Add reconfigure flow to Duck DNS (#157948) 2025-12-05 10:19:05 +01:00
Abílio Costa
a47255c233 Bump whirlpool-sixth-sense to 1.0.3 (#157996) 2025-12-05 08:27:31 +01:00
hanwg
c1e7492743 Improve action descriptions for Telegram bot (#158022) 2025-12-05 08:26:42 +01:00
Kevin Stillhammer
63e8cf582f Set PARALLEL_UPDATES in fressnapf_tracker (#158008) 2025-12-05 08:21:48 +01:00
Allen Porter
73f23168a2 Bump python-roborock to 3.10.2 (#158020) 2025-12-05 08:20:41 +01:00
Mark Adkins
20d8176515 SharkIQ dep upgrade v1.5.0 (#158015) 2025-12-04 22:00:47 -05:00
Ezra Freedman
c9351a022e Add HassStopMoving intent for covers and valves (#155267)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-12-04 22:23:49 +01:00
epenet
4e8a31a4e2 Improve Tuya data validation (#157968)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 22:20:05 +01:00
johanzander
2beb551db3 Replace bare Exception with specific exceptions in Growatt (#157790) 2025-12-04 20:44:41 +01:00
Markus Jacobsen
90cea0325f Bump mozart_api to 5.3.1.108.0 (#157983) 2025-12-04 19:29:35 +00:00
dontinelli
f5dd9d83ac Bump solarlog_cli to 0.6.1 (#157845) 2025-12-04 19:24:33 +00:00
Alsatian67
e0484ba1ff Improve dev error message for YAML platform setup missing method (#155505)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-04 20:06:37 +01:00
Kevin Stillhammer
62f758f695 mark quality_scale rules done for fressnapf_tracker (#157990) 2025-12-04 20:03:53 +01:00
Abílio Costa
20d2115122 Bump oralb-ble to 1.0.2 (#157992) 2025-12-04 18:51:16 +01:00
Jan Bouwhuis
2bed7afe0e Move out example URL and IP of strings.json for reolink (#157970) 2025-12-04 18:37:30 +01:00
Petro31
2eeac5f9c9 Update template deprecation to be more explicit (#157965) 2025-12-04 18:34:01 +01:00
Abílio Costa
a35af9097b Remove uneeded async_setup_component from trigger/condition tests (#157873) 2025-12-04 17:21:30 +00:00
Luke Lashley
710b7c2b41 Bump python-Roborock to 3.10.0 (#157980) 2025-12-04 17:26:41 +01:00
Abílio Costa
c058810461 Cache flattened service descriptions in websocket api (#157510)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-04 16:05:49 +00:00
Kevin Stillhammer
0ccfd77fef add switch platform to fressnapf_tracker (#157971) 2025-12-04 16:44:53 +01:00
epenet
4805b33a27 Fix unit parsing in Tuya climate entities (#157964) 2025-12-04 16:17:39 +01:00
Hem Bhagat
c333036959 Move translatable URL out of strings.json for ntfy integration (#155859) 2025-12-04 16:17:16 +01:00
Petro31
002eed24f1 Fix template migration errors (#157949) 2025-12-04 16:16:58 +01:00
Jan Bouwhuis
9a9f8271b3 Move pilight URL out of strings.json (#157967) 2025-12-04 16:02:28 +01:00
Paulus Schoutsen
855d7c6e16 Extract WebRTC integration (#157648) 2025-12-04 09:44:24 -05:00
Jordan Harvey
837de55ce6 Set account number as required for Anglian Water config entry (#157939) 2025-12-04 15:39:52 +01:00
epenet
81ed259c59 Move Tuya type information classes to separate module (#157958)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 15:34:04 +01:00
Manu
5f00452c96 Convert image URLs to secure URLs in Xbox integration (#157945) 2025-12-04 15:12:33 +01:00
Jan Bouwhuis
06a44de3fb Move Yeelight URLs out of translatable strings for action descriptions (#157957) 2025-12-04 15:10:50 +01:00
Jan Bouwhuis
11b4d75cfb Move out zwave_js api docs url from strings.json (#157959) 2025-12-04 15:10:26 +01:00
David Rapan
845c9ee05f Fix Starlink's ever updating uptime (#155574)
Signed-off-by: David Rapan <david@rapan.cz>
2025-12-04 14:44:23 +01:00
Jordan Harvey
dedf6b1223 Add pyanglianwater to Anglian Water loggers (#157947) 2025-12-04 13:55:24 +01:00
Felipe Santos
c1b631d049 Remove Intellicode extension from devcontainer (#157894) 2025-12-04 13:43:01 +01:00
milanhin
6cc645bc6c Remove deprecation warning of step_id in ConfigFlow class (#157925) 2025-12-04 13:41:52 +01:00
Jan Bouwhuis
f10866395d Move out URL of Xiaomy_aquara from strings.json (#157937)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 12:59:55 +01:00
Jan Bouwhuis
df68448b27 Move translatable URL from rainmachine push_weather_data action description (#157941)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-04 12:59:15 +01:00
Abílio Costa
bf7b96622c Correct websocket commands test name (#157870) 2025-12-04 11:14:34 +00:00
Kevin Stillhammer
53c644ac5b add light platform to fressnapf_tracker (#157865) 2025-12-04 11:09:53 +01:00
starkillerOG
5e9107e52b Bump reolink_aio to 0.17.1 (#157929) 2025-12-04 10:58:25 +01:00
Jan Bouwhuis
ca9ea267c7 Move teslemetry time-of-use URL out of strings.json (#157874) 2025-12-04 10:34:51 +01:00
ryanjones-gentex
f1bfe2f11e Add HomeLink integration (#136460)
Co-authored-by: Nicholas Aelick <niaexa@syntronic.com>
2025-12-04 10:32:02 +01:00
Franck Nijhof
34cc6036b9 Merge branch 'master' into dev 2025-12-04 09:12:55 +00:00
cdnninja
2facfbadaa Add VeSync type hints and returns (#157900)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-04 09:19:48 +01:00
cdnninja
1b1dface35 Fix VeSync binary sensor discovery (#157898) 2025-12-04 09:14:35 +01:00
Franck Nijhof
ca4a2d441e 2025.12.0 (#157330) 2025-12-03 19:06:27 +01:00
Franck Nijhof
c2ce322af1 Bump version to 2025.12.0 2025-12-03 16:28:33 +00:00
Thomas55555
079f306a65 Fix strings in Google Air Quality (#157862) 2025-12-03 16:28:06 +00:00
Franck Nijhof
7bf60f9d15 Bump version to 2025.12.0b9 2025-12-03 15:57:15 +00:00
Robert Resch
7dddd89ac2 Add retry logic to docker.io image push step (#157859) 2025-12-03 15:57:03 +00:00
Luke Lashley
a2322ef3c7 Bump Roborock to 3.9.3 (#157852) 2025-12-03 15:57:01 +00:00
Bram Kragten
5f6ef2109a Update frontend to 20251203.0 (#157851) 2025-12-03 15:57:00 +00:00
starkillerOG
44f0a8899a Bump reolink_aio to 0.17.0 (#157850) 2025-12-03 15:56:58 +00:00
Robert Resch
78fa29b41f Bump deebot-client to 17.0.0 (#157836) 2025-12-03 15:56:57 +00:00
Manu
06d4f085c0 Prevent startup blocking when a friend’s trophy summary is private on PlayStation Network (#157597)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-12-03 15:56:55 +00:00
victorigualada
a9deb2a08a Bump hass-nabucasa from 1.6.2 to 1.7.0 (#157834) 2025-12-03 13:56:01 +00:00
victorigualada
0d26d22986 Allow non strict response_format structures for Cloud LLM generation (#157822) 2025-12-03 13:55:09 +00:00
Erik Montnemery
062366966b Pin Python point release used in CI (#157819) 2025-12-03 13:53:30 +00:00
Franck Nijhof
1b8a50e80a Bump version to 2025.12.0b8 2025-12-03 12:16:30 +00:00
Franck Nijhof
59761385f0 Add final learn more and feedback links for purpose-specific triggers and conditions preview feature (#157830) 2025-12-03 12:16:21 +00:00
Robert Resch
6536d348e5 Prioritize default stun port over alternative (#157829) 2025-12-03 12:16:20 +00:00
Michael
c157c83d54 Add integration_type to Oralb (#157828) 2025-12-03 12:16:18 +00:00
torben-iometer
77425cc40f bump iometer to v0.3.0 (#157826) 2025-12-03 12:16:16 +00:00
Franck Nijhof
c4b67329c3 Bump version to 2025.12.0b7 2025-12-03 10:42:56 +00:00
Allen Porter
c1f8c89bd0 Bump python-roborock to 3.9.2 (#157815)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:42:32 +00:00
Allen Porter
b1bf6f5678 Bump google-nest-sdm to 9.1.2 (#157812)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:42:30 +00:00
Josef Zweck
d347136188 Mark nordpool as service integration_type (#157810) 2025-12-03 10:42:29 +00:00
Kamil Breguła
a4319f3bf8 Update release URL in WLED (#157801) 2025-12-03 10:42:27 +00:00
Marc Mueller
db27aee62a Fix ping TypeError when killing the process (#157794) 2025-12-03 10:42:26 +00:00
Joost Lekkerkerker
a7446b3da9 Make occupancy trigger check occupancy instead of presence (#157791) 2025-12-03 10:42:24 +00:00
Stefan Agner
7fc5464621 Add storage link to low disk space repair issue (#157786)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-12-03 10:42:23 +00:00
hanwg
a00b50c195 Fix bug in group notify entities when title is missing (#157171)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 10:42:21 +00:00
Franck Nijhof
738fb59efa Bump version to 2025.12.0b6 2025-12-02 21:23:23 +00:00
Joris Pelgröm
04e512a48e Bump letpot to 0.6.4 (#157781) 2025-12-02 21:23:12 +00:00
Michael Hansen
c63aca2d9b Bump hassil to 3.5.0 (#157780) 2025-12-02 21:23:11 +00:00
Kamil Breguła
c95203e095 Handle unsupported version in WLED (#157778)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-02 21:23:09 +00:00
Josef Zweck
259235ceeb Add integration_type for tedee (#157776) 2025-12-02 21:23:08 +00:00
Paulus Schoutsen
c7f1729300 Allow fetching the Cloud ICE servers (#157774) 2025-12-02 21:23:06 +00:00
puddly
065329e668 Fix ZHA network formation (#157769) 2025-12-02 21:23:05 +00:00
Marcel van der Veldt
a93ed69fe4 Let AuthenticationRequired also trigger the reauth flow in MusicAssistant (#157580) 2025-12-02 21:23:04 +00:00
Sab44
189497622d Fix orphaned devices not being removed during integration startup (#155900) 2025-12-02 21:23:02 +00:00
Franck Nijhof
a466fc4a01 Bump version to 2025.12.0b5 2025-12-02 18:19:42 +00:00
Matthias Alphart
8a968b5d0e Add integration_type for Fronius (#157760) 2025-12-02 18:19:13 +00:00
Michael Hansen
3baee5c4ac Bump intents to 2025.12.2 (#157758) 2025-12-02 18:17:57 +00:00
Bram Kragten
f624a43770 Update frontend to 20251202.0 (#157755)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-02 18:16:17 +00:00
Artur Pragacz
242935774b Add integration type to ping (#157730)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:17 +00:00
Artur Pragacz
051ad5878f Add integration type to rest (#157728)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:16 +00:00
Artur Pragacz
b2156c1d4c Add integration type to speedtestdotnet (#157727)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:15 +00:00
Artur Pragacz
7d4394f7ed Add integration type to google_translate (#157718)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:14 +00:00
Brett Adams
4df172374c Add integration_type to Tesla Fleet manifest (#157679) 2025-12-02 18:14:13 +00:00
Brett Adams
c97755472e Add integration_type to Teslemetry manifest (#157677) 2025-12-02 18:14:11 +00:00
Erik Montnemery
ebc9060b01 Improve trigger descriptions (#157643)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-02 18:14:10 +00:00
Erik Montnemery
bbcc2a94b3 Add occupancy binary sensor triggers (#157631) 2025-12-02 18:14:09 +00:00
victorigualada
692188fa85 Don't register Home Assistant Cloud LLM platforms if not logged in (#157630) 2025-12-02 18:14:08 +00:00
Jordan Harvey
2c993ea5a2 Fix Anglian Water sensor setup (#157457) 2025-12-02 18:14:06 +00:00
Franck Nijhof
c765776726 Bump version to 2025.12.0b4 2025-12-02 12:42:32 +00:00
Robert Resch
723365d8e6 Create the go2rtc unix socket inside a temporary folder (#157742) 2025-12-02 12:42:13 +00:00
Artur Pragacz
3d8e136049 Add integration type to xiaomi_ble (#157740)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:11 +00:00
Artur Pragacz
2fe9fc7ee3 Add integration type to broadlink (#157739)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:10 +00:00
Artur Pragacz
e11e31a1a0 Add integration type to ring (#157738)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:09 +00:00
Artur Pragacz
989407047d Add integration type to roborock (#157737)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:08 +00:00
Artur Pragacz
6d3087c5a4 Add integration type to webostv (#157736)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:06 +00:00
Artur Pragacz
9bd3c35231 Add integration type to tplink (#157735)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:05 +00:00
Artur Pragacz
b7e97971cf Add integration type to ibeacon (#157734)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:04 +00:00
Artur Pragacz
4d232c63f8 Add integration type to dlna_dmr (#157733)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:03 +00:00
Artur Pragacz
6fc000ee2a Add integration type to google (#157729)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:02 +00:00
Artur Pragacz
623d3ecde5 Add integration type to music_assistant (#157725)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:00 +00:00
Artur Pragacz
0fbb3215b4 Add integration type to dlna_dms (#157723)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:59 +00:00
Artur Pragacz
c82ce1ff89 Add integration type to met (#157720)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:58 +00:00
Franck Nijhof
8c891a20e5 Rename preview feature to purpose-specific triggers and conditions (#157717) 2025-12-02 12:41:57 +00:00
Erik Montnemery
97c50b2d86 Improve helpers.condition.async_subscribe_platform_events (#157710) 2025-12-02 12:41:55 +00:00
Erik Montnemery
ef4062a565 Improve helpers.trigger.async_subscribe_platform_events (#157709) 2025-12-02 12:41:54 +00:00
cdnninja
e31cce5d9b Bump pyvesync to 3.3.3 (#157697) 2025-12-02 12:41:53 +00:00
Paulus Schoutsen
21f6b9a53a Add integration_type to Nuki Bridge manifest (#157683)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:52 +00:00
Paulus Schoutsen
047e549112 Add integration_type to Motionblinds manifest (#157682)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:51 +00:00
Paulus Schoutsen
4c4aecd9a7 Add integration_type to Konnected.io manifest (#157681) 2025-12-02 12:41:50 +00:00
Paulus Schoutsen
733496ff3f Add integration_type to HomeWizard Energy manifest (#157680)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:49 +00:00
Paulus Schoutsen
f682e93243 Add integration_type to Tessie manifest (#157676)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:48 +00:00
Paulus Schoutsen
c8fa5b0290 Add integration_type to SwitchBot Bluetooth manifest (#157675)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:46 +00:00
Paulus Schoutsen
8ff2a22664 Add integration_type to Sonos manifest (#157674)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:45 +00:00
Paulus Schoutsen
c174ab2d96 Add integration_type to SmartThings manifest (#157673) 2025-12-02 12:41:44 +00:00
Paulus Schoutsen
10f0ff7bd7 Add integration_type to Reolink manifest (#157672)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:43 +00:00
Paulus Schoutsen
4a4eb33bf7 Add integration_type to HomeKit Device manifest (#157671)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:42 +00:00
Paulus Schoutsen
8199c4e5de Add integration_type to Home Connect manifest (#157668)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:40 +00:00
Paulus Schoutsen
0bfa8318a7 Add integration_type to Ecowitt manifest (#157666) 2025-12-02 12:41:39 +00:00
Paulus Schoutsen
ed66a4920c Add integration_type to Apple TV manifest (#157664)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:38 +00:00
Åke Strandberg
f51007c448 Add program id:s and phases to new Miele WQ1000 (#157660) 2025-12-02 12:41:37 +00:00
TheJulianJES
bd44402b04 Set Matter integration type to "hub" (#157657) 2025-12-02 12:41:36 +00:00
TheJulianJES
99fa92d966 Set ZHA integration type to "hub" (#157656) 2025-12-02 12:41:35 +00:00
Arjan
1cb8f19020 Meteo France: add new mapping "Brouillard dense givrant" (#157627) 2025-12-02 12:41:33 +00:00
Copilot
81cdbdd4df Add labs_updated event to subscription allowlist (#157552)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2025-12-02 12:41:32 +00:00
Franck Nijhof
c82706eaf5 Bump version to 2025.12.0b3 2025-12-01 19:26:30 +00:00
andreimoraru
07f9bec8b6 bump yt-dlp to 2025.11.12 (#157645) 2025-12-01 19:26:14 +00:00
Åke Strandberg
33d576234b Add code mappings for Miele WQ1000 (#157642) 2025-12-01 19:26:13 +00:00
Bram Kragten
9e2b4615f1 Update frontend to 20251201.0 (#157638) 2025-12-01 19:26:11 +00:00
Petro31
a46dc7e05f Reload config entry templates when labs flag automation.new_triggers_conditions is set (#157637)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-01 19:26:09 +00:00
Erik Montnemery
7dd9953345 Bump area registry to version 1.9 and sort areas (#157634) 2025-12-01 19:26:08 +00:00
Maciej Bieniek
1145026190 Bump aioshelly to version 13.22.0 (#157629) 2025-12-01 19:26:06 +00:00
Erik Montnemery
d8f9574bc3 Remove cover triggers (#157621) 2025-12-01 19:26:05 +00:00
Erik Montnemery
e91f8d3a81 Remove description_configured from condition and trigger translations (#157620) 2025-12-01 19:26:04 +00:00
Aidan Timson
8c0fd0565e Default area icons for new instances (#157619) 2025-12-01 19:26:02 +00:00
Paul Bottein
cc620fc0f8 Fix user store not loaded on restart (#157616) 2025-12-01 19:26:00 +00:00
Erik Montnemery
5a89332680 Bump floor registry to version 1.3 and sort floors (#157614)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-01 19:25:59 +00:00
LG-ThinQ-Integration
1831c5e249 Bump thinqconnect to 1.0.9 (#157607) 2025-12-01 19:25:57 +00:00
cdnninja
dddd2503ea Bump pyvesync to 3.3.2 (#157605) 2025-12-01 19:25:56 +00:00
Norbert Rittel
91ba510a1e Fix spelling of "to log in" in anglian_water (#157594) 2025-12-01 19:25:54 +00:00
Norbert Rittel
6e5e739496 Fix spelling of "to set up" in hue_ble (#157593) 2025-12-01 19:25:53 +00:00
Sanjay Govind
6b39eb069c Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-12-01 19:25:51 +00:00
Allen Porter
847c332c70 Bump google-nest-sdm to 9.1.1 (#157562) 2025-12-01 19:25:50 +00:00
J. Nick Koston
1a19f3b527 Bump aioesphomeapi to 42.9.0 (#157558) 2025-12-01 19:25:49 +00:00
Thomas55555
8110935d2d Bump google air quality api to 1.1.3 (#157555) 2025-12-01 19:25:47 +00:00
Raphael Hehl
af69da94f5 Bump uiprotect to 7.31.0 (#157543) 2025-12-01 19:25:46 +00:00
Jan Bouwhuis
c1cf17d4db Fix MQTT entity cannot be renamed (#157540) 2025-12-01 19:25:44 +00:00
Allen Porter
6079637909 Bump python-roborock to 3.8.4 (#157538) 2025-12-01 19:25:43 +00:00
Jordan Harvey
9268e12b20 Disable cookie quotes for Anglian Water (#157518) 2025-12-01 19:25:41 +00:00
Raphael Hehl
d07993f4a4 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-12-01 19:25:40 +00:00
Allen Porter
441cb4197c Bump python-roborock to 3.8.3 (#157512) 2025-12-01 19:25:38 +00:00
J. Nick Koston
d2a095588d Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-12-01 19:25:37 +00:00
Arie Catsman
f2578da7db Bump pyenphase to 2.4.2 (#157500) 2025-12-01 19:25:35 +00:00
Jan Bouwhuis
22200d6804 Fix subentry ID is not updated when renaming the entity ID (#157498) 2025-12-01 19:25:34 +00:00
Maciej Bieniek
8a4e5c3a28 Remove name from Shelly RGBCCT sensors (#157492) 2025-12-01 19:25:32 +00:00
Maciej Bieniek
30f31c7d8c Remove name for Shelly gas valve (gen1) entity (#157490) 2025-12-01 19:25:30 +00:00
Maciej Bieniek
232c4255a1 Add missing string for Shelly away mode switch (#157488) 2025-12-01 19:25:28 +00:00
Petro31
236f7cd22c Ensure platform template does not appear in repair (#157486) 2025-12-01 19:25:27 +00:00
Åke Strandberg
5948ff2e31 Add loggers to senz manifest (#157479) 2025-12-01 19:25:25 +00:00
epenet
380127bc70 Fix blocking call in Tuya initialisation (#157477) 2025-12-01 19:25:23 +00:00
epenet
b6a1e8251a Remove unnecessary instanciating in Tuya find_dpcode (#157473) 2025-12-01 19:25:22 +00:00
David Woodhouse
c20236717c Clarify percentage_command_topic and percentage_state_topic for MQTT fan (#157460)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-12-01 19:25:21 +00:00
Artur Pragacz
1fd9feaace Provide log info for discovered flows in logger (#157454) 2025-12-01 19:25:19 +00:00
ElectricSteve
7ce072b4dc bump: youtubeaio to 2.1.1 (#157452) 2025-12-01 19:25:17 +00:00
Artur Pragacz
45aa0399c7 Add tools in default agent also in fallback pipeline (#157441) 2025-12-01 19:25:16 +00:00
Hem Bhagat
d82b3871c1 Move translatable URLs out of strings.json for opentherm_gw integration (#157437) 2025-12-01 19:25:14 +00:00
Thomas55555
8f6d1162e5 Fix strings in Google Air Quality (#157297) 2025-12-01 19:21:28 +00:00
puddly
dafce97341 Disable owning integrations for the entire firmware interaction process (#157082) 2025-12-01 19:21:26 +00:00
Sebastian Schneider
ffd5d33bbc Support UniFi LED control for devices without RGB (#156812) 2025-12-01 19:21:24 +00:00
Franck Nijhof
bac32bc379 Bump version to 2025.12.0b2 2025-11-27 17:13:26 +00:00
Allen Porter
6344837009 Fix regression in roborock image entity naming (#157432) 2025-11-27 17:12:08 +00:00
Allen Porter
9079ff5ea8 Update roborock test typing (#157370) 2025-11-27 17:12:06 +00:00
Bram Kragten
cd646aea11 Update frontend to 20251127.0 (#157431) 2025-11-27 17:09:23 +00:00
Shay Levy
b3a93d9fab Fix Shelly support for button5 trigger (#157422) 2025-11-27 17:09:22 +00:00
Denis Shulyaka
db98fb138b Fix Anthropic init with incorrect model (#157421) 2025-11-27 17:09:21 +00:00
Petro31
348c8bca7c Avoid custom template platform deprecations (#157415) 2025-11-27 17:09:20 +00:00
Allen Porter
e30707ad5e Bump python-roborock to 3.8.1 (#157376) 2025-11-27 17:09:18 +00:00
Petro31
3fa4dcb980 Reload templates when labs flag automation.new_triggers_conditions is set (#157368) 2025-11-27 17:09:17 +00:00
Kamil Breguła
57835efc9d Fix MAC address mix-ups between WLED devices (#155491)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-27 17:09:16 +00:00
Franck Nijhof
f8d5a8bc58 Bump version to 2025.12.0b1 2025-11-27 11:49:46 +00:00
epenet
3f1f8da6f5 Bump renault-api to 0.5.1 (#157411) 2025-11-27 11:48:09 +00:00
Jan Čermák
55613f56b6 Fix state classes of Ecowitt rain sensors (#157409) 2025-11-27 11:48:08 +00:00
victorigualada
3ee2a78663 Bump hass-nabucasa from 1.6.1 to 1.6.2 (#157405) 2025-11-27 11:48:06 +00:00
victorigualada
814a0c4cc9 Return early when setting cloud ai_task and conversation and not logged in to cloud (#157402) 2025-11-27 11:48:04 +00:00
starkillerOG
71b674d8f1 Bump reolink-aio to 0.16.6 (#157399) 2025-11-27 11:48:03 +00:00
Erik Montnemery
c952fc5e31 Minor polish of cover trigger tests (#157397) 2025-11-27 11:48:02 +00:00
Allen Porter
8c3d40a348 Remove old roborock map storage (#157379) 2025-11-27 11:48:01 +00:00
Paulus Schoutsen
2451dfb63d Default conversation agent to store tool calls in chat log (#157377) 2025-11-27 11:48:00 +00:00
Sarah Seidman
8e5921eab6 Normalize input for Droplet pairing code (#157361) 2025-11-27 11:47:59 +00:00
Jaap Pieroen
bc730da9b1 Bugfix: Essent remove average gas price today (#157317) 2025-11-27 11:47:57 +00:00
abelyliu
28b7ebea6e Fix parsing of Tuya electricity RAW values (#157039) 2025-11-27 11:47:56 +00:00
Erik Montnemery
cfa447c7a9 Add climate started_cooling and started_drying triggers (#156945) 2025-11-27 11:47:55 +00:00
Franck Nijhof
f64c870e42 Bump version to 2025.12.0b0 2025-11-26 17:13:42 +00:00
229 changed files with 6809 additions and 1420 deletions

View File

@@ -13,6 +13,7 @@ core: &core
# Our base platforms, that are used by other integrations
base_platforms: &base_platforms
- homeassistant/components/ai_task/**
- homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/**

View File

@@ -27,7 +27,6 @@
"charliermarsh.ruff",
"ms-python.pylint",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github",

8
CODEOWNERS generated
View File

@@ -571,6 +571,8 @@ build.json @home-assistant/supervisor
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -1356,8 +1358,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter
@@ -1803,6 +1805,8 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/web_rtc/ @home-assistant/core
/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core

View File

@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),

View File

@@ -30,6 +30,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
@@ -68,34 +69,19 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
account_number=user_input[CONF_ACCOUNT_NUMBER],
)
)
if isinstance(validation_response, BaseAuth):
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account_number,
title=user_input[CONF_ACCOUNT_NUMBER],
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
}

View File

@@ -127,6 +127,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"binary_sensor",
"climate",
"cover",
"device_tracker",
"fan",
"lawn_mower",
"light",

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["mozart-api==5.1.0.247.1"],
"requirements": ["mozart-api==5.3.1.108.0"],
"zeroconf": ["_bangolufsen._tcp.local."]
}

View File

@@ -583,7 +583,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
for sound_mode in sound_modes:
label = f"{sound_mode.name} ({sound_mode.id})"
self._sound_modes[label] = sound_mode.id
self._sound_modes[label] = cast(int, sound_mode.id)
if sound_mode.id == active_sound_mode.id:
self._attr_sound_mode = label

View File

@@ -42,14 +42,25 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
def get_device_buttons(model: BeoModel) -> list[str]:
"""Get supported buttons for a given model."""
# Beoconnect Core does not have any buttons
if model == BeoModel.BEOCONNECT_CORE:
return []
buttons = DEVICE_BUTTONS.copy()
# Beosound Premiere does not have a bluetooth button
if model == BeoModel.BEOSOUND_PREMIERE:
# Models that don't have a microphone button
if model in (
BeoModel.BEOSOUND_A5,
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Models that don't have a Bluetooth button
if model in (
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
# Beoconnect Core does not have any buttons
elif model == BeoModel.BEOCONNECT_CORE:
buttons = []
return buttons

View File

@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit, RTCIceServer
from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -37,6 +37,7 @@ from homeassistant.components.stream import (
Stream,
create_stream,
)
from homeassistant.components.web_rtc import async_get_ice_servers
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -84,7 +85,6 @@ from .prefs import (
get_dynamic_camera_stream_settings,
)
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
@@ -93,7 +93,6 @@ from .webrtc import (
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
async_register_ice_servers,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -400,20 +399,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
@callback
def get_ice_servers() -> list[RTCIceServer]:
if hass.config.webrtc.ice_servers:
return hass.config.webrtc.ice_servers
return [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:3478",
"stun:stun.home-assistant.io:80",
]
),
]
async_register_ice_servers(hass, get_ice_servers)
return True
@@ -731,11 +716,7 @@ 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()
]
ice_servers = async_get_ice_servers(self.hass)
config.configuration.ice_servers.extend(ice_servers)
return config

View File

@@ -3,7 +3,7 @@
"name": "Camera",
"after_dependencies": ["media_player"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["http"],
"dependencies": ["http", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable, Iterable
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
@@ -12,12 +12,7 @@ from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
from webrtc_models import (
RTCConfiguration,
RTCIceCandidate,
RTCIceCandidateInit,
RTCIceServer,
)
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
@@ -38,9 +33,6 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
_WEBRTC = "WebRTC"
@@ -367,21 +359,3 @@ async def async_get_supported_provider(
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
) -> Callable[[], None]:
"""Register a ICE server.
The registering integration is responsible to implement caching if needed.
"""
servers = hass.data.setdefault(DATA_ICE_SERVERS, [])
def remove() -> None:
servers.remove(get_ice_server_fn)
servers.append(get_ice_server_fn)
return remove

View File

@@ -19,8 +19,8 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE

View File

@@ -8,7 +8,7 @@
"google_assistant"
],
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["auth", "http", "repairs", "webhook"],
"dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/cloud",
"integration_type": "system",
"iot_class": "cloud_push",

View File

@@ -56,7 +56,6 @@ class DeviceAutomationConditionProtocol(Protocol):
class DeviceCondition(Condition):
"""Device condition."""
_hass: HomeAssistant
_config: ConfigType
@classmethod
@@ -87,7 +86,7 @@ class DeviceCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
self._hass = hass
super().__init__(hass, config)
assert config.options is not None
self._config = config.options

View File

@@ -11,5 +11,13 @@
"see": {
"service": "mdi:account-eye"
}
},
"triggers": {
"entered_home": {
"trigger": "mdi:account-arrow-left"
},
"left_home": {
"trigger": "mdi:account-arrow-right"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted device trackers to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_home": "{entity_name} is home",
@@ -44,6 +48,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"see": {
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
@@ -80,5 +93,27 @@
"name": "See"
}
},
"title": "Device tracker"
"title": "Device tracker",
"triggers": {
"entered_home": {
"description": "Triggers when one or more device trackers enter home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
}
},
"name": "Entered home"
},
"left_home": {
"description": "Triggers when one or more device trackers leave home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
}
},
"name": "Left home"
}
}
}

View File

@@ -0,0 +1,21 @@
"""Provides triggers for device_trackers."""
from homeassistant.const import STATE_HOME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import (
Trigger,
make_entity_state_trigger,
make_from_entity_state_trigger,
)
from .const import DOMAIN
TRIGGERS: dict[str, type[Trigger]] = {
"entered_home": make_entity_state_trigger(DOMAIN, STATE_HOME),
"left_home": make_from_entity_state_trigger(DOMAIN, from_state=STATE_HOME),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for device trackers."""
return TRIGGERS

View File

@@ -0,0 +1,18 @@
.trigger_common: &trigger_common
target:
entity:
domain: device_tracker
fields:
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
entered_home: *trigger_common
left_home: *trigger_common

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.5.0"]
"requirements": ["aiodns==3.6.0"]
}

View File

@@ -102,6 +102,12 @@ class ConfiguredDoorBird:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
@@ -146,13 +152,7 @@ class ConfiguredDoorBird:
async def _async_register_events(self) -> dict[str, Any]:
"""Register events on device."""
# Override url if another is specified in the configuration
if custom_url := self.custom_url:
hass_url = custom_url
else:
# Get the URL of this server
hass_url = get_url(self._hass, prefer_external=False)
hass_url = self._get_hass_url()
http_fav = await self._async_get_http_favorites()
if any(
# Note that a list comp is used here to ensure all
@@ -191,10 +191,14 @@ class ConfiguredDoorBird:
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))

View File

@@ -2,33 +2,22 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine, Sequence
from datetime import datetime, timedelta
import logging
from typing import Any, cast
from aiohttp import ClientSession
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import (
CALLBACK_TYPE,
HassJob,
HomeAssistant,
ServiceCall,
callback,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
from .const import ATTR_CONFIG_ENTRY
from .coordinator import DuckDnsConfigEntry, DuckDnsUpdateCoordinator
from .helpers import update_duckdns
_LOGGER = logging.getLogger(__name__)
@@ -36,17 +25,8 @@ ATTR_TXT = "txt"
DOMAIN = "duckdns"
INTERVAL = timedelta(minutes=5)
BACKOFF_INTERVALS = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
SERVICE_SET_TXT = "set_txt"
UPDATE_URL = "https://www.duckdns.org/update"
CONFIG_SCHEMA = vol.Schema(
{
@@ -71,8 +51,6 @@ SERVICE_TXT_SCHEMA = vol.Schema(
}
)
type DuckDnsConfigEntry = ConfigEntry
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Initialize the DuckDNS component."""
@@ -99,21 +77,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Set up Duck DNS from a config entry."""
session = async_get_clientsession(hass)
coordinator = DuckDnsUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
async def update_domain_interval(_now: datetime) -> bool:
"""Update the DuckDNS entry."""
return await _update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
)
entry.async_on_unload(
async_track_time_interval_backoff(
hass, update_domain_interval, BACKOFF_INTERVALS
)
)
# Add a dummy listener as we do not have regular entities
entry.async_on_unload(coordinator.async_add_listener(lambda: None))
return True
@@ -153,7 +122,7 @@ async def update_domain_service(call: ServiceCall) -> None:
session = async_get_clientsession(call.hass)
await _update_duckdns(
await update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
@@ -164,73 +133,3 @@ async def update_domain_service(call: ServiceCall) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: DuckDnsConfigEntry) -> bool:
"""Unload a config entry."""
return True
_SENTINEL = object()
async def _update_duckdns(
session: ClientSession,
domain: str,
token: str,
*,
txt: str | None | object = _SENTINEL,
clear: bool = False,
) -> bool:
"""Update DuckDNS."""
params = {"domains": domain, "token": token}
if txt is not _SENTINEL:
if txt is None:
# Pass in empty txt value to indicate it's clearing txt record
params["txt"] = ""
clear = True
else:
params["txt"] = cast(str, txt)
if clear:
params["clear"] = "true"
resp = await session.get(UPDATE_URL, params=params)
body = await resp.text()
if body != "OK":
_LOGGER.warning("Updating DuckDNS domain failed: %s", domain)
return False
return True
@callback
@bind_hass
def async_track_time_interval_backoff(
hass: HomeAssistant,
action: Callable[[datetime], Coroutine[Any, Any, bool]],
intervals: Sequence[timedelta],
) -> CALLBACK_TYPE:
"""Add a listener that fires repetitively at every timedelta interval."""
remove: CALLBACK_TYPE | None = None
failed = 0
async def interval_listener(now: datetime) -> None:
"""Handle elapsed intervals with backoff."""
nonlocal failed, remove
try:
failed += 1
if await action(now):
failed = 0
finally:
delay = intervals[failed] if failed < len(intervals) else intervals[-1]
remove = async_call_later(
hass, delay.total_seconds(), interval_listener_job
)
interval_listener_job = HassJob(interval_listener, cancel_on_shutdown=True)
hass.async_run_hass_job(interval_listener_job, dt_util.utcnow())
def remove_listener() -> None:
"""Remove interval listener."""
if remove:
remove()
return remove_listener

View File

@@ -8,7 +8,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN, CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
@@ -16,8 +16,8 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
from . import _update_duckdns
from .const import DOMAIN
from .helpers import update_duckdns
from .issue import deprecate_yaml_issue
_LOGGER = logging.getLogger(__name__)
@@ -31,6 +31,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Duck DNS."""
@@ -44,7 +46,7 @@ class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_DOMAIN: user_input[CONF_DOMAIN]})
session = async_get_clientsession(self.hass)
try:
if not await _update_duckdns(
if not await update_duckdns(
session,
user_input[CONF_DOMAIN],
user_input[CONF_ACCESS_TOKEN],
@@ -79,3 +81,37 @@ class DuckDnsConfigFlow(ConfigFlow, domain=DOMAIN):
deprecate_yaml_issue(self.hass, import_success=True)
return result
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfigure flow."""
errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()
if user_input is not None:
session = async_get_clientsession(self.hass)
try:
if not await update_duckdns(
session,
entry.data[CONF_DOMAIN],
user_input[CONF_ACCESS_TOKEN],
):
errors["base"] = "update_failed"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
return self.async_update_reload_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_NAME: entry.title},
)

View File

@@ -0,0 +1,83 @@
"""Coordinator for the Duck DNS integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aiohttp import ClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .helpers import update_duckdns
_LOGGER = logging.getLogger(__name__)
type DuckDnsConfigEntry = ConfigEntry[DuckDnsUpdateCoordinator]
INTERVAL = timedelta(minutes=5)
BACKOFF_INTERVALS = (
INTERVAL,
timedelta(minutes=1),
timedelta(minutes=5),
timedelta(minutes=15),
timedelta(minutes=30),
)
class DuckDnsUpdateCoordinator(DataUpdateCoordinator[None]):
"""Duck DNS update coordinator."""
config_entry: DuckDnsConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: DuckDnsConfigEntry) -> None:
"""Initialize the Duck DNS update coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=INTERVAL,
)
self.session = async_get_clientsession(hass)
self.failed = 0
async def _async_update_data(self) -> None:
"""Update Duck DNS."""
retry_after = BACKOFF_INTERVALS[
min(self.failed, len(BACKOFF_INTERVALS))
].total_seconds()
try:
if not await update_duckdns(
self.session,
self.config_entry.data[CONF_DOMAIN],
self.config_entry.data[CONF_ACCESS_TOKEN],
):
self.failed += 1
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={
CONF_DOMAIN: self.config_entry.data[CONF_DOMAIN],
},
retry_after=retry_after,
)
except ClientError as e:
self.failed += 1
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={
CONF_DOMAIN: self.config_entry.data[CONF_DOMAIN],
},
retry_after=retry_after,
) from e
self.failed = 0

View File

@@ -0,0 +1,35 @@
"""Helpers for Duck DNS integration."""
from aiohttp import ClientSession
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
UPDATE_URL = "https://www.duckdns.org/update"
async def update_duckdns(
session: ClientSession,
domain: str,
token: str,
*,
txt: str | None | UndefinedType = UNDEFINED,
clear: bool = False,
) -> bool:
"""Update DuckDNS."""
params = {"domains": domain, "token": token}
if txt is not UNDEFINED:
if txt is None:
# Pass in empty txt value to indicate it's clearing txt record
params["txt"] = ""
clear = True
else:
params["txt"] = txt
if clear:
params["clear"] = "true"
resp = await session.get(UPDATE_URL, params=params)
body = await resp.text()
return body == "OK"

View File

@@ -1,13 +1,23 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]",
"update_failed": "Updating Duck DNS failed"
},
"step": {
"reconfigure": {
"data": {
"access_token": "[%key:component::duckdns::config::step::user::data::access_token%]"
},
"data_description": {
"access_token": "[%key:component::duckdns::config::step::user::data_description::access_token%]"
},
"title": "Re-configure {name}"
},
"user": {
"data": {
"access_token": "Token",
@@ -22,11 +32,17 @@
}
},
"exceptions": {
"connection_error": {
"message": "Updating Duck DNS domain {domain} failed due to a connection error"
},
"entry_not_found": {
"message": "Duck DNS integration entry not found"
},
"entry_not_selected": {
"message": "Duck DNS integration entry not selected"
},
"update_failed": {
"message": "Updating Duck DNS domain {domain} failed"
}
},
"issues": {

View File

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

View File

@@ -15,7 +15,9 @@ from .coordinator import (
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -17,6 +17,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class FressnapfTrackerBinarySensorDescription(BinarySensorEntityDescription):

View File

@@ -8,6 +8,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry, FressnapfTrackerDataUpdateCoordinator
from .entity import FressnapfTrackerBaseEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,

View File

@@ -4,6 +4,14 @@
"pet": {
"default": "mdi:paw"
}
},
"switch": {
"energy_saving": {
"default": "mdi:sleep",
"state": {
"off": "mdi:sleep-off"
}
}
}
}
}

View File

@@ -0,0 +1,95 @@
"""Light platform for fressnapf_tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ColorMode,
LightEntity,
LightEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .const import DOMAIN
from .entity import FressnapfTrackerEntity
PARALLEL_UPDATES = 1
LIGHT_ENTITY_DESCRIPTION = LightEntityDescription(
translation_key="led",
entity_category=EntityCategory.CONFIG,
key="led_brightness_value",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker lights."""
async_add_entities(
FressnapfTrackerLight(coordinator, LIGHT_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.led_activatable is not None
and coordinator.data.led_activatable.has_led
and coordinator.data.tracker_settings.features.flash_light
)
class FressnapfTrackerLight(FressnapfTrackerEntity, LightEntity):
"""Fressnapf Tracker light."""
_attr_color_mode: ColorMode = ColorMode.BRIGHTNESS
_attr_supported_color_modes: set[ColorMode] = {ColorMode.BRIGHTNESS}
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
if TYPE_CHECKING:
# The entity is not created if led_brightness_value is None
assert self.coordinator.data.led_brightness_value is not None
return int(round((self.coordinator.data.led_brightness_value / 100) * 255))
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
self.raise_if_not_activatable()
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
brightness = int((brightness / 255) * 100)
await self.coordinator.client.set_led_brightness(brightness)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_led_brightness(0)
await self.coordinator.async_request_refresh()
def raise_if_not_activatable(self) -> None:
"""Raise error with reasoning if light is not activatable."""
if TYPE_CHECKING:
# The entity is not created if led_activatable is None
assert self.coordinator.data.led_activatable is not None
error_type: str | None = None
if not self.coordinator.data.led_activatable.seen_recently:
error_type = "not_seen_recently"
elif not self.coordinator.data.led_activatable.not_charging:
error_type = "charging"
elif not self.coordinator.data.led_activatable.nonempty_battery:
error_type = "low_battery"
if error_type is not None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=error_type,
)
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if self.coordinator.data.led_brightness_value is not None:
return self.coordinator.data.led_brightness_value > 0
return False

View File

@@ -28,20 +28,26 @@ rules:
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
discovery-update-info:
status: exempt
comment: |
This integration is a cloud service and thus does not support discovery.
discovery:
status: exempt
comment: |
This integration is a cloud service and thus does not support discovery.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
@@ -50,12 +56,15 @@ rules:
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have many entities. All of them are fundamental.
entity-translations: done
exception-translations: todo
icon-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo

View File

@@ -18,6 +18,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class FressnapfTrackerSensorDescription(SensorEntityDescription):

View File

@@ -45,5 +45,28 @@
}
}
}
},
"entity": {
"light": {
"led": {
"name": "Flashlight"
}
},
"switch": {
"energy_saving": {
"name": "Sleep mode"
}
}
},
"exceptions": {
"charging": {
"message": "The flashlight cannot be activated while charging."
},
"low_battery": {
"message": "The flashlight cannot be activated due to low battery."
},
"not_seen_recently": {
"message": "The flashlight cannot be activated when the tracker has not moved recently."
}
}
}

View File

@@ -0,0 +1,60 @@
"""Switch platform for Fressnapf Tracker."""
from typing import TYPE_CHECKING, Any
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
PARALLEL_UPDATES = 1
SWITCH_ENTITY_DESCRIPTION = SwitchEntityDescription(
translation_key="energy_saving",
entity_category=EntityCategory.CONFIG,
device_class=SwitchDeviceClass.SWITCH,
key="energy_saving",
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker switches."""
async_add_entities(
FressnapfTrackerSwitch(coordinator, SWITCH_ENTITY_DESCRIPTION)
for coordinator in entry.runtime_data
if coordinator.data.tracker_settings.features.energy_saving_mode
)
class FressnapfTrackerSwitch(FressnapfTrackerEntity, SwitchEntity):
"""Fressnapf Tracker switch."""
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
await self.coordinator.client.set_energy_saving(True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
await self.coordinator.client.set_energy_saving(False)
await self.coordinator.async_request_refresh()
@property
def is_on(self) -> bool:
"""Return true if device is on."""
if TYPE_CHECKING:
# The entity is not created if energy_saving is None
assert self.coordinator.data.energy_saving is not None
return self.coordinator.data.energy_saving.value == 1

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.0"]
"requirements": ["home-assistant-frontend==20251203.1"]
}

View File

@@ -0,0 +1,58 @@
"""The homelink integration."""
from __future__ import annotations
from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
from .const import DOMAIN
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator, HomeLinkData
PLATFORMS: list[Platform] = [Platform.EVENT]
async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Set up homelink from a config entry."""
auth_implementation = oauth2.SRPAuthImplementation(hass, DOMAIN)
config_entry_oauth2_flow.async_register_implementation(
hass, DOMAIN, auth_implementation
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass), session
)
provider = MQTTProvider(authenticated_session)
coordinator = HomeLinkCoordinator(hass, provider, entry)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, coordinator.async_on_unload
)
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = HomeLinkData(
provider=provider, coordinator=coordinator, last_update_id=None
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.coordinator.async_on_unload(None)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,14 @@
"""application_credentials platform for the gentex homelink integration."""
from homeassistant.components.application_credentials import ClientCredential
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from . import oauth2
async def async_get_auth_implementation(
hass: HomeAssistant, auth_domain: str, _credential: ClientCredential
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
"""Return custom SRPAuth implementation."""
return oauth2.SRPAuthImplementation(hass, auth_domain)

View File

@@ -0,0 +1,66 @@
"""Config flow for homelink."""
import logging
from typing import Any
import botocore.exceptions
from homelink.auth.srp_auth import SRPAuth
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from .const import DOMAIN
from .oauth2 import SRPAuthImplementation
_LOGGER = logging.getLogger(__name__)
class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle homelink OAuth2 authentication."""
DOMAIN = DOMAIN
def __init__(self) -> None:
"""Set up the flow handler."""
super().__init__()
self.flow_impl = SRPAuthImplementation(self.hass, DOMAIN)
@property
def logger(self):
"""Get the logger."""
return _LOGGER
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Ask for username and password."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
srp_auth = SRPAuth()
try:
tokens = await self.hass.async_add_executor_job(
srp_auth.async_get_access_token,
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
)
except botocore.exceptions.ClientError:
_LOGGER.exception("Error authenticating homelink account")
errors["base"] = "srp_auth_failed"
except Exception:
_LOGGER.exception("An unexpected error occurred")
errors["base"] = "unknown"
else:
self.external_data = {"tokens": tokens}
return await self.async_step_creation()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
),
errors=errors,
)

View File

@@ -0,0 +1,7 @@
"""Constants for the homelink integration."""
DOMAIN = "gentex_homelink"
OAUTH2_TOKEN = "https://auth.homelinkcloud.com/oauth2/token"
POLLING_INTERVAL = 5
EVENT_PRESSED = "Pressed"

View File

@@ -0,0 +1,113 @@
"""Makes requests to the state server and stores the resulting data so that the buttons can access it."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
import logging
from typing import TYPE_CHECKING, TypedDict
from homelink.model.device import Device
from homelink.mqtt_provider import MQTTProvider
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.ssl import get_default_context
if TYPE_CHECKING:
from .event import HomeLinkEventEntity
_LOGGER = logging.getLogger(__name__)
type HomeLinkConfigEntry = ConfigEntry[HomeLinkData]
type EventCallback = Callable[[HomeLinkEventData], None]
@dataclass
class HomeLinkData:
"""Class for HomeLink integration runtime data."""
provider: MQTTProvider
coordinator: HomeLinkCoordinator
last_update_id: str | None
class HomeLinkEventData(TypedDict):
"""Data for a single event."""
requestId: str
timestamp: int
class HomeLinkMQTTMessage(TypedDict):
"""HomeLink MQTT Event message."""
type: str
data: dict[str, HomeLinkEventData] # Each key is a button id
class HomeLinkCoordinator:
"""HomeLink integration coordinator."""
def __init__(
self,
hass: HomeAssistant,
provider: MQTTProvider,
config_entry: HomeLinkConfigEntry,
) -> None:
"""Initialize my coordinator."""
self.hass = hass
self.config_entry = config_entry
self.provider = provider
self.device_data: list[Device] = []
self.buttons: list[HomeLinkEventEntity] = []
self._listeners: dict[str, EventCallback] = {}
@callback
def async_add_event_listener(
self, update_callback: EventCallback, target_event_id: str
) -> Callable[[], None]:
"""Listen for updates."""
self._listeners[target_event_id] = update_callback
return partial(self.__async_remove_listener_internal, target_event_id)
def __async_remove_listener_internal(self, listener_id: str):
del self._listeners[listener_id]
@callback
def async_handle_state_data(self, data: dict[str, HomeLinkEventData]):
"""Notify listeners."""
for button_id, event in data.items():
if listener := self._listeners.get(button_id):
listener(event)
async def async_config_entry_first_refresh(self) -> None:
"""Refresh data for the first time when a config entry is setup."""
await self._async_setup()
async def async_on_unload(self, _event):
"""Disconnect and unregister when unloaded."""
await self.provider.disable()
async def _async_setup(self) -> None:
"""Set up the coordinator."""
await self.provider.enable(get_default_context())
await self.discover_devices()
self.provider.listen(self.on_message)
async def discover_devices(self):
"""Discover devices and build the Entities."""
self.device_data = await self.provider.discover()
def on_message(
self: HomeLinkCoordinator, _topic: str, message: HomeLinkMQTTMessage
):
"MQTT Callback function."
if message["type"] == "state":
self.hass.add_job(self.async_handle_state_data, message["data"])
if message["type"] == "requestSync":
self.hass.add_job(
self.hass.config_entries.async_reload,
self.config_entry.entry_id,
)

View File

@@ -0,0 +1,83 @@
"""Platform for Event integration."""
from __future__ import annotations
from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, EVENT_PRESSED
from .coordinator import HomeLinkCoordinator, HomeLinkEventData
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the entities for the binary sensor."""
coordinator = config_entry.runtime_data.coordinator
for device in coordinator.device_data:
buttons = [
HomeLinkEventEntity(b.id, b.name, device.id, device.name, coordinator)
for b in device.buttons
]
coordinator.buttons.extend(buttons)
async_add_entities(coordinator.buttons)
# Updates are centralized by the coordinator.
PARALLEL_UPDATES = 0
class HomeLinkEventEntity(EventEntity):
"""Event Entity."""
_attr_has_entity_name = True
_attr_event_types = [EVENT_PRESSED]
_attr_device_class = EventDeviceClass.BUTTON
def __init__(
self,
id: str,
param_name: str,
device_id: str,
device_name: str,
coordinator: HomeLinkCoordinator,
) -> None:
"""Initialize the event entity."""
self.id: str = id
self._attr_name: str = param_name
self._attr_unique_id: str = id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=device_name,
)
self.coordinator = coordinator
self.last_request_id: str | None = None
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.async_add_event_listener(
self._handle_event_data_update, self.id
)
)
@callback
def _handle_event_data_update(self, update_data: HomeLinkEventData) -> None:
"""Update this button."""
if update_data["requestId"] != self.last_request_id:
self._trigger_event(EVENT_PRESSED)
self.last_request_id = update_data["requestId"]
self.async_write_ha_state()
async def async_update(self):
"""Request early polling. Left intentionally blank because it's not possible in this implementation."""

View File

@@ -0,0 +1,11 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.1"]
}

View File

@@ -0,0 +1,114 @@
"""API for homelink bound to Home Assistant OAuth."""
from json import JSONDecodeError
import logging
import time
from typing import cast
from aiohttp import ClientError, ClientSession
from homelink.auth.abstract_auth import AbstractAuth
from homelink.settings import COGNITO_CLIENT_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import OAUTH2_TOKEN
_LOGGER = logging.getLogger(__name__)
class SRPAuthImplementation(config_entry_oauth2_flow.AbstractOAuth2Implementation):
"""Base class to abstract OAuth2 authentication."""
def __init__(self, hass: HomeAssistant, domain) -> None:
"""Initialize the SRP Auth implementation."""
self.hass = hass
self._domain = domain
self.client_id = COGNITO_CLIENT_ID
@property
def name(self) -> str:
"""Name of the implementation."""
return "SRPAuth"
@property
def domain(self) -> str:
"""Domain that is providing the implementation."""
return self._domain
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Left intentionally blank because the auth is handled by SRP."""
return ""
async def async_resolve_external_data(self, external_data) -> dict:
"""Format the token from the source appropriately for HomeAssistant."""
tokens = external_data["tokens"]
new_token = {}
new_token["access_token"] = tokens["AuthenticationResult"]["AccessToken"]
new_token["refresh_token"] = tokens["AuthenticationResult"]["RefreshToken"]
new_token["token_type"] = tokens["AuthenticationResult"]["TokenType"]
new_token["expires_in"] = tokens["AuthenticationResult"]["ExpiresIn"]
new_token["expires_at"] = (
time.time() + tokens["AuthenticationResult"]["ExpiresIn"]
)
return new_token
async def _token_request(self, data: dict) -> dict:
"""Make a token request."""
session = async_get_clientsession(self.hass)
data["client_id"] = self.client_id
_LOGGER.debug("Sending token request to %s", OAUTH2_TOKEN)
resp = await session.post(OAUTH2_TOKEN, data=data)
if resp.status >= 400:
try:
error_response = await resp.json()
except (ClientError, JSONDecodeError):
error_response = {}
error_code = error_response.get("error", "unknown")
error_description = error_response.get(
"error_description", "unknown error"
)
_LOGGER.error(
"Token request for %s failed (%s): %s",
self.domain,
error_code,
error_description,
)
resp.raise_for_status()
return cast(dict, await resp.json())
async def _async_refresh_token(self, token: dict) -> dict:
"""Refresh tokens."""
new_token = await self._token_request(
{
"grant_type": "refresh_token",
"client_id": self.client_id,
"refresh_token": token["refresh_token"],
}
)
return {**token, **new_token}
class AsyncConfigEntryAuth(AbstractAuth):
"""Provide homelink authentication tied to an OAuth2 based config entry."""
def __init__(
self,
websession: ClientSession,
oauth_session: config_entry_oauth2_flow.OAuth2Session,
) -> None:
"""Initialize homelink auth."""
super().__init__(websession)
self._oauth_session = oauth_session
async def async_get_access_token(self) -> str:
"""Return a valid access token."""
if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"]

View File

@@ -0,0 +1,76 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register any service actions
appropriate-polling:
status: exempt
comment: Integration does not poll
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register any service actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register any service actions
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: It is not necessary to update IP addresses of devices or services in this Integration
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: done
entity-category: todo
entity-device-class: todo
entity-disabled-by-default:
status: exempt
comment: Entities are not noisy and are expected to be enabled by default
entity-translations:
status: exempt
comment: Entity properties are user-defined, and therefore cannot be translated
exception-translations: todo
icon-translations:
status: exempt
comment: Entities in this integration do not use icons, and therefore do not require translation
reconfiguration-flow: todo
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,38 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"error": {
"srp_auth_failed": "Error authenticating HomeLink account",
"unknown": "An unknown error occurred. Please try again later"
},
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Email address associated with your HomeLink account",
"password": "Password associated with your HomeLink account"
}
}
}
}
}

View File

@@ -149,6 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
),
supports_response=SupportsResponse.ONLY,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)
return True

View File

@@ -23,7 +23,7 @@ CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image-preview"
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_TOP_P = "top_p"

View File

@@ -162,7 +162,7 @@
"fields": {
"filenames": {
"description": "Attachments to add to the prompt (images, PDFs, etc)",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Attachment filenames"
},
"prompt": {

View File

@@ -159,4 +159,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -92,7 +92,7 @@
},
"filename": {
"description": "Path to the image or video to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

@@ -1,9 +1,11 @@
"""The Growatt server PV inverter sensor integration."""
from collections.abc import Mapping
from json import JSONDecodeError
import logging
import growattServer
from requests import RequestException
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
@@ -35,8 +37,7 @@ def get_device_list_classic(
# Log in to api and fetch first plant if no plant id is defined.
try:
login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD])
# DEBUG: Log the actual response structure
except Exception as ex:
except (RequestException, JSONDecodeError) as ex:
raise ConfigEntryError(
f"Error communicating with Growatt API during login: {ex}"
) from ex
@@ -53,7 +54,7 @@ def get_device_list_classic(
if plant_id == DEFAULT_PLANT_ID:
try:
plant_info = api.plant_list(user_id)
except Exception as ex:
except (RequestException, JSONDecodeError) as ex:
raise ConfigEntryError(
f"Error communicating with Growatt API during plant list: {ex}"
) from ex
@@ -64,7 +65,7 @@ def get_device_list_classic(
# Get a list of devices for specified plant to add sensors for.
try:
devices = api.device_list(plant_id)
except Exception as ex:
except (RequestException, JSONDecodeError) as ex:
raise ConfigEntryError(
f"Error communicating with Growatt API during device list: {ex}"
) from ex

View File

@@ -28,9 +28,7 @@ rules:
status: todo
comment: Update server URL dropdown to show regional descriptions (e.g., 'China', 'United States') instead of raw URLs.
docs-installation-parameters: todo
entity-unavailable:
status: todo
comment: Replace bare Exception catches in __init__.py with specific growattServer exceptions.
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done

View File

@@ -111,7 +111,7 @@ async def async_migrate_entry(
hass: HomeAssistant, entry: HomeConnectConfigEntry
) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", entry.version)
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
match entry.minor_version:
@@ -147,5 +147,7 @@ async def async_migrate_entry(
)["sub"],
)
_LOGGER.debug("Migration to version %s successful", entry.version)
_LOGGER.debug(
"Migration to version %s.%s successful", entry.version, entry.minor_version
)
return True

View File

@@ -22,6 +22,7 @@ from homeassistant.components.cover import (
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER,
CoverDeviceClass,
)
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -38,6 +39,7 @@ from homeassistant.components.valve import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_SET_VALVE_POSITION,
SERVICE_STOP_VALVE,
ValveDeviceClass,
)
from homeassistant.const import (
@@ -143,6 +145,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
NevermindIntentHandler(),
)
intent.async_register(hass, SetPositionIntentHandler())
intent.async_register(hass, StopMovingIntentHandler())
intent.async_register(hass, StartTimerIntentHandler())
intent.async_register(hass, CancelTimerIntentHandler())
intent.async_register(hass, CancelAllTimersIntentHandler())
@@ -433,6 +436,31 @@ class SetPositionIntentHandler(intent.DynamicServiceIntentHandler):
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
class StopMovingIntentHandler(intent.DynamicServiceIntentHandler):
"""Intent handler for stopping covers and valves."""
def __init__(self) -> None:
"""Create stop moving handler."""
super().__init__(
intent.INTENT_STOP_MOVING,
description="Stops a moving device or entity",
platforms={COVER_DOMAIN, VALVE_DOMAIN},
device_classes={CoverDeviceClass, ValveDeviceClass},
)
def get_domain_and_service(
self, intent_obj: intent.Intent, state: State
) -> tuple[str, str]:
"""Get the domain and service name to call."""
if state.domain == COVER_DOMAIN:
return (COVER_DOMAIN, SERVICE_STOP_COVER)
if state.domain == VALVE_DOMAIN:
return (VALVE_DOMAIN, SERVICE_STOP_VALVE)
raise intent.IntentHandleError(f"Domain not supported: {state.domain}")
class GetCurrentDateIntentHandler(intent.IntentHandler):
"""Gets the current date."""

View File

@@ -7,11 +7,10 @@ in the Home Assistant Labs UI for users to enable or disable.
from __future__ import annotations
from collections.abc import Callable
import logging
from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.generated.labs import LABS_PREVIEW_FEATURES
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.storage import Store
@@ -19,6 +18,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_custom_components
from .const import DOMAIN, LABS_DATA, STORAGE_KEY, STORAGE_VERSION
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import (
EventLabsUpdatedData,
LabPreviewFeature,
@@ -135,55 +135,3 @@ async def _async_scan_all_preview_features(
_LOGGER.debug("Loaded %d total lab preview features", len(preview_features))
return preview_features
@callback
def async_is_preview_feature_enabled(
hass: HomeAssistant, domain: str, preview_feature: str
) -> bool:
"""Check if a lab preview feature is enabled.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
Returns:
True if the preview feature is enabled, False otherwise
"""
if LABS_DATA not in hass.data:
return False
labs_data = hass.data[LABS_DATA]
return (domain, preview_feature) in labs_data.data.preview_feature_status
@callback
def async_listen(
hass: HomeAssistant,
domain: str,
preview_feature: str,
listener: Callable[[], None],
) -> Callable[[], None]:
"""Listen for changes to a specific preview feature.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
listener: Callback to invoke when the preview feature is toggled
Returns:
Callable to unsubscribe from the listener
"""
@callback
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
"""Handle labs feature update event."""
if (
event.data["domain"] == domain
and event.data["preview_feature"] == preview_feature
):
listener()
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)

View File

@@ -0,0 +1,63 @@
"""Helper functions for the Home Assistant Labs integration."""
from __future__ import annotations
from collections.abc import Callable
from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import Event, HomeAssistant, callback
from .const import LABS_DATA
from .models import EventLabsUpdatedData
@callback
def async_is_preview_feature_enabled(
hass: HomeAssistant, domain: str, preview_feature: str
) -> bool:
"""Check if a lab preview feature is enabled.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
Returns:
True if the preview feature is enabled, False otherwise
"""
if LABS_DATA not in hass.data:
return False
labs_data = hass.data[LABS_DATA]
return (domain, preview_feature) in labs_data.data.preview_feature_status
@callback
def async_listen(
hass: HomeAssistant,
domain: str,
preview_feature: str,
listener: Callable[[], None],
) -> Callable[[], None]:
"""Listen for changes to a specific preview feature.
Args:
hass: HomeAssistant instance
domain: Integration domain
preview_feature: Preview feature name
listener: Callback to invoke when the preview feature is toggled
Returns:
Callable to unsubscribe from the listener
"""
@callback
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
"""Handle labs feature update event."""
if (
event.data["domain"] == domain
and event.data["preview_feature"] == preview_feature
):
listener()
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)

View File

@@ -12,6 +12,7 @@ from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import HomeAssistant, callback
from .const import LABS_DATA
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import EventLabsUpdatedData
@@ -20,6 +21,7 @@ def async_setup(hass: HomeAssistant) -> None:
"""Set up the number websocket API."""
websocket_api.async_register_command(hass, websocket_list_preview_features)
websocket_api.async_register_command(hass, websocket_update_preview_feature)
websocket_api.async_register_command(hass, websocket_subscribe_feature)
@callback
@@ -108,3 +110,52 @@ async def websocket_update_preview_feature(
hass.bus.async_fire(EVENT_LABS_UPDATED, event_data)
connection.send_result(msg["id"])
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "labs/subscribe",
vol.Required("domain"): str,
vol.Required("preview_feature"): str,
}
)
def websocket_subscribe_feature(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to a specific lab preview feature updates."""
domain = msg["domain"]
preview_feature_key = msg["preview_feature"]
labs_data = hass.data[LABS_DATA]
preview_feature_id = f"{domain}.{preview_feature_key}"
if preview_feature_id not in labs_data.preview_features:
connection.send_error(
msg["id"],
websocket_api.ERR_NOT_FOUND,
f"Preview feature {preview_feature_id} not found",
)
return
preview_feature = labs_data.preview_features[preview_feature_id]
@callback
def send_event() -> None:
"""Send feature state to client."""
enabled = async_is_preview_feature_enabled(hass, domain, preview_feature_key)
connection.send_message(
websocket_api.event_message(
msg["id"],
preview_feature.to_dict(enabled=enabled),
)
)
connection.subscriptions[msg["id"]] = async_listen(
hass, domain, preview_feature_key, send_event
)
connection.send_result(msg["id"])
send_event()

View File

@@ -108,6 +108,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_MESSAGE,
_async_service_message,
schema=SERVICE_MESSAGE_SCHEMA,
description_placeholders={"icons_url": "https://developer.lametric.com/icons"},
)

View File

@@ -211,7 +211,7 @@
"name": "[%key:common::config_flow::data::device%]"
},
"icon": {
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: https://developer.lametric.com/icons.",
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: {icons_url}.",
"name": "Icon ID"
},
"icon_type": {

View File

@@ -52,7 +52,7 @@ class StateConditionBase(Condition):
self, hass: HomeAssistant, config: ConditionConfig, state: str
) -> None:
"""Initialize condition."""
self._hass = hass
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.target
assert config.options

View File

@@ -30,9 +30,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: Devices don't require authentication
reauthentication-flow: done
test-coverage: todo
# Gold

View File

@@ -81,6 +81,9 @@ async def async_setup_entry(
SERVICE_PUBLISH,
SERVICE_PUBLISH_SCHEMA,
"publish",
description_placeholders={
"markdown_guide_url": "https://www.markdownguide.org/basic-syntax/"
},
)

View File

@@ -345,7 +345,7 @@
"name": "Icon URL"
},
"markdown": {
"description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/.",
"description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: {markdown_guide_url}.",
"name": "Format as Markdown"
},
"message": {

View File

@@ -129,4 +129,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -156,7 +156,7 @@
},
"filename": {
"description": "Path to the file to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["oralb_ble"],
"requirements": ["oralb-ble==0.17.6"]
"requirements": ["oralb-ble==1.0.2"]
}

View File

@@ -25,6 +25,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -101,7 +102,18 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
except OSError:
_LOGGER.error("Pilight send failed for %s", str(message_data))
hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
def _register_service() -> None:
hass.services.async_register(
DOMAIN,
SERVICE_NAME,
send_code,
schema=RF_CODE_SCHEMA,
description_placeholders={
"pilight_protocols_docs_url": "https://manual.pilight.org/protocols/index.html"
},
)
run_callback_threadsafe(hass.loop, _register_service).result()
# Publish received codes on the HA event bus
# A whitelist of codes to be published in the event bus

View File

@@ -4,7 +4,7 @@
"description": "Sends RF code to Pilight device.",
"fields": {
"protocol": {
"description": "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports.",
"description": "Protocol that Pilight recognizes. See {pilight_protocols_docs_url} for supported protocols and additional parameters that each protocol supports.",
"name": "Protocol"
}
},

View File

@@ -54,8 +54,11 @@ from .const import (
)
from .coordinator import RainMachineDataUpdateCoordinator
DEFAULT_SSL = True
API_URL_REFERENCE = (
"https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post"
)
DEFAULT_SSL = True
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -455,7 +458,15 @@ async def async_setup_entry( # noqa: C901
):
if hass.services.has_service(DOMAIN, service_name):
continue
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
hass.services.async_register(
DOMAIN,
service_name,
method,
schema=schema,
description_placeholders={
"api_url": API_URL_REFERENCE,
},
)
return True

View File

@@ -128,7 +128,7 @@
"name": "Push flow meter data"
},
"push_weather_data": {
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.",
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: {api_url}",
"fields": {
"condition": {
"description": "Current weather condition code (WNUM).",

View File

@@ -422,6 +422,8 @@ class ReolinkHost:
"name": self._api.nvr_name,
"base_url": self._base_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
"example_ip": "192.168.1.10",
"example_url": "http://192.168.1.10:8123",
},
)
@@ -436,6 +438,8 @@ class ReolinkHost:
translation_placeholders={
"base_url": self._base_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
"example_ip": "192.168.1.10",
"example_url": "http://192.168.1.10:8123",
},
)
else:

View File

@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.17.0"]
"requirements": ["reolink-aio==0.17.1"]
}

View File

@@ -1004,7 +1004,7 @@
"title": "Reolink firmware update required"
},
"https_webhook": {
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device",
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `{example_url}` where `{example_ip}` is the IP of the Home Assistant device",
"title": "Reolink webhook URL uses HTTPS (SSL)"
},
"password_too_long": {
@@ -1016,7 +1016,7 @@
"title": "Reolink incompatible with global SSL certificate"
},
"webhook_url": {
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `{example_url}` where `{example_ip}` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
"title": "Reolink webhook URL unreachable"
}
},

View File

@@ -1,20 +1,23 @@
"""The Rituals Perfume Genie integration."""
import asyncio
import logging
import aiohttp
from pyrituals import Account, Diffuser
from aiohttp import ClientError, ClientResponseError
from pyrituals import Account, AuthenticationException, Diffuser
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_HASH, DOMAIN, UPDATE_INTERVAL
from .coordinator import RitualsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
@@ -26,12 +29,38 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Rituals Perfume Genie from a config entry."""
# Initiate reauth for old config entries which don't have username / password in the entry data
if CONF_EMAIL not in entry.data or CONF_PASSWORD not in entry.data:
raise ConfigEntryAuthFailed("Missing credentials")
session = async_get_clientsession(hass)
account = Account(session=session, account_hash=entry.data[ACCOUNT_HASH])
account = Account(
email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
# Authenticate first so API token/cookies are available for subsequent calls
await account.authenticate()
account_devices = await account.get_devices()
except aiohttp.ClientError as err:
except AuthenticationException as err:
# Credentials invalid/expired -> raise AuthFailed to trigger reauth flow
raise ConfigEntryAuthFailed(err) from err
except ClientResponseError as err:
_LOGGER.debug(
"HTTP error during Rituals setup: status=%s, url=%s, headers=%s",
err.status,
err.request_info,
dict(err.headers or {}),
)
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
# Migrate old unique_ids to the new format
@@ -45,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Create a coordinator for each diffuser
coordinators = {
diffuser.hublot: RitualsDataUpdateCoordinator(
hass, entry, diffuser, update_interval
hass, entry, account, diffuser, update_interval
)
for diffuser in account_devices
}
@@ -106,3 +135,14 @@ def async_migrate_entities_unique_ids(
registry_entry.entity_id,
new_unique_id=f"{diffuser.hublot}-{new_unique_id}",
)
# Migration helpers for API v2
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate config entry to version 2: drop legacy ACCOUNT_HASH and bump version."""
if entry.version < 2:
data = dict(entry.data)
data.pop(ACCOUNT_HASH, None)
hass.config_entries.async_update_entry(entry, data=data, version=2)
return True
return True

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
import logging
from typing import Any
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any
from aiohttp import ClientResponseError
from aiohttp import ClientError
from pyrituals import Account, AuthenticationException
import voluptuous as vol
@@ -13,9 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_HASH, DOMAIN
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
@@ -28,39 +26,88 @@ DATA_SCHEMA = vol.Schema(
class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Rituals Perfume Genie."""
VERSION = 1
VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
errors = {}
session = async_get_clientsession(self.hass)
account = Account(user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session)
try:
await account.authenticate()
except ClientResponseError:
_LOGGER.exception("Unexpected response")
errors["base"] = "cannot_connect"
except AuthenticationException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(account.email)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account.email,
data={ACCOUNT_HASH: account.account_hash},
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
account = Account(
email=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=session,
)
try:
await account.authenticate()
except AuthenticationException:
errors["base"] = "invalid_auth"
except ClientError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data=user_input,
)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Rituals."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Form to log in again."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if TYPE_CHECKING:
assert reauth_entry.unique_id is not None
if user_input:
session = async_get_clientsession(self.hass)
account = Account(
email=reauth_entry.unique_id,
password=user_input[CONF_PASSWORD],
session=session,
)
try:
await account.authenticate()
except AuthenticationException:
errors["base"] = "invalid_auth"
except ClientError:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data={
CONF_EMAIL: reauth_entry.unique_id,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
reauth_entry.data,
),
errors=errors,
)

View File

@@ -4,6 +4,7 @@ from datetime import timedelta
DOMAIN = "rituals_perfume_genie"
# Old (API V1)
ACCOUNT_HASH = "account_hash"
# The API provided by Rituals is currently rate limited to 30 requests

View File

@@ -3,11 +3,13 @@
from datetime import timedelta
import logging
from pyrituals import Diffuser
from aiohttp import ClientError, ClientResponseError
from pyrituals import Account, AuthenticationException, Diffuser
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -23,10 +25,12 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
account: Account,
diffuser: Diffuser,
update_interval: timedelta,
) -> None:
"""Initialize global Rituals Perfume Genie data updater."""
self.account = account
self.diffuser = diffuser
super().__init__(
hass,
@@ -37,5 +41,36 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
)
async def _async_update_data(self) -> None:
"""Fetch data from Rituals."""
await self.diffuser.update_data()
"""Fetch data from Rituals, with one silent re-auth on 401.
If silent re-auth also fails, raise ConfigEntryAuthFailed to trigger reauth flow.
Other HTTP/network errors are wrapped in UpdateFailed so HA can retry.
"""
try:
await self.diffuser.update_data()
except (AuthenticationException, ClientResponseError) as err:
# Treat 401/403 like AuthenticationException → one silent re-auth, single retry
if isinstance(err, ClientResponseError) and (status := err.status) not in (
401,
403,
):
# Non-auth HTTP error → let HA retry
raise UpdateFailed(f"HTTP {status}") from err
self.logger.debug(
"Auth issue detected (%r). Attempting silent re-auth.", err
)
try:
await self.account.authenticate()
await self.diffuser.update_data()
except AuthenticationException as err2:
# Credentials invalid → trigger HA reauth
raise ConfigEntryAuthFailed from err2
except ClientResponseError as err2:
# Still HTTP auth errors after refresh → trigger HA reauth
if err2.status in (401, 403):
raise ConfigEntryAuthFailed from err2
raise UpdateFailed(f"HTTP {err2.status}") from err2
except ClientError as err:
# Network issues (timeouts, DNS, etc.)
raise UpdateFailed(f"Network error: {err!r}") from err

View File

@@ -1,10 +1,10 @@
{
"domain": "rituals_perfume_genie",
"name": "Rituals Perfume Genie",
"codeowners": ["@milanmeu", "@frenck"],
"codeowners": ["@milanmeu", "@frenck", "@quebulm"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
"iot_class": "cloud_polling",
"loggers": ["pyrituals"],
"requirements": ["pyrituals==0.0.6"]
"requirements": ["pyrituals==0.0.7"]
}

View File

@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,12 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "Please enter the correct password."
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==3.9.3",
"python-roborock==3.10.2",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

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

View File

@@ -44,6 +44,7 @@ from .entity import (
)
from .utils import (
async_remove_orphaned_entities,
async_remove_shelly_entity,
format_ble_addr,
get_blu_trv_device_info,
get_device_entry_gen,
@@ -80,6 +81,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_action="trigger_reboot",
supported=lambda coordinator: coordinator.sleep_period == 0,
),
ShellyButtonDescription[ShellyBlockCoordinator](
key="self_test",
@@ -197,7 +199,8 @@ async def async_setup_entry(
"""Set up button entities."""
entry_data = config_entry.runtime_data
coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None
if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
device_gen = get_device_entry_gen(config_entry)
if device_gen in RPC_GENERATIONS:
coordinator = entry_data.rpc
else:
coordinator = entry_data.block
@@ -210,6 +213,12 @@ async def async_setup_entry(
hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator)
)
# Remove the 'restart' button for sleeping devices as it was mistakenly
# added in https://github.com/home-assistant/core/pull/154673
entry_sleep_period = config_entry.data[CONF_SLEEP_PERIOD]
if device_gen in RPC_GENERATIONS and entry_sleep_period:
async_remove_shelly_entity(hass, BUTTON_PLATFORM, f"{coordinator.mac}-reboot")
entities: list[ShellyButton] = []
entities.extend(
@@ -224,7 +233,7 @@ async def async_setup_entry(
return
# add RPC buttons
if config_entry.data[CONF_SLEEP_PERIOD]:
if entry_sleep_period:
async_setup_entry_rpc(
hass,
config_entry,

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["solarlog_cli"],
"quality_scale": "platinum",
"requirements": ["solarlog_cli==0.6.0"]
"requirements": ["solarlog_cli==0.6.1"]
}

View File

@@ -10,17 +10,26 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SqueezeboxConfigEntry
from .const import STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_RESCAN
from .entity import LMSStatusEntity
from . import SqueezeboxConfigEntry, SqueezeBoxPlayerUpdateCoordinator
from .const import (
PLAYER_SENSOR_ALARM_ACTIVE,
PLAYER_SENSOR_ALARM_SNOOZE,
PLAYER_SENSOR_ALARM_UPCOMING,
SIGNAL_PLAYER_DISCOVERED,
STATUS_SENSOR_NEEDSRESTART,
STATUS_SENSOR_RESCAN,
)
from .entity import LMSStatusEntity, SqueezeboxEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
SENSORS: tuple[BinarySensorEntityDescription, ...] = (
SERVER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key=STATUS_SENSOR_RESCAN,
device_class=BinarySensorDeviceClass.RUNNING,
@@ -32,6 +41,23 @@ SENSORS: tuple[BinarySensorEntityDescription, ...] = (
),
)
PLAYER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_UPCOMING,
translation_key=PLAYER_SENSOR_ALARM_UPCOMING,
),
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_ACTIVE,
translation_key=PLAYER_SENSOR_ALARM_ACTIVE,
device_class=BinarySensorDeviceClass.RUNNING,
),
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_SNOOZE,
translation_key=PLAYER_SENSOR_ALARM_SNOOZE,
device_class=BinarySensorDeviceClass.RUNNING,
),
)
_LOGGER = logging.getLogger(__name__)
@@ -42,9 +68,29 @@ async def async_setup_entry(
) -> None:
"""Platform setup using common elements."""
@callback
def _player_discovered(
player_coordinator: SqueezeBoxPlayerUpdateCoordinator,
) -> None:
_LOGGER.debug(
"Setting up binary sensor entities for player %s, model %s",
player_coordinator.player.name,
player_coordinator.player.model,
)
async_add_entities(
SqueezeboxBinarySensorEntity(player_coordinator, description)
for description in PLAYER_SENSORS
)
entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_PLAYER_DISCOVERED}{entry.entry_id}", _player_discovered
)
)
async_add_entities(
ServerStatusBinarySensor(entry.runtime_data.coordinator, description)
for description in SENSORS
for description in SERVER_SENSORS
)
@@ -55,3 +101,24 @@ class ServerStatusBinarySensor(LMSStatusEntity, BinarySensorEntity):
def is_on(self) -> bool:
"""LMS Status directly from coordinator data."""
return bool(self.coordinator.data[self.entity_description.key])
class SqueezeboxBinarySensorEntity(SqueezeboxEntity, BinarySensorEntity):
"""Representation of player based binary sensors."""
description: BinarySensorEntityDescription
def __init__(
self,
coordinator: SqueezeBoxPlayerUpdateCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the SqueezeBox sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{format_mac(self._player.player_id)}_{description.key}"
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
return getattr(self.coordinator.player, self.entity_description.key, None)

View File

@@ -19,6 +19,9 @@ STATUS_SENSOR_INFO_TOTAL_GENRES = "info total genres"
STATUS_SENSOR_INFO_TOTAL_SONGS = "info total songs"
STATUS_SENSOR_PLAYER_COUNT = "player count"
STATUS_SENSOR_OTHER_PLAYER_COUNT = "other player count"
PLAYER_SENSOR_ALARM_UPCOMING = "alarm_upcoming"
PLAYER_SENSOR_ALARM_SNOOZE = "alarm_snooze"
PLAYER_SENSOR_ALARM_ACTIVE = "alarm_active"
STATUS_QUERY_LIBRARYNAME = "libraryname"
STATUS_QUERY_MAC = "mac"
STATUS_QUERY_UUID = "uuid"

View File

@@ -41,6 +41,15 @@
},
"entity": {
"binary_sensor": {
"alarm_active": {
"name": "Alarm active"
},
"alarm_snooze": {
"name": "Alarm snoozed"
},
"alarm_upcoming": {
"name": "Alarm upcoming"
},
"needsrestart": {
"name": "Needs restart"
},

View File

@@ -72,7 +72,6 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
def _get_starlink_data(self) -> StarlinkData:
"""Retrieve Starlink data."""
context = self.channel_context
status = status_data(context)
location = location_data(context)
sleep = get_sleep_config(context)
status, obstruction, alert = status_data(context)

View File

@@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import now
from homeassistant.util.variance import ignore_variance
from .coordinator import StarlinkConfigEntry, StarlinkData
from .entity import StarlinkEntity
@@ -91,6 +92,10 @@ class StarlinkAccumulationSensor(StarlinkSensorEntity, RestoreSensor):
self._attr_native_value = last_native_value
uptime_to_stable_datetime = ignore_variance(
lambda value: now() - timedelta(seconds=value), timedelta(minutes=1)
)
SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
StarlinkSensorEntityDescription(
key="ping",
@@ -150,9 +155,7 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
translation_key="last_restart",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: (
now() - timedelta(seconds=data.status["uptime"], milliseconds=-500)
).replace(microsecond=0),
value_fn=lambda data: uptime_to_stable_datetime(data.status["uptime"]),
entity_class=StarlinkSensorEntity,
),
StarlinkSensorEntityDescription(

View File

@@ -150,6 +150,7 @@ class SunCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config)
assert config.options is not None
self._options = config.options

View File

@@ -524,6 +524,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_send_telegram_message,
schema=schema,
supports_response=supports_response,
description_placeholders={
"formatting_options_url": "https://core.telegram.org/bots/api#formatting-options"
},
)
return True

View File

@@ -64,6 +64,12 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
"botfather_username": "@BotFather",
"botfather_url": "https://t.me/botfather",
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
}
STEP_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
{
vol.Required(CONF_PLATFORM): SelectSelector(
@@ -310,10 +316,7 @@ class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle a flow to create a new config entry for a Telegram bot."""
description_placeholders: dict[str, str] = {
"botfather_username": "@BotFather",
"botfather_url": "https://t.me/botfather",
}
description_placeholders: dict[str, str] = DESCRIPTION_PLACEHOLDERS.copy()
if not user_input:
return self.async_show_form(
step_id="user",
@@ -552,13 +555,14 @@ class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
},
},
),
description_placeholders=DESCRIPTION_PLACEHOLDERS,
)
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
CONF_PROXY_URL
)
errors: dict[str, str] = {}
description_placeholders: dict[str, str] = {}
description_placeholders: dict[str, str] = DESCRIPTION_PLACEHOLDERS.copy()
user_input[CONF_API_KEY] = api_key
bot_name = await self._validate_bot(

View File

@@ -60,7 +60,7 @@
"proxy_url": "Proxy URL"
},
"data_description": {
"proxy_url": "Proxy URL if working behind one, optionally including username and password.\n(socks5://username:password@proxy_ip:proxy_port)"
"proxy_url": "Proxy URL if working behind one, optionally including username and password.\n({socks_url})"
},
"name": "Advanced settings"
}
@@ -400,7 +400,7 @@
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
},
"caption": {
"description": "The title of the media.",
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
},
"chat_id": {
@@ -499,7 +499,7 @@
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
},
"caption": {
"description": "The title of the animation.",
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
},
"config_entry_id": {
@@ -600,7 +600,7 @@
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
},
"caption": {
"description": "The title of the document.",
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
},
"config_entry_id": {
@@ -745,7 +745,7 @@
"name": "Keyboard"
},
"message": {
"description": "Message body of the notification.",
"description": "Message body of the notification.\nCan't parse entities? Format your message according to the [formatting options]({formatting_options_url}).",
"name": "Message"
},
"message_tag": {
@@ -757,7 +757,7 @@
"name": "Message thread ID"
},
"parse_mode": {
"description": "Parser for the message text.",
"description": "Parser for the message text.\nSee [formatting options]({formatting_options_url}) for more details.",
"name": "Parse mode"
},
"reply_to_message_id": {
@@ -787,7 +787,7 @@
"name": "Authentication method"
},
"caption": {
"description": "The title of the image.",
"description": "The title of the media.\nCan't parse entities? Format your message according to the [formatting options]({formatting_options_url}).",
"name": "Caption"
},
"config_entry_id": {
@@ -991,7 +991,7 @@
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
},
"caption": {
"description": "The title of the video.",
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
},
"config_entry_id": {
@@ -1070,7 +1070,7 @@
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
},
"caption": {
"description": "The title of the voice message.",
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
},
"config_entry_id": {

View File

@@ -1,7 +1,7 @@
"""Helpers for template integration."""
from collections.abc import Callable
from enum import Enum
from enum import StrEnum
import hashlib
import itertools
import logging
@@ -33,6 +33,7 @@ from homeassistant.helpers.entity_platform import (
async_get_platforms,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import yaml as yaml_util
@@ -190,12 +191,12 @@ def async_create_template_tracking_entities(
async_add_entities(entities)
def _format_template(value: Any) -> Any:
def _format_template(value: Any, field: str | None = None) -> Any:
if isinstance(value, template.Template):
return value.template
if isinstance(value, Enum):
return value.name
if isinstance(value, StrEnum):
return value.value
if isinstance(value, (int, float, str, bool)):
return value
@@ -207,14 +208,13 @@ def format_migration_config(
config: ConfigType | list[ConfigType], depth: int = 0
) -> ConfigType | list[ConfigType]:
"""Recursive method to format templates as strings from ConfigType."""
types = (dict, list)
if depth > 9:
raise RecursionError
if isinstance(config, list):
items = []
for item in config:
if isinstance(item, types):
if isinstance(item, (dict, list)):
if len(item) > 0:
items.append(format_migration_config(item, depth + 1))
else:
@@ -223,9 +223,18 @@ def format_migration_config(
formatted_config = {}
for field, value in config.items():
if isinstance(value, types):
if isinstance(value, dict):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
elif isinstance(value, list):
if len(value) > 0:
formatted_config[field] = format_migration_config(value, depth + 1)
else:
formatted_config[field] = []
elif isinstance(value, ScriptVariables):
formatted_config[field] = format_migration_config(
value.as_dict(), depth + 1
)
else:
formatted_config[field] = _format_template(value)
@@ -260,9 +269,9 @@ def create_legacy_template_issue(
try:
config.pop(CONF_PLATFORM, None)
modified_yaml = format_migration_config(config)
yaml_config = yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})
# Format to show up properly in a numbered bullet on the repair.
yaml_config = " ```\n " + yaml_config.replace("\n", "\n ") + "```"
yaml_config = (
f"```\n{yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})}\n```"
)
except RecursionError:
yaml_config = f"{DOMAIN}:\n - {domain}: - ..."
@@ -278,6 +287,7 @@ def create_legacy_template_issue(
"domain": domain,
"breadcrumb": breadcrumb,
"config": yaml_config,
"filename": "<filename>",
},
)

View File

@@ -635,14 +635,14 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
# Support legacy mireds in template light.
temperature = int(render)
if (min_kelvin := self._attr_min_color_temp_kelvin) is not None:
min_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
else:
min_mireds = DEFAULT_MIN_MIREDS
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
max_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
max_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
else:
max_mireds = DEFAULT_MAX_MIREDS
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
min_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
else:
min_mireds = DEFAULT_MIN_MIREDS
if min_mireds <= temperature <= max_mireds:
self._attr_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(temperature)
@@ -856,42 +856,36 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
try:
if render in (None, "None", ""):
self._attr_max_mireds = DEFAULT_MAX_MIREDS
self._attr_max_color_temp_kelvin = None
self._attr_min_color_temp_kelvin = None
return
self._attr_max_mireds = max_mireds = int(render)
self._attr_max_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(max_mireds)
self._attr_min_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(int(render))
)
except ValueError:
_LOGGER.exception(
"Template must supply an integer temperature within the range for"
" this light, or 'None'"
)
self._attr_max_mireds = DEFAULT_MAX_MIREDS
self._attr_max_color_temp_kelvin = None
self._attr_min_color_temp_kelvin = None
@callback
def _update_min_mireds(self, render):
"""Update the min mireds from the template."""
try:
if render in (None, "None", ""):
self._attr_min_mireds = DEFAULT_MIN_MIREDS
self._attr_min_color_temp_kelvin = None
self._attr_max_color_temp_kelvin = None
return
self._attr_min_mireds = min_mireds = int(render)
self._attr_min_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(min_mireds)
self._attr_max_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(int(render))
)
except ValueError:
_LOGGER.exception(
"Template must supply an integer temperature within the range for"
" this light, or 'None'"
)
self._attr_min_mireds = DEFAULT_MIN_MIREDS
self._attr_min_color_temp_kelvin = None
self._attr_max_color_temp_kelvin = None
@callback
def _update_supports_transition(self, render):

View File

@@ -529,7 +529,7 @@
"title": "Deprecated battery level option in {entity_name}"
},
"deprecated_legacy_templates": {
"description": "The legacy `platform: template` syntax for `{domain}` is being removed. Please migrate `{breadcrumb}` to the modern template syntax.\n\n1. Remove existing template definition.\n2. Add new template definition:\n{config}\n3. Restart Home Assistant or reload template entities.",
"description": "The legacy `platform: template` syntax for `{domain}` is being removed. Please migrate `{breadcrumb}` to the modern template syntax.\n#### Step 1 - Remove legacy configuration\nRemove the `{breadcrumb}` template definition from the `configuration.yaml` `{domain}:` section.\n\n**Note:** If you are using `{domain}: !include {filename}.yaml` in `configuration.yaml`, remove the {domain} definition from the included `{filename}.yaml`.\n#### Step 2 - Add the modern configuration\nAdd new template definition inside `configuration.yaml`:\n{config}\n**Note:** If there are any existing `template:` sections in your configuration, make sure to omit the `template:` line from the yaml above. There can only be 1 `template:` section in `configuration.yaml`. Also, ensure the indentation is aligned with the existing entities within the `template:` section.\n#### Step 3 - Restart Home Assistant or reload template entities",
"title": "Legacy {domain} template deprecation"
}
},

View File

@@ -325,6 +325,9 @@ def async_setup_services(hass: HomeAssistant) -> None:
vol.Required(ATTR_TOU_SETTINGS): dict,
}
),
description_placeholders={
"time_of_use_url": "https://developer.tesla.com/docs/fleet-api#time_of_use_settings"
},
)
async def add_charge_schedule(call: ServiceCall) -> None:

View File

@@ -1358,7 +1358,7 @@
"name": "Energy Site"
},
"tou_settings": {
"description": "See https://developer.tesla.com/docs/fleet-api#time_of_use_settings for details.",
"description": "See {time_use_url} for details.",
"name": "Settings"
}
},

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