Compare commits

...

635 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
TheJulianJES
3c0cfd5e0c Display error when forming new ZHA network fails (#157863) 2025-12-03 22:37:52 -05:00
Luke Lashley
69f66ffef4 Correctly pass MopParserConfig for Roborock (#157891) 2025-12-03 17:35:51 -08:00
Norbert Rittel
d2c3543b6c Consistently use "Labs" as name in kitchen_sink (#157875) 2025-12-04 00:19:24 +01:00
Franck Nijhof
ca4a2d441e 2025.12.0 (#157330) 2025-12-03 19:06:27 +01:00
Joost Lekkerkerker
f42fe9cee3 Add button to reset hood filter to SmartThings (#157847) 2025-12-03 18:38:23 +01:00
Kevin Stillhammer
b67873f40c bump fressnapftracker to 0.2.0 (#157868) 2025-12-03 18:29:20 +01:00
Markus Jacobsen
ecc08fce0f Reduce naming verbosity in Bang & Olufsen (#157825) 2025-12-03 17:46:18 +01:00
Ludovic BOUÉ
375f536b15 Add Matter DoorPositionSensor open/closed count sensors (#155809) 2025-12-03 17:35:47 +01:00
Kevin Stillhammer
5cff813eac remove deep_sleep binary_sensor from fressnapf_tracker (#157857) 2025-12-03 17:28:59 +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
Thomas55555
9129665c64 Fix strings in Google Air Quality (#157862) 2025-12-03 17:26:21 +01: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
Robert Resch
f4e11da1a6 Add retry logic to docker.io image push step (#157859) 2025-12-03 16:53:45 +01:00
Bram Kragten
e0238b5ab2 Update frontend to 20251203.0 (#157851) 2025-12-03 10:40:05 -05:00
Luke Lashley
352f3813e2 Bump Roborock to 3.9.3 (#157852) 2025-12-03 16:37:59 +01:00
Manu
b1399a5541 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 16:29:51 +01:00
J. Nick Koston
316cddec86 Bump bleak to 2.0.0 (#157766) 2025-12-03 15:25:42 +00:00
starkillerOG
2f71aec26f Bump reolink_aio to 0.17.0 (#157850) 2025-12-03 16:22:02 +01:00
Joost Lekkerkerker
aa72b76ee7 Add cooktop fixture to SmartThings (#157842) 2025-12-03 15:36:29 +01:00
Erik Montnemery
e009898107 Remove template config entry from source device (#157814)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 15:24:48 +01:00
Artur Pragacz
ceb13e70b9 Add integration type to wake_on_lan (#157726)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 15:14:57 +01:00
Robert Resch
498a80ac7f Bump deebot-client to 17.0.0 (#157836) 2025-12-03 15:12:31 +01: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
Artur Pragacz
1a60c46d67 Bump aioonkyo to 0.4.0 (#157838) 2025-12-03 14:46:52 +01:00
Matthias Alphart
62fba5ca20 Update xknx to 3.12.0 (#157835) 2025-12-03 14:40:40 +01:00
victorigualada
b54cde795c Bump hass-nabucasa from 1.6.2 to 1.7.0 (#157834) 2025-12-03 14:37:45 +01:00
victorigualada
0f456373bf Allow non strict response_format structures for Cloud LLM generation (#157822) 2025-12-03 14:31:09 +01:00
IAmStiven
a5042027b8 Add support for new ElevenLabs model Scribe v2 (#156961) 2025-12-03 14:29:25 +01: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
b15b5ba95c Add final learn more and feedback links for purpose-specific triggers and conditions preview feature (#157830) 2025-12-03 13:14:37 +01:00
Robert Resch
cd6e72798e Prioritize default stun port over alternative (#157829) 2025-12-03 13:14:28 +01:00
Kamil Breguła
739157e59f Simplify availability property in WLED (#157800)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-03 13:00:21 +01:00
torben-iometer
267aa1af42 bump iometer to v0.3.0 (#157826) 2025-12-03 12:47:05 +01:00
Michael
7328b61a69 Add integration_type to Oralb (#157828) 2025-12-03 12:46:50 +01: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
Allen Porter
203f2fb364 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 11:23:00 +01:00
Josef Zweck
b956c17ce4 Mark nordpool as service integration_type (#157810) 2025-12-03 11:22:42 +01:00
Marc Mueller
5163dc0567 Fix ping TypeError when killing the process (#157794) 2025-12-03 11:22:14 +01:00
Allen Porter
31a0478717 Bump python-roborock to 3.9.2 (#157815)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:56:56 +01:00
dependabot[bot]
24da3f0db8 Bump actions/checkout from 6.0.0 to 6.0.1 (#157806)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 10:38:45 +01:00
dependabot[bot]
786922fc5d Bump actions/stale from 10.1.0 to 10.1.1 (#157807)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 10:36:44 +01:00
Erik Montnemery
c2f8b6986b Pin Python point release used in CI (#157819) 2025-12-03 10:26:15 +01:00
hanwg
0a0832671f Fix bug in group notify entities when title is missing (#157171)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 09:44:01 +01:00
Aidan Timson
7b353d7ad4 Add levoit virtual integration (#157618) 2025-12-03 09:38:01 +01:00
epenet
99de73a729 Update SFR Box unit of measurement (#157813) 2025-12-03 08:46:59 +01:00
Joost Lekkerkerker
1995fbd252 Make occupancy trigger check occupancy instead of presence (#157791) 2025-12-03 08:15:31 +01:00
Kamil Breguła
315ea9dc76 Update release URL in WLED (#157801) 2025-12-03 05:55:03 +01:00
Josef Zweck
639a96f8cb La Marzocco add Bluetooth offline mode (#157011) 2025-12-03 05:53:27 +01:00
Stefan Agner
b6786c5a42 Add storage link to low disk space repair issue (#157786)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-12-02 22:14:24 -05:00
Kamil Breguła
6f6e9b8057 Add quality scale for WLED (#155482)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-03 01:19:02 +01:00
johanzander
e0c687e415 Remove extra logging in Growatt (#157788) 2025-12-03 00:38:03 +01:00
Abestanis
982362110c Allow to configure KNX time, date & datetime entities via UI (#157603) 2025-12-02 23:45:43 +01:00
Lukas
90dc3a8fdf Pooldose: add number platform (#157787) 2025-12-02 23:31:44 +01:00
J. Nick Koston
5112742b71 Bump habluetooth to 5.8.0 (#157771) 2025-12-02 15:55:37 -06:00
johanzander
8899bc01bd Add bronze quality scale to Growatt Server integration (#154649)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 22:36:51 +01: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
Joris Pelgröm
ed8f9105ff Bump letpot to 0.6.4 (#157781) 2025-12-02 22:12:41 +01:00
Michael Hansen
185de98f5e Bump hassil to 3.5.0 (#157780) 2025-12-02 22:11:00 +01:00
Paulus Schoutsen
e857abb43f Allow fetching the Cloud ICE servers (#157774) 2025-12-02 16:02:30 -05:00
Joost Lekkerkerker
5b1829f3a1 Add hood filter usage entity to SmartThings (#157775) 2025-12-02 21:45:17 +01:00
Kamil Breguła
520156a33a Handle unsupported version in WLED (#157778)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-02 21:20:36 +01:00
Kevin Stillhammer
e3b5342b76 use sentence casing in binary_sensor for fressnapf_tracker (#157772) 2025-12-02 21:06:18 +01:00
Josef Zweck
951b19e80c Add integration_type for tedee (#157776) 2025-12-02 21:04:51 +01:00
Sab44
e2351ecec2 Fix orphaned devices not being removed during integration startup (#155900) 2025-12-02 21:03:37 +01:00
Joost Lekkerkerker
d75e5498c6 Add health concern entities to SmartThings (#157773) 2025-12-02 21:00:50 +01:00
puddly
2dd58dbe39 Fix ZHA network formation (#157769) 2025-12-02 14:59:55 -05:00
Joost Lekkerkerker
4ef17799db Add snapshot test to Vivotek (#157767) 2025-12-02 20:47:02 +01:00
Joost Lekkerkerker
9373378350 Add fixture for hood to SmartThings (#157770) 2025-12-02 20:46:06 +01:00
Marcel van der Veldt
18833a194b Let AuthenticationRequired also trigger the reauth flow in MusicAssistant (#157580) 2025-12-02 14:22:40 -05:00
Kevin Stillhammer
2631c77bee add platform binary_sensor to fressnapf_tracker (#157753) 2025-12-02 20:05:34 +01:00
Kevin McCormack
c67247bf32 Add config flow for Vivotek integration (#154801)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 19:47:22 +01:00
Joost Lekkerkerker
18b5ffd365 Add SmartThings walloven fixtures (#157748) 2025-12-02 19:32:28 +01: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
Bram Kragten
c4e3a4d65e Update frontend to 20251202.0 (#157755)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-02 17:50:13 +01:00
victorigualada
84d2686517 Don't register Home Assistant Cloud LLM platforms if not logged in (#157630) 2025-12-02 17:47:08 +01:00
Michael Hansen
ae8980ce5b Bump intents to 2025.12.2 (#157758) 2025-12-02 17:43:21 +01:00
epenet
b2d4c9ecb4 Add exception translation to SFR box (#157756) 2025-12-02 17:42:16 +01:00
Matthias Alphart
f5b046ee7d Add integration_type for Fronius (#157760) 2025-12-02 17:31:05 +01:00
epenet
55c5fb7374 Migrate Tuya climate (swing) to use wrapper class (#157646) 2025-12-02 17:24:36 +01:00
Erik Montnemery
5d78cd328a Remove explicit templating of velbus service data (#157749) 2025-12-02 17:00:10 +01:00
epenet
bc36578ada Add mac address to SFR Box device registry entries (#157752) 2025-12-02 16:52:09 +01:00
Erik Montnemery
e63242e465 Add occupancy binary sensor triggers (#157631) 2025-12-02 16:37:02 +01:00
epenet
e84c09745d Bump SFR box IQS to silver (#157754) 2025-12-02 16:32:38 +01:00
Julian Meier
f07991d0ba Add boot and energy sensor to MyStrom Switch (#155132) 2025-12-02 15:42:04 +01:00
epenet
872fef1f6f Add reconfigure flow to SFR Box (#157711) 2025-12-02 15:35:25 +01:00
Kevin Stillhammer
c866dc973c Add sensor platform to fressnapf_tracker (#157658)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-02 15:33:57 +01:00
Zoltán Farkasdi
e2acf30637 Add Netatmo outdoor camera test (#156740) 2025-12-02 15:01:47 +01:00
epenet
29631a2c5a Cleanup SFR Box sensors (#157708) 2025-12-02 14:52:52 +01:00
Heindrich Paul
1d31e6d0ea Create more sensors for Nederlandse Spoorwegen (#154466)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 14:39:05 +01: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
Artur Pragacz
8109d9a39c Add integration type to music_assistant (#157725)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:38:08 +01:00
Artur Pragacz
e1abd451b8 Add integration type to google (#157729)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:37:37 +01:00
Robert Resch
2c72cd94f2 Create the go2rtc unix socket inside a temporary folder (#157742) 2025-12-02 13:35:39 +01:00
Franck Nijhof
3bccb4b89c Rename preview feature to purpose-specific triggers and conditions (#157717) 2025-12-02 13:34:52 +01:00
Artur Pragacz
6d4fb30630 Add integration type to tplink (#157735)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:24:21 +01:00
Artur Pragacz
c04411f1bc Add integration type to dlna_dmr (#157733)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:48:59 +01:00
Artur Pragacz
753ea023de Add integration type to ibeacon (#157734)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:48:33 +01:00
Artur Pragacz
1ca1cf59eb Add integration type to ring (#157738)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:44:09 +01:00
Artur Pragacz
5b01bb1a29 Add integration type to broadlink (#157739)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:43:19 +01:00
Artur Pragacz
15c89d24eb Add integration type to xiaomi_ble (#157740)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:21 +01:00
Artur Pragacz
b26b2347e6 Add integration type to roborock (#157737)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:10 +01:00
Artur Pragacz
7d54103c09 Add integration type to speedtestdotnet (#157727)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:27:25 +01:00
Artur Pragacz
c705a1dc4b Add integration type to rest (#157728)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:26:02 +01:00
Artur Pragacz
998bd23446 Add integration type to webostv (#157736)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:25:37 +01:00
Artur Pragacz
3a1a58d6ad Add integration type to ping (#157730)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:23:19 +01:00
Artur Pragacz
f9219dd841 Add integration type to dlna_dms (#157723)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:04:17 +01:00
Artur Pragacz
402ed7e0f3 Add integration type to met (#157720)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:51:51 +01:00
epenet
7a1a5df89e Use _async_send_commands in Tuya base entity (#157716) 2025-12-02 11:50:07 +01:00
Artur Pragacz
df558fc1e7 Add integration type to google_translate (#157718)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:47:30 +01:00
Erik Montnemery
ec66407ef1 Improve helpers.condition.async_subscribe_platform_events (#157710) 2025-12-02 11:32:14 +01:00
Paulus Schoutsen
6b99234a43 Add integration_type to SwitchBot Bluetooth manifest (#157675)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:31:04 +01:00
Erik Montnemery
393be71009 Improve trigger descriptions (#157643)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-02 11:08:39 +01:00
epenet
12bc1687ec Use _async_send_commands in Tuya vacuum (#157704) 2025-12-02 11:01:51 +01:00
epenet
c59b322c0a Use _async_send_commands in Tuya light (#157703) 2025-12-02 11:01:38 +01:00
Arjan
e00266463d Meteo France: add new mapping "Brouillard dense givrant" (#157627) 2025-12-02 10:55:51 +01:00
dependabot[bot]
cbc8a33553 Bump github/codeql-action from 4.31.5 to 4.31.6 (#157700)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 10:52:13 +01:00
Paulus Schoutsen
28582f75d4 Add integration_type to Ecowitt manifest (#157666) 2025-12-02 10:49:58 +01:00
J. Diego Rodríguez Royo
39cccd212d Bump aiohomeconnect to version 0.24.0 (#157670) 2025-12-02 10:46:37 +01:00
Brett Adams
329ea33337 Add integration_type to Teslemetry manifest (#157677) 2025-12-02 10:45:41 +01:00
Brett Adams
521733c420 Revert integration type in Tessie (#157713) 2025-12-02 10:45:21 +01:00
Brett Adams
33e9f9a0ff Add integration_type to Tesla Fleet manifest (#157679) 2025-12-02 10:44:49 +01:00
Erik Montnemery
5fda2bccbe Improve helpers.trigger.async_subscribe_platform_events (#157709) 2025-12-02 10:37:19 +01:00
Åke Strandberg
ae75332656 Add program id:s and phases to new Miele WQ1000 (#157660) 2025-12-02 09:25:47 +01:00
Paulus Schoutsen
b171785f96 Add integration_type to SmartThings manifest (#157673) 2025-12-02 09:17:49 +01:00
Paulus Schoutsen
ff3d6783c6 Add integration_type to Konnected.io manifest (#157681) 2025-12-02 09:15:18 +01:00
cdnninja
b1e579bea0 Bump pyvesync to 3.3.3 (#157697) 2025-12-02 09:14:41 +01:00
Jan Bouwhuis
87241ea051 Add read support for MQTT config entry version to 2.1 (#157623) 2025-12-02 08:02:06 +01:00
dependabot[bot]
a871ec0bdf Bump home-assistant/wheels from 2025.11.0 to 2025.12.0 (#157699) 2025-12-02 07:41:44 +01:00
Copilot
b8829b645a 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 07:35:29 +01:00
Paulus Schoutsen
5b056a83d4 Add integration_type to Motionblinds manifest (#157682)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 07:15:11 +01:00
Paulus Schoutsen
02a70123c1 Add integration_type to HomeWizard Energy manifest (#157680)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 00:04:08 -05:00
Paulus Schoutsen
5f6d2f537a Add integration_type to Tessie manifest (#157676)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 23:50:35 -05:00
Paulus Schoutsen
5e04e9f04d Add integration_type to Home Connect manifest (#157668)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 23:49:39 -05:00
Paulus Schoutsen
56515ad7b5 Add integration_type to Sonos manifest (#157674)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:03:53 +01:00
Paulus Schoutsen
a1fe2bf4fa Add integration_type to HomeKit Device manifest (#157671)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:02:59 +01:00
Paulus Schoutsen
b8fa8efd91 Add integration_type to Apple TV manifest (#157664)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:01:56 +01:00
Jesse Hills
03557b5ef2 Bump aioesphomeapi to 42.10.0 (#157678) 2025-12-01 20:59:35 -05:00
Paulus Schoutsen
dafec8ce58 Add integration_type to Reolink manifest (#157672)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:58:51 -05:00
Paulus Schoutsen
6ff3f74347 Add integration_type to Nuki Bridge manifest (#157683)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:58:03 -05:00
karwosts
ddd8cf7fde Fix a bad script error message (#157654) 2025-12-01 15:19:30 -05:00
TheJulianJES
1356eea52f Set Matter integration type to "hub" (#157657) 2025-12-01 15:18:55 -05:00
TheJulianJES
6188e0e39b Set ZHA integration type to "hub" (#157656) 2025-12-01 15:18:49 -05: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
Aidan Timson
699fa1617d Default area icons for new instances (#157619) 2025-12-01 20:02:38 +01:00
Erik Montnemery
449f0fa5a5 Bump floor registry to version 1.3 and sort floors (#157614)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-01 19:54:43 +01:00
andreimoraru
2e008d2bb7 bump yt-dlp to 2025.11.12 (#157645) 2025-12-01 19:44:54 +01:00
Petro31
05dec2619d Ensure platform template does not appear in repair (#157486) 2025-12-01 19:38:49 +01:00
Paul Bottein
25a6778ba8 Fix user store not loaded on restart (#157616) 2025-12-01 19:37:27 +01:00
epenet
f564b8cb44 Remove unnecessary instanciating in Tuya find_dpcode (#157473) 2025-12-01 19:37:06 +01:00
Erik Montnemery
ce6bfdebfc Improve typing of floor registry events (#157624) 2025-12-01 19:36:50 +01:00
Bram Kragten
f00a944ac1 Update frontend to 20251201.0 (#157638) 2025-12-01 19:28:18 +01:00
Petro31
3073a99ce6 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:27:30 +01:00
Erik Montnemery
8b04ce1328 Bump area registry to version 1.9 and sort areas (#157634) 2025-12-01 19:26:17 +01:00
Andrew Jackson
39f76787ab Allow multiline post in Mastodon (#157647) 2025-12-01 18:35:59 +01:00
puddly
e8acced335 Disable owning integrations for the entire firmware interaction process (#157082) 2025-12-01 18:18:32 +01:00
Åke Strandberg
758a30eebc Add code mappings for Miele WQ1000 (#157642) 2025-12-01 17:28:02 +01:00
epenet
faf94bea24 Use read_wrapper entity helper in Tuya (#157632) 2025-12-01 17:00:08 +01:00
epenet
ff91c57228 Adjust Tuya wrapper to return a command list (#157622) 2025-12-01 16:59:26 +01:00
epenet
3d2b506997 Rename Tuya method (#157640) 2025-12-01 16:56:46 +01:00
Kevin Stillhammer
d3c1c28605 Add integration fressnapf_tracker (#157480)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-01 16:48:30 +01:00
mettolen
d4e1f7741d Add sensor entities to Saunum integration (#157342)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-01 16:47:00 +01:00
mettolen
e713632eed Add reauth flow to Airobot integration (#157501) 2025-12-01 16:28:10 +01:00
Maciej Bieniek
060ad35ddc Bump aioshelly to version 13.22.0 (#157629) 2025-12-01 15:47:53 +01:00
Erik Montnemery
6c5dba40cd Remove cover triggers (#157621) 2025-12-01 14:09:29 +01:00
Erik Montnemery
a04d595424 Remove description_configured from condition and trigger translations (#157620) 2025-12-01 12:57:07 +01:00
Lukas
fe85eaf2a2 Pooldose: Add sensors for water meter (#157382) 2025-12-01 11:43:50 +01:00
Raphael Hehl
3551c4b01f Fix UniFi Protect G6 Instant speaker volume control (#157549) 2025-12-01 11:38:34 +01:00
Shay Levy
e7edd51a65 Refactor Shelly number platform to use upstream set_thermostat_state (#157527)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 12:37:12 +02:00
Raphael Hehl
0c4f2326ef Use public API for UniFi Protect light brightness control (#157550) 2025-12-01 11:32:03 +01:00
dependabot[bot]
81f4456d7c Bump actions/ai-inference from 2.0.3 to 2.0.4 (#157608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:04:04 +01:00
Maciej Bieniek
2b608bf15c Remove name from Shelly RGBCCT sensors (#157492) 2025-12-01 10:54:19 +01:00
karwosts
972ed4b27f Finish removal of sensor.sun_solar_rising (#157606) 2025-12-01 09:26:54 +01:00
LG-ThinQ-Integration
23c167da1b Bump thinqconnect to 1.0.9 (#157607) 2025-12-01 09:26:01 +01:00
Jan Bouwhuis
34d6938171 Fix subentry ID is not updated when renaming the entity ID (#157498) 2025-12-01 07:55:13 +01:00
Kevin Stillhammer
4bb8590076 Revert "Force httpx client to use IPv4 for waze_travel_time" (#157596) 2025-12-01 06:35:26 +01:00
Norbert Rittel
5e0923b60d Fix spelling of "to set up" in hue_ble (#157593) 2025-12-01 06:33:18 +01:00
Norbert Rittel
ad48f3c634 Fix spelling of "to log in" in anglian_water (#157594) 2025-12-01 06:32:54 +01:00
cdnninja
2bdd6854eb Bump pyvesync to 3.3.2 (#157605) 2025-11-30 23:41:46 -05:00
Lukas
0bf906911c pooldose bump to api 0.8.1 (#157591) 2025-11-30 23:49:40 +01:00
Ludovic BOUÉ
874d6f5613 Add Matter fixture for Eufy vacuum Omni E28 (#157590) 2025-11-30 21:47:31 +01:00
Raphael Hehl
43ba10eebd Add missing translations for UniFi Protect integration (#157570) 2025-11-30 17:05:05 +01:00
Sanjay Govind
64bed19805 Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-11-30 16:02:43 +01:00
Shay Levy
6357067f0f Rename Shelly SENSORS to BLOCK_SENSORS to match naming in other platforms (#157553) 2025-11-30 12:48:35 +02:00
Thomas55555
e328ba4045 Bump google air quality api to 1.1.3 (#157555) 2025-11-30 07:17:36 +01:00
Allen Porter
332dbddce6 Bump google-nest-sdm to 9.1.1 (#157562) 2025-11-29 23:19:44 -05:00
J. Nick Koston
82d935a819 Bump aioesphomeapi to 42.9.0 (#157558) 2025-11-29 18:04:55 -06:00
Raphael Hehl
4b84998c0c Fix UFPConfigEntry type consistency in unifiprotect (#157548) 2025-11-29 17:07:44 -06:00
Raphael Hehl
e10c1ebcf6 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-11-29 22:53:34 +02:00
Raphael Hehl
0174bad182 Add PARALLEL_UPDATES to UniFi Protect platforms (#157504) 2025-11-29 19:48:43 +01:00
Allen Porter
d5be623684 Bump python-roborock to 3.8.4 (#157538) 2025-11-29 20:34:27 +02:00
Raphael Hehl
d006b044c8 Bump uiprotect to 7.31.0 (#157543) 2025-11-29 20:33:09 +02:00
Jan Bouwhuis
fdd9571623 Fix MQTT entity cannot be renamed (#157540) 2025-11-29 19:29:54 +01:00
Shay Levy
4f4c5152b9 Refactor Shelly setup to use async_setup_entry_block for block entities (#157517) 2025-11-29 18:08:12 +02:00
Denis Shulyaka
b031a082cd Bump anthropic to 0.75.0 (#157491) 2025-11-29 14:35:30 +01:00
Shay Levy
a1132195fd Refactor Shelly RPC event platform to use base class (#157499) 2025-11-29 13:09:32 +02:00
Jordan Harvey
708b3dc8b2 Disable cookie quotes for Anglian Water (#157518) 2025-11-29 11:52:55 +01:00
J. Nick Koston
8ae0216135 Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-11-29 03:40:22 -06:00
David Woodhouse
1472281cd5 Clarify percentage_command_topic and percentage_state_topic for MQTT fan (#157460)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-11-29 02:47:34 -06:00
Allen Porter
ceaa71d198 Bump python-roborock to 3.8.3 (#157512) 2025-11-29 09:34:22 +01:00
Arie Catsman
7f0d0c555a Bump pyenphase to 2.4.2 (#157500) 2025-11-28 21:58:57 +01:00
steaura
3b94b2491a Update bootstrap.py for grammar in slow startup error log (#157458) 2025-11-28 19:34:30 +01:00
Sebastian Schneider
8c8708d5bc Support UniFi LED control for devices without RGB (#156812) 2025-11-28 17:33:15 +01:00
Maciej Bieniek
ca35102138 Remove name for Shelly gas valve (gen1) entity (#157490) 2025-11-28 15:26:17 +01:00
Maciej Bieniek
1a1b50ef1a Add missing string for Shelly away mode switch (#157488) 2025-11-28 16:07:40 +02:00
epenet
5a4d51e57a Mark config-flow-test-coverage as done in SFR Box IQS (#157485) 2025-11-28 12:46:01 +01:00
epenet
9e1bc637e2 Improve diagnostics tests in SFR Box API (#157483) 2025-11-28 11:58:33 +01:00
Joakim Plate
ab879c07ca Add logbook support for args same as params for zha (#154997) 2025-11-28 11:15:49 +01:00
Hem Bhagat
488c97531e Move translatable URLs out of strings.json for opentherm_gw integration (#157437) 2025-11-28 10:45:45 +01:00
epenet
3b52c5df79 Use snapshot_platform helper in SFR Box tests (#157481) 2025-11-28 10:44:39 +01:00
Shay Levy
7f4b56104d Update Shelly utils coverage to 100% (#157478) 2025-11-28 11:32:41 +02:00
Åke Strandberg
ab8135ba1a Add loggers to senz manifest (#157479) 2025-11-28 10:19:28 +01:00
epenet
a88599bc09 Improve tests in SFR Box (#157444)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 10:10:38 +01:00
Manuel Stahl
45034279c8 Update pystiebeleltron to 0.2.5 (#157450) 2025-11-28 09:48:51 +01:00
Artur Pragacz
9f3dae6254 Add tools in default agent also in fallback pipeline (#157441) 2025-11-28 09:47:52 +01:00
epenet
ef36d7b1e5 Fix blocking call in Tuya initialisation (#157477) 2025-11-28 09:45:28 +01:00
dependabot[bot]
e5346ba017 Bump home-assistant/builder from 2025.09.0 to 2025.11.0 (#157468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:30:37 +01:00
dependabot[bot]
68d41d2a48 Bump docker/metadata-action from 5.9.0 to 5.10.0 (#157467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:30:22 +01:00
dependabot[bot]
00a882c20a Bump actions/ai-inference from 2.0.2 to 2.0.3 (#157466)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:28:59 +01:00
Jordan Harvey
44a6772947 Fix Anglian Water sensor setup (#157457) 2025-11-28 07:25:04 +01:00
cdnninja
f874ba1355 Move device_info to attribute in vesync (#157462) 2025-11-28 07:20:50 +01:00
Allen Porter
4fc125c49a Improve Nest error message wording in test before setup (#157465) 2025-11-28 07:19:54 +01:00
Artur Pragacz
8c59196e19 Provide log info for discovered flows in logger (#157454) 2025-11-28 02:13:10 +01:00
Shay Levy
326f7f0559 Add coverage to Shelly utils (#157455) 2025-11-28 00:29:47 +02:00
ElectricSteve
11afda8c22 bump: youtubeaio to 2.1.1 (#157452) 2025-11-27 22:42:57 +01:00
StaleLoafOfBread
f1ee0e4ac9 Add support for gallons per day as a unit of volume flow rate (#157394) 2025-11-27 20:42:16 +01:00
Joakim Plate
5f522e5afa Fix cancel propagation in update coordinator and config entry (#153504) 2025-11-27 19:48:45 +01:00
Thomas55555
4f6624d0aa Fix strings in Google Air Quality (#157297) 2025-11-27 19:26:33 +01:00
epenet
70990645a7 Mark config-flow as done in SFR Box IQS (#157439) 2025-11-27 19:14:13 +01:00
Andrew Jackson
2f7d74ff62 Add icons to transmission entities (#157436) 2025-11-27 18:38:32 +01:00
epenet
885667832b Add initial IQS to sfr_box (#155419) 2025-11-27 18:36:51 +01: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
Petro31
4646929987 Avoid custom template platform deprecations (#157415) 2025-11-27 18:06:29 +01:00
Petro31
010aea952c Reload templates when labs flag automation.new_triggers_conditions is set (#157368) 2025-11-27 18:05:33 +01:00
Bram Kragten
563678dc47 Update frontend to 20251127.0 (#157431) 2025-11-27 18:05:18 +01:00
epenet
a48f01f213 Raise UpdateFailed if API returns None in sfr_box (#157434) 2025-11-27 18:01:56 +01:00
Andrew Jackson
08b758b0d2 Add device info and parallel_updates to Transmission (#157423) 2025-11-27 17:37:27 +01:00
Allen Porter
4306fbea52 Fix regression in roborock image entity naming (#157432) 2025-11-27 17:36:18 +01:00
Robert Resch
6f4c479f8f Use same cosign version in build workflow (#157365) 2025-11-27 17:13:04 +01:00
Shay Levy
1d9c06264e Fix Shelly support for button5 trigger (#157422) 2025-11-27 16:38:45 +01:00
epenet
d045ecaf13 Add parallel_updates to SFR Box (#157426) 2025-11-27 16:04:25 +01:00
Markus Jacobsen
f7c41e694c Add media content id attribute to Bang & Olufsen (#156597) 2025-11-27 15:53:43 +01:00
Kamil Breguła
9ee7ed5cdb 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 15:10:32 +01:00
Denis Shulyaka
83c4e2abc9 Fix Anthropic init with incorrect model (#157421) 2025-11-27 14:16:46 +01: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
Erik Montnemery
a7dbf551a3 Add climate started_cooling and started_drying triggers (#156945) 2025-11-27 12:41:08 +01:00
Petro31
0b2bb9f6bf Modernize template binary sensor (#157279) 2025-11-27 12:28:16 +01:00
tan-lawrence
0769163b67 Use "medium" instead of "med" for the medium fan mode in Coolmaster (#157253) 2025-11-27 12:27:49 +01:00
Robert Resch
2bb51e1146 Reduce Devcontainer docker layers (#157412) 2025-11-27 12:27:18 +01:00
Paulus Schoutsen
d2248d282c Default conversation agent to store tool calls in chat log (#157377) 2025-11-27 12:27:03 +01:00
Jan Čermák
8fe79a88ca Fix state classes of Ecowitt rain sensors (#157409) 2025-11-27 12:24:28 +01:00
Jaap Pieroen
7a328539b2 Bugfix: Essent remove average gas price today (#157317) 2025-11-27 12:24:07 +01:00
abelyliu
ec69efee4d Fix parsing of Tuya electricity RAW values (#157039) 2025-11-27 12:23:33 +01:00
Shay Levy
dbcde549d4 Update Shelly coordinator coverage to 100% (#157380) 2025-11-27 12:22:19 +01:00
Michael
988355e138 Add tests for the switch platform to the AdGuard Home integration (#157105) 2025-11-27 12:21:23 +01:00
victorigualada
7711eac607 Return early when setting cloud ai_task and conversation and not logged in to cloud (#157402) 2025-11-27 12:20:42 +01:00
Denis Shulyaka
32fe53cceb Add anthropic model to the device info (#157413) 2025-11-27 12:16:05 +01:00
Andrew Jackson
3a65d3c0dc Add tests to Transmission (#157355) 2025-11-27 12:15:10 +01:00
epenet
7fe26223ac Bump renault-api to 0.5.1 (#157411) 2025-11-27 12:06:57 +01:00
victorigualada
7e8496afb2 Bump hass-nabucasa from 1.6.1 to 1.6.2 (#157405) 2025-11-27 11:40:50 +01:00
Paulus Schoutsen
2ec5190243 Install requirements_test_all in dev (#157392) 2025-11-27 10:30:50 +01:00
Erik Montnemery
a706db8fdb Minor polish of cover trigger tests (#157397) 2025-11-27 09:57:03 +01:00
starkillerOG
a00923c48b Bump reolink-aio to 0.16.6 (#157399) 2025-11-27 09:53:25 +01:00
Sarah Seidman
7480d59f0f Normalize input for Droplet pairing code (#157361) 2025-11-27 08:36:30 +01:00
Erik Montnemery
4c8d9ed401 Adjust type hints in sensor group (#157373) 2025-11-27 08:34:16 +01:00
Lukas
eef10c59db Pooldose bump api 0.8.0 (new) (#157381) 2025-11-27 08:33:32 +01:00
dependabot[bot]
a1a1f8dd77 Bump docker/metadata-action from 5.5.1 to 5.9.0 (#157395) 2025-11-27 07:26:58 +01:00
dependabot[bot]
c75a5c5151 Bump docker/setup-buildx-action from 3.5.0 to 3.11.1 (#157396) 2025-11-27 07:25:16 +01:00
Allen Porter
cdaaa2bd8f Update fitbit to use new asyncio client library for device list (#157308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-27 00:23:49 -05:00
Allen Porter
bd84dac8fb Update roborock test typing (#157370) 2025-11-27 00:21:48 -05:00
Allen Porter
42cbeca5b0 Remove old roborock map storage (#157379) 2025-11-27 00:21:04 -05:00
Allen Porter
ad0a498d10 Bump python-roborock to 3.8.1 (#157376) 2025-11-26 16:12:19 -08:00
Jan Bouwhuis
973405822b Move translatable URL out of strings.json for knx integration (#155244) 2025-11-26 23:09:59 +01:00
Franck Nijhof
b883d2f519 Bump version to 2026.1.0dev0 2025-11-26 17:15:29 +00:00
Franck Nijhof
f64c870e42 Bump version to 2025.12.0b0 2025-11-26 17:13:42 +00:00
Christopher Fenner
4654d6de87 Filter devices based on online status in ViCare integration (#157287) 2025-11-26 18:00:52 +01:00
Ludovic BOUÉ
990c8cd4e6 Add Matter Window covering operational status (#156066)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-11-26 18:00:13 +01:00
Raphael Hehl
f8c76f42e3 Add session clearing on config entry removal for UniFi Protect integration (#157360)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:59:49 +01:00
Erik Montnemery
21d914c8ca Disable experimental conditions according to labs flag setting (#157345) 2025-11-26 17:59:12 +01:00
Erik Montnemery
ec77add1a6 Reload scripts when labs flag automation.new_triggers_conditions is set (#157348) 2025-11-26 17:53:38 +01:00
Erik Montnemery
ef3b7dfd1d Reload automations when labs flag automation.new_triggers_conditions is set (#157347) 2025-11-26 17:45:25 +01:00
Robert Resch
51241d963d Bump deebot-client to 16.4.0 (#157358) 2025-11-26 17:28:41 +01:00
Joost Lekkerkerker
7c48e6e046 Delete leftover SmartThings smartapps (#157188) 2025-11-26 17:14:36 +01:00
Bram Kragten
38d8da4279 Update frontend to 20251126.0 (#157352) 2025-11-26 17:13:25 +01:00
Raphael Hehl
3396a72fa8 Bump uiprotect to version 7.29.0 (#157354) 2025-11-26 17:04:38 +01:00
Erik Montnemery
2d26ab390e Save device registry store in worker thread (#157351) 2025-11-26 17:02:10 +01:00
Thomas55555
1bf5bc9323 Bump google air quality api to 1.1.2 (#157337) 2025-11-26 16:04:01 +01:00
Erik Montnemery
87ea96a3e0 Save entity registry store in worker thread (#157274) 2025-11-26 16:03:14 +01:00
Jan Čermák
e3cf65510b Update Home Assistant base image to 2025.11.3 (#157346) 2025-11-26 15:15:08 +01:00
Robert Resch
f69fce68d6 Use buildx imagetools to copy base image to docker.io and enable provenance (#157341)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-11-26 15:12:32 +01:00
Abílio Costa
f758cfa82f Add get_conditions_for_target websocket command (#157344)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 14:08:56 +00:00
Artur Pragacz
9c7a928b29 Add get encryption key websocket to esphome (#154058) 2025-11-26 14:41:19 +01:00
Petro31
405a9948a2 Deprecate legacy and undocumented template entity configurations (#155355) 2025-11-26 14:30:06 +01:00
Oscar
0e3bab3ce4 Energyid bugfix (#157343) 2025-11-26 14:29:28 +01:00
Erik Montnemery
4900d25ac8 Disable experimental triggers according to labs flag setting (#157320) 2025-11-26 14:27:05 +01:00
Shay Levy
ea10cdb4b0 Remove Shelly redundant device entry check for sleepy devices (#157333) 2025-11-26 14:54:51 +02:00
Oscar
6baf77d256 Energyid integration (#138206)
Co-authored-by: Jan Pecinovsky <jan.pecinovsky@energieid.be>
Co-authored-by: Jan Pecinovsky <janpecinovsky@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 13:38:57 +01:00
Artur Pragacz
13bc0ebed8 Remove incorrect after dependency in music assistant (#157339) 2025-11-26 13:38:18 +01:00
Marcel van der Veldt
611af9c832 Add support for authentication to the Music Assistant integration (#157257)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2025-11-26 13:34:26 +01:00
Abílio Costa
c2b7a63dd9 Add get_services_for_target websocket command (#157334) 2025-11-26 12:30:51 +00:00
Robert Resch
550716a753 Optimize docker container publish job (#157076)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 13:24:45 +01:00
TheJulianJES
56a71e6798 Add missing ZHA button strings (#157335) 2025-11-26 13:21:17 +01:00
Simone Chemelli
80ec51c56b Bump aioamazondevices to 10.0.0 (#157331) 2025-11-26 13:01:40 +01:00
Allen Porter
ea651c4a22 Overhaul Roborock integration to use new devices based API (#154837) 2025-11-26 12:52:09 +01:00
Bram Kragten
ff40ce419e Add context support for triggers.yaml (#156531) 2025-11-26 12:50:17 +01:00
OzGav
d95308719c Qualify Music Assistant to Bronze Quality Level (#155260)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-11-26 12:42:21 +01:00
Petro31
f4fb95ee43 Modernize template light (#156469) 2025-11-26 12:13:27 +01:00
Simone Chemelli
14d95cc86b Temporary raise scan interval for Alexa Devices (#157326) 2025-11-26 11:29:57 +01:00
Joost Lekkerkerker
4257435975 Add Matter info to SmartThings Device (#157321) 2025-11-26 11:28:49 +01:00
Abílio Costa
a6aab088fb Add get_triggers_for_target websocket command (#156778) 2025-11-26 11:05:03 +01:00
Aarni Koskela
655a63c104 Add clamp/wrap/remap to template math functions (#154537) 2025-11-26 11:00:12 +01:00
Robert Resch
a2ade413c2 Fix aarch64 image download by specifing the platform (#157316)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 10:02:35 +01:00
Jan Bouwhuis
10299b2ef4 Add description placeholders to service translation strings (#154984)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 09:54:22 +01:00
David Rapan
26444d8d34 Move Shelly sensor translation logic to base class (#157129)
Signed-off-by: David Rapan <david@rapan.cz>
2025-11-26 10:43:16 +02:00
Lukas
554c122a37 Add switch platform to PoolDose integration (#157296) 2025-11-26 09:09:35 +01:00
puddly
1c0dd02a7c Abort USB discovery flows on device unplug (#156303) 2025-11-26 09:00:41 +01:00
puddly
c41493860d Bump ZHA to 0.0.80 (#157311)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-11-26 08:43:14 +01:00
Erik Montnemery
f5b8ede5f9 Add models and websocket_api modules to labs integration (#157313) 2025-11-26 08:42:18 +01:00
dependabot[bot]
474a60511b Bump docker/setup-buildx-action from 3.10.0 to 3.11.1 (#157310) 2025-11-26 07:56:04 +01:00
TheJulianJES
9657f3f832 Fix ZHA custom quirks friendly name priority (#156751)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-11-26 05:15:42 +01:00
TheJulianJES
59f4bc1908 Raise user-friendly error for locked ports when flashing ZBT (#157272) 2025-11-26 04:37:09 +01:00
Bram Kragten
9ebc6cbb23 Add target to condition description (#157298) 2025-11-26 01:22:48 +01:00
Paul Bottein
f433ca7455 Let Home Assistant generate the entity id for esphome entities (#154097)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-25 18:09:35 -06:00
Shay Levy
f77629d0f8 Add coverage for Shelly repairs (#157277) 2025-11-26 00:06:48 +02:00
Joost Lekkerkerker
0ac1b22e03 Bump pySmartThings to 3.5.0 (#157290) 2025-11-25 23:36:50 +02:00
Thomas55555
1069233851 Add Google Air Quality integration (#145237)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-11-25 21:18:44 +01:00
Harvey
d2fd200469 New integration: Hue BLE (#118635)
Co-authored-by: Mr. Bubbles <manni@zapto.de>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-11-25 21:15:34 +01:00
Petro31
20cdd9386e Modernize template sensor (#157251) 2025-11-25 21:15:17 +01:00
Jordan Harvey
1be2e4f90c Add anglian_water integration (#156225)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-25 20:48:24 +01:00
Raphael Hehl
839f647396 Unifiprotect Prevent duplicate vehicle detection events from firing (#157278)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-25 13:41:53 -06:00
victorigualada
7c2741bd36 Add home assistant cloud conversation (#157090) 2025-11-25 14:04:19 -05:00
dotlambda
d6fb268119 Bump xmltodict to 1.0.2 (#156670) 2025-11-25 12:54:25 -06:00
Erik Montnemery
521a6784b4 Add opt-in to Store for serializing in an executor (#157263) 2025-11-25 19:10:39 +01:00
Raphael Hehl
d2ba7e8e3e UnifiProtect add vehicle detection event entity with license plate recognition support (#157203)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-25 11:30:06 -06:00
Andrew Jackson
405c2f96fd Add bronze quality scale to transmission (#156388)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 18:13:15 +01:00
Jaap Pieroen
90ef5b1c25 Add Essent dynamic price integration (#157010)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-25 18:00:34 +01:00
dotlambda
562f72f321 Bump panasonic-viera to 0.4.4 (#157268) 2025-11-25 17:59:13 +01:00
Tom Wilkie
f5ee3bd872 Refactor Prometheus metrics handling (#157159) 2025-11-25 17:53:08 +01:00
Erik Montnemery
8dd35cb129 Add entity triggers and conditions (#156852)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-11-25 17:52:20 +01:00
Manu
6fa971d393 Refactor media player browse media in Xbox integration (#156672) 2025-11-25 17:49:29 +01:00
Samuel Xiao
6deff1c78f Add smart radiator thermostat support to Switchbot Cloud (#154445)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-11-25 17:46:55 +01:00
Manu
f96996b27f Detect image type from magic numbers in image component (#157190) 2025-11-25 17:38:16 +01:00
J. Nick Koston
eb9fc66ca9 Bump yalexs-ble to 3.2.1 (#157265) 2025-11-25 17:32:47 +01:00
Petar Petrov
43e4fe4526 Add support for downstream water meters in energy dashboard (#155927) 2025-11-25 17:18:39 +01:00
Denis Shulyaka
252dbb706f Anthropic: consolidate recommended values in a dict (#156787) 2025-11-25 17:08:55 +01:00
Petro31
d7ad0cba94 Modernize template image (#157255) 2025-11-25 17:05:32 +01:00
Robert Resch
159a8d39d6 Optimize build_base build job (#157231)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 17:04:15 +01:00
victorigualada
8f1abb6dbb Add HomeAssistant Cloud ai_task (#157015) 2025-11-25 11:01:32 -05:00
Joost Lekkerkerker
242c02890f Add snapshot tests to Awair (#157266) 2025-11-25 16:58:59 +01:00
Erik Montnemery
eb793a3942 Disable Ruff rule D417 (#157264) 2025-11-25 16:48:37 +01:00
Rico Hageman
e65c47ba0f Add quality scale definition to Awair (#150234)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-25 16:25:34 +01:00
Marcel van der Veldt
24dba24571 Bump music assistant client to version 1.3.2 (#157261) 2025-11-25 16:25:07 +01:00
Erik Montnemery
4c04dc00dd Respect callback decorator in store helper async_delay_save (#157158) 2025-11-25 16:08:09 +01:00
Joakim Sørensen
0c366506c5 Bump hass-nabucasa from 1.6.0 to 1.6.1 (#157256) 2025-11-25 15:49:37 +01:00
Lukas Malkmus
a0323e80f5 Add support for switchbot presence sensor (#156314)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-11-25 15:15:57 +01:00
mettolen
e496fb2227 Add reconfigure flow to Saunum integration (#157128)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-11-25 15:10:10 +01:00
Franck Nijhof
c2219aadb1 Handle invalid IP addresses in ip_bans.yaml gracefully (#157232)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-11-25 14:49:05 +01:00
DeerMaximum
2cd0637324 Fix NINA flow tests to end a final state (#156664) 2025-11-25 14:36:45 +01:00
Robert Resch
293f8f7c87 Remove bluetooth_tracker integration (#157246) 2025-11-25 14:09:08 +01:00
Robert Resch
1af569ae17 Bump wheels to 2025.11.0 (#157247) 2025-11-25 14:05:46 +01:00
Erik Montnemery
d4db5ec0cc Fix use of storage helper in the labs integration (#157249)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 13:52:02 +01:00
Paul Bottein
4be1fa9a3a Provide icon and title for lovelace panel (#156955) 2025-11-25 13:48:28 +01:00
Robert Resch
149c1e6772 Remove rpi_camera (#157245) 2025-11-25 13:44:00 +01:00
Franck Nijhof
e37e7574a4 Bump debugpy to 1.8.17 (#157248)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-25 13:34:56 +01:00
epenet
37152a27ba Remove old migration from Tuya (#157237) 2025-11-25 13:34:01 +01:00
Josef Zweck
5025af8334 Add steam temperature number to lamarzocco (#157167) 2025-11-25 13:33:13 +01:00
mettolen
ec9fb9837a Add current humidity to Airobot climate entity (#157209) 2025-11-25 13:15:41 +01:00
Franck Nijhof
30451e3aaa Register preview labs feature for the new intuitive triggers and conditions (#157189)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-11-25 12:54:28 +01:00
Kevin Stillhammer
c66c3497c1 Bump pywaze to 1.1.1 (#157240) 2025-11-25 12:18:41 +01:00
Robert Resch
1063e71318 Don't fetch everything on builder init job (#157243) 2025-11-25 12:00:19 +01:00
Maciej Bieniek
1a875b021a Bump imgw-pib to version 1.6.0 (#157239) 2025-11-25 11:57:45 +01:00
epenet
15328a4aff Cleanup unused function in Tuya util (#157227) 2025-11-25 10:58:40 +01:00
Paul Bottein
083cfb89af Add winter mode to front-end integration under lab preview feature (#157181) 2025-11-25 09:34:11 +01:00
epenet
bd129c2085 Bump tuya-device-sharing-sdk to 0.2.6 (#157223) 2025-11-25 09:18:02 +01:00
Petro31
f73bc9242b Modernize template fan (#156470) 2025-11-25 08:08:19 +01:00
wollew
4506be5065 Complete test coverage for velux light and cover entities (#156770)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-11-25 08:07:36 +01:00
Manu
80c611e562 Add support for additional remote commands to Xbox integration (#157206) 2025-11-25 07:49:13 +01:00
Will Moss
b44aafc294 Improved error handling for oauth2 configuration in neato integration (#156300) 2025-11-25 07:28:04 +01:00
Petro31
af1e3205b8 Modernize template lock (#156402) 2025-11-25 07:26:42 +01:00
dependabot[bot]
1360fe7f23 Bump actions/setup-python from 6.0.0 to 6.1.0 (#157219)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 07:22:24 +01:00
dependabot[bot]
b5bb8583f8 Bump github/codeql-action from 4.31.4 to 4.31.5 (#157220)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 07:21:59 +01:00
Andre Lengwenus
9b62c212ce Bump LCN quality scale to Silver (#157151) 2025-11-25 06:45:26 +01:00
Simone Chemelli
8fa56ad92e Bump aioamazondevices to 9.0.3 (#157205)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-25 06:09:20 +01:00
ndrwrbgs
f82f0a1862 Correctcurrent_temp local variable name in homekit _get_current_temperature (#157202) 2025-11-25 06:04:13 +01:00
J. Nick Koston
878881b100 Fix elkm1 connection cleanup on setup failure (#157208) 2025-11-24 23:12:04 -05:00
J. Nick Koston
743583d9bd Bump aioesphomeapi to 42.8.0 (#157214) 2025-11-24 22:49:57 -05:00
Jan Bouwhuis
f537204d22 Fix websocket_api timeout test (#157204) 2025-11-24 20:12:32 -06:00
Glenn Waters
ec74be7922 Bump elkm1-lib to 2.2.13 (#157212)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 18:27:53 -06:00
David Rapan
3574f647d0 Move Shelly binary sensor translation logic to base class (#157127)
Signed-off-by: David Rapan <david@rapan.cz>
2025-11-25 00:31:25 +02:00
David Rapan
6c4296a0de Remove Shelly binary sensor name removal (#157065)
Signed-off-by: David Rapan <david@rapan.cz>
2025-11-24 22:35:25 +01:00
Paulus Schoutsen
e780e3db8c Add chat log subscription endpoint (#155287)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 22:31:19 +01:00
David Rapan
4ed2efa4e8 Remove Shelly switch name removal (#157072)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 22:24:24 +01:00
David Rapan
abef6f7b3e Remove Shelly sensor name removal (#157071)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 22:09:42 +01:00
David Rapan
5556fb99e6 Remove Shelly event name removal (#157067)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 22:01:58 +01:00
J. Nick Koston
16669e39bd Filter devices with active discovery flows from Shelly user step (#157201) 2025-11-24 14:52:05 -06:00
Raphael Hehl
ca088d81c3 Bump uiprotect to version 7.28.0 (#157198)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 14:44:39 -06:00
Michael Hansen
12847fb0a4 Bump intents (#157200) 2025-11-24 21:39:24 +01:00
J. Nick Koston
8b758c46f4 Combine Shelly BLE WiFi provisioning SSID and password steps (#157199) 2025-11-24 14:34:59 -06:00
J. Nick Koston
f439471dc1 Add BLE IP fallback for Shelly provisioning when zeroconf fails (#157144) 2025-11-24 13:55:09 -06:00
Raphael Hehl
5ff3233b09 Remove license plate event sensor (#157196) 2025-11-24 13:48:38 -06:00
Jan Bouwhuis
22daed083f Fix MQTT link text to be sentence cased (#157191) 2025-11-24 20:27:22 +01:00
Peter Norlander
13384de464 Extend Matter lock to support selecting OperatingMode (#157132)
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2025-11-24 20:16:56 +01:00
J. Nick Koston
f5e5183190 Show available shelly devices in user config flow (#157138) 2025-11-24 13:16:42 -06:00
Jan Bouwhuis
e18668b8f9 Add MQTT water heater subentry support (#157182) 2025-11-24 19:54:42 +01:00
Åke Strandberg
15647f2720 Add miele select platform to support sabbath mode (#156866) 2025-11-24 19:29:13 +01:00
victorigualada
c961126ee5 Bump hass-nabucasa from 1.5.1 to 1.6.0 (#157177) 2025-11-24 16:43:10 +00:00
Joost Lekkerkerker
5142c5f418 Use SmartThings modelCode as model_id (#157179) 2025-11-24 17:07:22 +01:00
David Rapan
3d459704e1 Remove Shelly update name removal (#157073)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 18:02:23 +02:00
Joost Lekkerkerker
5a8ddcd0b3 Bump pySmartThings to 3.4.0 (#157178) 2025-11-24 16:55:18 +01:00
David Rapan
2667a40b92 Remove Shelly number name removal (#157069)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 17:08:13 +02:00
David Rapan
08baa99691 Remove Shelly button name removal (#157066)
Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-11-24 14:28:38 +02:00
David Rapan
d84cf26f40 Fix Shelly Self-test sensor name (#157169) 2025-11-24 14:03:56 +02:00
Robert Resch
ba5472da90 Pin go2rtc version to sha hash (#157166)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 12:52:58 +01:00
Jan Bouwhuis
e20b88a54f Revert MQTT subscribe on_subscribe arg (#157168) 2025-11-24 12:32:55 +01:00
TimL
ac69712a51 update firmware handling in SMLIGHT integration (#157145) 2025-11-24 12:13:28 +01:00
Jan Bouwhuis
f0e75ba0ed Add MQTT valve subentry support (#157124)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-11-24 12:04:38 +01:00
mettolen
e64598e7f5 Add light entity to Saunum integration (#157081)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-11-24 12:02:46 +01:00
vexofp
e6f9a8e7d6 Assign icons for more Octoprint sensors (#157150) 2025-11-24 11:51:58 +01:00
Josef Zweck
1e8b42f843 Bump pylamarzocco to 2.2.2 (#157165) 2025-11-24 11:50:11 +01:00
Franck Nijhof
430eee0b28 Address Home Assistant Labs review comments (#157075)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-24 11:34:38 +01:00
Paulus Schoutsen
b4799aa7ea Abort Z-Wave JS discovery from ESPHome if add-on umanaged (#157013)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 11:09:20 +01:00
Markus Jacobsen
ab45460069 Add Beoremote One support to Bang & Olufsen (#155082)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-11-24 08:17:04 +01:00
dependabot[bot]
c8fd6db3ff Bump actions/ai-inference from 2.0.1 to 2.0.2 (#157153) 2025-11-24 07:54:24 +01:00
Jan Bouwhuis
0a9f200ca4 Bump incomfort-client to v0.6.10 (#157136) 2025-11-24 06:31:50 +01:00
J. Nick Koston
8591335660 Bump dbus-fast to 3.1.2 (#157147) 2025-11-23 23:03:26 -05:00
TimL
c01089e994 Bump pysmlight to 0.2.11 (#157146) 2025-11-23 21:05:41 -06:00
1069 changed files with 74071 additions and 12999 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",

View File

@@ -14,6 +14,9 @@ env:
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2025.11.3"
ARCHITECTURES: '["amd64", "aarch64"]'
jobs:
init:
@@ -21,18 +24,16 @@ jobs:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
outputs:
architectures: ${{ steps.info.outputs.architectures }}
version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -79,7 +80,7 @@ jobs:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
@@ -88,9 +89,14 @@ jobs:
fail-fast: false
matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -116,7 +122,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -184,16 +190,60 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.09.0
- &install_cosign
name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
args: |
$BUILD_ARGS \
--${{ matrix.arch }} \
--cosign \
--target /data \
--generic ${{ needs.init.outputs.version }}
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build variables
id: vars
shell: bash
run: |
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${{ steps.vars.outputs.base_image }}"
- name: Verify cache image signature
id: cache
continue-on-error: true
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${{ steps.vars.outputs.cache_image }}"
- name: Build base image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
run: |
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -223,7 +273,7 @@ jobs:
- green
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set build additional args
run: |
@@ -245,7 +295,7 @@ jobs:
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.09.0
uses: home-assistant/builder@2025.11.0
with:
args: |
$BUILD_ARGS \
@@ -261,7 +311,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -304,13 +354,7 @@ jobs:
matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.2.3"
- *install_cosign
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -320,88 +364,104 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Meta Image
- name: Verify architecture image signatures
shell: bash
run: |
export DOCKER_CLI_EXPERIMENTAL=enabled
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Verifying ${arch} image signature..."
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
echo "✓ All images verified successfully"
function create_manifest() {
local tag_l=${1}
local tag_r=${2}
local registry=${{ matrix.registry }}
# Generate all Docker tags based on version string
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
# Examples:
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
tags: |
type=raw,value=${{ needs.init.outputs.version }},priority=9999
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
docker manifest create "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
shell: bash
run: |
# Use imagetools to copy image blobs directly between registries
# This preserves provenance/attestations and seems to be much faster than pull/push
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
- name: Create and push multi-arch manifests
shell: bash
run: |
# Build list of architecture images dynamically
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
ARCH_IMAGES=()
for arch in $ARCHS; do
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
done
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
cosign sign --yes "${registry}/home-assistant:${tag_l}"
}
# Build list of all tags for single manifest creation
# Note: Using sep-tags=',' in metadata-action for easier parsing
TAG_ARGS=()
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
for tag in "${TAGS[@]}"; do
TAG_ARGS+=("--tag" "${tag}")
done
function validate_image() {
local image=${1}
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
echo "Invalid signature!"
exit 1
fi
}
# Create manifest with ALL tags in a single operation (much faster!)
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
function push_dockerhub() {
local image=${1}
local tag=${2}
# Sign each tag separately (signing requires individual tag names)
echo "Signing all tags..."
for tag in "${TAGS[@]}"; do
echo "Signing ${tag}"
cosign sign --yes "${tag}"
done
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
docker push "docker.io/homeassistant/${image}:${tag}"
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
}
# Pull images from github container registry and verify signature
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
# Upload images to dockerhub
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
fi
# Create version tag
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
create_manifest "dev" "${{ needs.init.outputs.version }}"
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
create_manifest "beta" "${{ needs.init.outputs.version }}"
create_manifest "rc" "${{ needs.init.outputs.version }}"
else
create_manifest "stable" "${{ needs.init.outputs.version }}"
create_manifest "latest" "${{ needs.init.outputs.version }}"
create_manifest "beta" "${{ needs.init.outputs.version }}"
create_manifest "rc" "${{ needs.init.outputs.version }}"
# Create series version tag (e.g. 2021.6)
v="${{ needs.init.outputs.version }}"
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
fi
echo "All manifests created and signed successfully"
build_python:
name: Build PyPi package
@@ -414,10 +474,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -459,7 +519,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0

View File

@@ -40,9 +40,9 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.12"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
HA_SHORT_VERSION: "2026.1"
DEFAULT_PYTHON: "3.13.9"
ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -99,7 +99,7 @@ jobs:
steps:
- &checkout
name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -257,7 +257,7 @@ jobs:
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: &actions-setup-python actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true

View File

@@ -21,14 +21,14 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with:
category: "/language:python"

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -28,16 +28,14 @@ jobs:
name: Initialize wheels builder
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
outputs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- &checkout
name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -50,10 +48,6 @@ jobs:
pip install "$(grep '^uv' < requirements.txt)"
uv pip install -r requirements.txt
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master
- name: Create requirements_diff file
run: |
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
@@ -114,9 +108,10 @@ jobs:
fail-fast: false
matrix: &matrix-build
abi: ["cp313", "cp314"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
arch: ["amd64", "aarch64"]
include:
- os: ubuntu-latest
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
@@ -140,9 +135,8 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: &home-assistant-wheels home-assistant/wheels@2025.10.0
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -183,7 +177,6 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: *home-assistant-wheels
with:

View File

@@ -94,7 +94,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
files: ^(script/hassfest/(metadata|docker)\.py|homeassistant/const\.py$|pyproject\.toml)$
- id: hassfest-mypy-config
name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config

View File

@@ -120,7 +120,6 @@ homeassistant.components.blueprint.*
homeassistant.components.bluesound.*
homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
@@ -188,6 +187,7 @@ homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
homeassistant.components.energenie_power_sockets.*
homeassistant.components.energy.*
homeassistant.components.energyid.*
homeassistant.components.energyzero.*
homeassistant.components.enigma2.*
homeassistant.components.enphase_envoy.*

25
CODEOWNERS generated
View File

@@ -121,6 +121,8 @@ build.json @home-assistant/supervisor
/tests/components/androidtv/ @JeffLIrion @ollo69
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
/tests/components/androidtv_remote/ @tronikos @Drafteed
/homeassistant/components/anglian_water/ @pantherale0
/tests/components/anglian_water/ @pantherale0
/homeassistant/components/anova/ @Lash-L
/tests/components/anova/ @Lash-L
/homeassistant/components/anthemav/ @hyralex
@@ -183,8 +185,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/automation/ @home-assistant/core
/tests/components/automation/ @home-assistant/core
/homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @danielsjf
/tests/components/awair/ @ahayworth @danielsjf
/homeassistant/components/awair/ @ahayworth @ricohageman
/tests/components/awair/ @ahayworth @ricohageman
/homeassistant/components/aws_s3/ @tomasbedrich
/tests/components/aws_s3/ @tomasbedrich
/homeassistant/components/axis/ @Kane610
@@ -450,6 +452,8 @@ build.json @home-assistant/supervisor
/tests/components/energenie_power_sockets/ @gnumpi
/homeassistant/components/energy/ @home-assistant/core
/tests/components/energy/ @home-assistant/core
/homeassistant/components/energyid/ @JrtPec @Molier
/tests/components/energyid/ @JrtPec @Molier
/homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @autinerd
@@ -472,6 +476,8 @@ build.json @home-assistant/supervisor
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
/homeassistant/components/essent/ @jaapp
/tests/components/essent/ @jaapp
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core
@@ -533,6 +539,8 @@ build.json @home-assistant/supervisor
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415
/homeassistant/components/fressnapf_tracker/ @eifinger
/tests/components/fressnapf_tracker/ @eifinger
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
@@ -563,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
@@ -591,6 +601,8 @@ build.json @home-assistant/supervisor
/tests/components/goodwe/ @mletenay @starkillerOG
/homeassistant/components/google/ @allenporter
/tests/components/google/ @allenporter
/homeassistant/components/google_air_quality/ @Thomas55555
/tests/components/google_air_quality/ @Thomas55555
/homeassistant/components/google_assistant/ @home-assistant/cloud
/tests/components/google_assistant/ @home-assistant/cloud
/homeassistant/components/google_assistant_sdk/ @tronikos
@@ -700,6 +712,8 @@ build.json @home-assistant/supervisor
/tests/components/huawei_lte/ @scop @fphammerle
/homeassistant/components/hue/ @marcelveldt
/tests/components/hue/ @marcelveldt
/homeassistant/components/hue_ble/ @flip-dots
/tests/components/hue_ble/ @flip-dots
/homeassistant/components/huisbaasje/ @dennisschroer
/tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
@@ -1344,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
@@ -1751,6 +1765,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
/tests/components/vivotek/ @HarlemSquirrel
/homeassistant/components/vizio/ @raman325
/tests/components/vizio/ @raman325
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
@@ -1790,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

33
Dockerfile generated
View File

@@ -4,32 +4,33 @@
ARG BUILD_FROM
FROM ${BUILD_FROM}
LABEL \
io.hass.type="core" \
org.opencontainers.image.authors="The Home Assistant Authors" \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=240000 \
UV_SYSTEM_PYTHON=true \
UV_NO_CACHE=true
ARG QEMU_CPU
# Home Assistant S6-Overlay
COPY rootfs /
# Needs to be redefined inside the FROM statement to be set for RUN commands
ARG BUILD_ARCH
# Get go2rtc binary
RUN \
case "${BUILD_ARCH}" in \
"aarch64") go2rtc_suffix='arm64' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.12/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc@sha256:baef0aa19d759fcfd31607b34ce8eaf039d496282bba57731e6ae326896d7640 /usr/local/bin/go2rtc /bin/go2rtc
# Install uv
RUN pip3 install uv==0.9.6
RUN \
# Verify go2rtc can be executed
go2rtc --version \
# Install uv
&& pip3 install uv==0.9.6
WORKDIR /usr/src

View File

@@ -35,25 +35,22 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
USER vscode
COPY .python-version ./
RUN uv python install
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
RUN uv venv $VIRTUAL_ENV
RUN --mount=type=bind,source=.python-version,target=.python-version \
uv python install \
&& uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /tmp
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
&& uv pip install -e ~/hass-release/
# Install Python dependencies from requirements
COPY requirements.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
RUN uv pip install -r requirements.txt
COPY requirements_test.txt requirements_test_pre_commit.txt ./
RUN uv pip install -r requirements_test.txt
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
uv pip install -r requirements.txt -r requirements_test.txt
WORKDIR /workspaces

View File

@@ -1,16 +0,0 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.11.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.11.0
cosign:
base_identity: https://github.com/home-assistant/docker/.*
identity: https://github.com/home-assistant/core/.*
labels:
io.hass.type: core
org.opencontainers.image.title: Home Assistant
org.opencontainers.image.description: Open-source home automation platform running on Python 3
org.opencontainers.image.source: https://github.com/home-assistant/core
org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache-2.0

View File

@@ -7,6 +7,7 @@ from typing import Any, Final
from homeassistant.const import (
EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE,
EVENT_LABS_UPDATED,
EVENT_LOVELACE_UPDATED,
EVENT_PANELS_UPDATED,
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
@@ -45,6 +46,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED,
EVENT_LABEL_REGISTRY_UPDATED,
EVENT_LABS_UPDATED,
EVENT_CATEGORY_REGISTRY_UPDATED,
EVENT_FLOOR_REGISTRY_UPDATED,
}

View File

@@ -1000,7 +1000,7 @@ class _WatchPendingSetups:
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
_LOGGER.warning(
"Waiting on integrations to complete setup: %s",
"Waiting for integrations to complete setup: %s",
self._setup_started,
)

View File

@@ -2,6 +2,7 @@
"domain": "google",
"name": "Google",
"integrations": [
"google_air_quality",
"google_assistant",
"google_assistant_sdk",
"google_cloud",

View File

@@ -1,5 +1,5 @@
{
"domain": "philips",
"name": "Philips",
"integrations": ["dynalite", "hue", "philips_js"]
"integrations": ["dynalite", "hue", "hue_ble", "philips_js"]
}

View File

@@ -1,5 +1,5 @@
{
"domain": "raspberry_pi",
"name": "Raspberry Pi",
"integrations": ["raspberry_pi", "rpi_camera", "rpi_power", "remote_rpi_gpio"]
"integrations": ["raspberry_pi", "rpi_power", "remote_rpi_gpio"]
}

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

@@ -75,9 +75,19 @@ class AirobotClimate(AirobotEntity, ClimateEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
"""Return the current temperature.
If floor temperature is available, thermostat is set up for floor heating.
"""
if self._status.temp_floor is not None:
return self._status.temp_floor
return self._status.temp_air
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
return self._status.hum_air
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
@@ -126,6 +136,13 @@ class AirobotClimate(AirobotEntity, ClimateEntity):
await self.coordinator.async_request_refresh()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set HVAC mode.
This thermostat only supports HEAT mode. The climate platform validates
that only supported modes are passed, so this method is a no-op.
"""
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
try:

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any
@@ -174,6 +175,56 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
# Combine existing data with new password
data = {
CONF_HOST: reauth_entry.data[CONF_HOST],
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
try:
await validate_input(self.hass, data)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
description_placeholders={
"username": reauth_entry.data[CONF_USERNAME],
"host": reauth_entry.data[CONF_HOST],
},
errors=errors,
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@@ -11,6 +11,7 @@ from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -53,7 +54,15 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
try:
status = await self.client.get_statuses()
settings = await self.client.get_settings()
except (AirobotAuthError, AirobotConnectionError) as err:
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
except AirobotAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_failed",
) from err
except AirobotConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="connection_failed",
) from err
return AirobotData(status=status, settings=settings)

View File

@@ -12,6 +12,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairobotrest"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pyairobotrest==0.1.0"]
}

View File

@@ -34,7 +34,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done
# Gold
@@ -59,7 +59,9 @@ rules:
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
repair-issues:
status: exempt
comment: This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: Single device integration, no stale device handling needed.

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_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -14,15 +15,24 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "The thermostat password."
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
},
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
},
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"username": "Device ID"
},
"data_description": {
"host": "The hostname or IP address of your Airobot thermostat.",
@@ -34,6 +44,12 @@
}
},
"exceptions": {
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."
},
"connection_failed": {
"message": "Failed to communicate with device."
},
"set_preset_mode_failed": {
"message": "Failed to set preset mode to {preset_mode}."
},

View File

@@ -36,5 +36,28 @@
"alarm_trigger": {
"service": "mdi:bell-ring"
}
},
"triggers": {
"armed": {
"trigger": "mdi:shield"
},
"armed_away": {
"trigger": "mdi:shield-lock"
},
"armed_home": {
"trigger": "mdi:shield-home"
},
"armed_night": {
"trigger": "mdi:shield-moon"
},
"armed_vacation": {
"trigger": "mdi:shield-airplane"
},
"disarmed": {
"trigger": "mdi:shield-off"
},
"triggered": {
"trigger": "mdi:bell-ring"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"action_type": {
"arm_away": "Arm {entity_name} away",
@@ -71,6 +75,15 @@
"message": "Arming requires a code but none was given for {entity_id}."
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"alarm_arm_away": {
"description": "Arms the alarm in the away mode.",
@@ -143,5 +156,77 @@
"name": "Trigger"
}
},
"title": "Alarm control panel"
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed"
},
"armed_away": {
"description": "Triggers after one or more alarms become armed in away mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed away"
},
"armed_home": {
"description": "Triggers after one or more alarms become armed in home mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed home"
},
"armed_night": {
"description": "Triggers after one or more alarms become armed in night mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed night"
},
"armed_vacation": {
"description": "Triggers after one or more alarms become armed in vacation mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm armed vacation"
},
"disarmed": {
"description": "Triggers after one or more alarms become disarmed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm disarmed"
},
"triggered": {
"description": "Triggers after one or more alarms become triggered.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "Alarm triggered"
}
}
}

View File

@@ -0,0 +1,99 @@
"""Provides triggers for alarm control panels."""
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import (
EntityStateTriggerBase,
Trigger,
make_conditional_entity_state_trigger,
make_entity_state_trigger,
)
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
"""Trigger for entity state changes."""
_required_features: int
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if supports_feature(self._hass, entity_id, self._required_features)
}
def make_entity_state_trigger_required_features(
domain: str, to_state: str, required_features: int
) -> type[EntityStateTriggerBase]:
"""Create an entity state trigger class."""
class CustomTrigger(EntityStateTriggerRequiredFeatures):
"""Trigger for entity state changes."""
_domain = domain
_to_state = to_state
_required_features = required_features
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"armed": make_conditional_entity_state_trigger(
DOMAIN,
from_states={
AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING,
AlarmControlPanelState.TRIGGERED,
},
to_states={
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION,
},
),
"armed_away": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelEntityFeature.ARM_AWAY,
),
"armed_home": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelEntityFeature.ARM_HOME,
),
"armed_night": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelEntityFeature.ARM_NIGHT,
),
"armed_vacation": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for alarm control panels."""
return TRIGGERS

View File

@@ -0,0 +1,53 @@
.trigger_common: &trigger_common
target:
entity:
domain: alarm_control_panel
fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
armed: *trigger_common
armed_away:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
armed_home:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
armed_night:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
armed_vacation:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
disarmed: *trigger_common
triggered: *trigger_common

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 30
SCAN_INTERVAL = 300
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
@@ -45,7 +45,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
config_entry=entry,
update_interval=timedelta(seconds=SCAN_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=30, immediate=False
hass, _LOGGER, cooldown=SCAN_INTERVAL, immediate=False
),
)
self.api = AmazonEchoApi(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==9.0.2"]
"requirements": ["aioamazondevices==10.0.0"]
}

View File

@@ -0,0 +1,74 @@
"""The Anglian Water integration."""
from __future__ import annotations
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
ExpiredAccessTokenError,
SelfAssertedError,
SmartMeterUnavailableError,
)
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_PASSWORD,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
_PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(
hass: HomeAssistant, entry: AnglianWaterConfigEntry
) -> bool:
"""Set up Anglian Water from a config entry."""
auth = MSOB2CAuth(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_create_clientsession(
hass,
cookie_jar=CookieJar(quote_cookie=False),
),
refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
)
try:
await auth.send_refresh_request()
except (ExpiredAccessTokenError, SelfAssertedError) as err:
raise ConfigEntryAuthFailed from err
_aw = AnglianWater(authenticator=auth)
try:
await _aw.validate_smart_meter()
except SmartMeterUnavailableError as err:
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
) from err
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_ACCESS_TOKEN: auth.refresh_token}
)
entry.runtime_data = coordinator = AnglianWaterUpdateCoordinator(
hass=hass, api=_aw, config_entry=entry
)
await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: AnglianWaterConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -0,0 +1,89 @@
"""Config flow for the Anglian Water integration."""
from __future__ import annotations
import logging
from typing import Any
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import BaseAuth, MSOB2CAuth
from pyanglianwater.exceptions import (
InvalidAccountIdError,
SelfAssertedError,
SmartMeterUnavailableError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): selector.TextSelector(),
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
"""Validate the provided credentials."""
try:
await auth.send_login_request()
except SelfAssertedError:
return "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
return "unknown"
_aw = AnglianWater(authenticator=auth)
try:
await _aw.validate_smart_meter()
except (InvalidAccountIdError, SmartMeterUnavailableError):
return "smart_meter_unavailable"
return auth
class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anglian Water."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
validation_response = await validate_credentials(
MSOB2CAuth(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
session=async_create_clientsession(
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input[CONF_ACCOUNT_NUMBER],
)
)
if isinstance(validation_response, BaseAuth):
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
},
)
errors["base"] = validation_response
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -0,0 +1,4 @@
"""Constants for the Anglian Water integration."""
DOMAIN = "anglian_water"
CONF_ACCOUNT_NUMBER = "account_number"

View File

@@ -0,0 +1,49 @@
"""Anglian Water data coordinator."""
from __future__ import annotations
from datetime import timedelta
import logging
from pyanglianwater import AnglianWater
from pyanglianwater.exceptions import ExpiredAccessTokenError, UnknownEndpointError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(minutes=60)
class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
"""Anglian Water data update coordinator."""
config_entry: AnglianWaterConfigEntry
def __init__(
self,
hass: HomeAssistant,
api: AnglianWater,
config_entry: AnglianWaterConfigEntry,
) -> None:
"""Initialize update coordinator."""
super().__init__(
hass=hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=config_entry,
)
self.api = api
async def _async_update_data(self) -> None:
"""Update data from Anglian Water's API."""
try:
return await self.api.update()
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
raise UpdateFailed from err

View File

@@ -0,0 +1,48 @@
"""Anglian Water entity."""
from __future__ import annotations
import logging
from pyanglianwater.meter import SmartMeter
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AnglianWaterUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
"""Defines a Anglian Water entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AnglianWaterUpdateCoordinator,
smart_meter: SmartMeter,
key: str,
) -> None:
"""Initialize Anglian Water entity."""
super().__init__(coordinator)
self.smart_meter = smart_meter
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, smart_meter.serial_number)},
name=smart_meter.serial_number,
manufacturer="Anglian Water",
serial_number=smart_meter.serial_number,
)
async def async_added_to_hass(self) -> None:
"""When entity is loaded."""
self.coordinator.api.updated_data_callbacks.append(self.async_write_ha_state)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self) -> None:
"""When will be removed from HASS."""
self.coordinator.api.updated_data_callbacks.remove(self.async_write_ha_state)
await super().async_will_remove_from_hass()

View File

@@ -0,0 +1,11 @@
{
"domain": "anglian_water",
"name": "Anglian Water",
"codeowners": ["@pantherale0"],
"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

@@ -0,0 +1,83 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions are defined.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
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: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
Unable to discover meters.
discovery:
status: exempt
comment: |
Unable to discover meters.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
No entities are disabled by default.
entity-translations: done
exception-translations: done
icon-translations:
status: exempt
comment: |
Entities do not require different icons.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
Read-only integration and no repairs are possible.
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,117 @@
"""Anglian Water sensor platform."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from pyanglianwater.meter import SmartMeter
from homeassistant.components.sensor import (
EntityCategory,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
from .entity import AnglianWaterEntity
PARALLEL_UPDATES = 0
class AnglianWaterSensor(StrEnum):
"""Store keys for Anglian Water sensors."""
YESTERDAY_CONSUMPTION = "yesterday_consumption"
YESTERDAY_WATER_COST = "yesterday_water_cost"
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
LATEST_READING = "latest_reading"
@dataclass(frozen=True, kw_only=True)
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
"""Describes AnglianWater sensor entity."""
value_fn: Callable[[SmartMeter], float]
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.YESTERDAY_CONSUMPTION,
native_unit_of_measurement=UnitOfVolume.LITERS,
device_class=SensorDeviceClass.WATER,
value_fn=lambda entity: entity.get_yesterday_consumption,
state_class=SensorStateClass.TOTAL,
translation_key=AnglianWaterSensor.YESTERDAY_CONSUMPTION,
entity_category=EntityCategory.DIAGNOSTIC,
),
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.LATEST_READING,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.WATER,
value_fn=lambda entity: entity.latest_read,
state_class=SensorStateClass.TOTAL_INCREASING,
translation_key=AnglianWaterSensor.LATEST_READING,
entity_category=EntityCategory.DIAGNOSTIC,
),
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.YESTERDAY_WATER_COST,
native_unit_of_measurement="GBP",
device_class=SensorDeviceClass.MONETARY,
value_fn=lambda entity: entity.yesterday_water_cost,
translation_key=AnglianWaterSensor.YESTERDAY_WATER_COST,
entity_category=EntityCategory.DIAGNOSTIC,
),
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
native_unit_of_measurement="GBP",
device_class=SensorDeviceClass.MONETARY,
value_fn=lambda entity: entity.yesterday_sewerage_cost,
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AnglianWaterConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
async_add_devices(
AnglianWaterSensorEntity(
coordinator=entry.runtime_data,
description=entity_description,
smart_meter=smart_meter,
)
for entity_description in ENTITY_DESCRIPTIONS
for smart_meter in entry.runtime_data.api.meters.values()
)
class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
"""Defines a Anglian Water sensor."""
entity_description: AnglianWaterSensorEntityDescription
def __init__(
self,
coordinator: AnglianWaterUpdateCoordinator,
smart_meter: SmartMeter,
description: AnglianWaterSensorEntityDescription,
) -> None:
"""Initialize Anglian Water sensor."""
super().__init__(coordinator, smart_meter, description.key)
self.entity_description = description
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.smart_meter)

View File

@@ -0,0 +1,55 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"smart_meter_unavailable": "This account does not have any smart meters associated with it. If this is unexpected, enter your Billing Account Number found at the top of your latest bill.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"account_number": "Billing Account Number",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"account_number": "Your account number found on your latest bill.",
"password": "Your password",
"username": "Username or email used to log in to the Anglian Water website."
},
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
}
}
},
"entity": {
"sensor": {
"latest_reading": {
"name": "Latest reading"
},
"yesterday_consumption": {
"name": "Yesterday's usage"
},
"yesterday_sewerage_cost": {
"name": "Yesterday's sewerage cost"
},
"yesterday_water_cost": {
"name": "Yesterday's water cost"
}
}
},
"exceptions": {
"auth_expired": {
"message": "Authentication token expired"
},
"service_unavailable": {
"message": "Anglian Water services are currently unavailable for maintenance."
},
"smart_meter_unavailable": {
"message": "This account no longer has a smart meter associated with it."
}
}
}

View File

@@ -17,13 +17,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CHAT_MODEL,
DEFAULT_CONVERSATION_NAME,
DOMAIN,
LOGGER,
RECOMMENDED_CHAT_MODEL,
)
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -43,14 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
try:
# Use model from first conversation subentry for validation
subentries = list(entry.subentries.values())
if subentries:
model_id = subentries[0].data.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
else:
model_id = RECOMMENDED_CHAT_MODEL
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
await client.models.list(timeout=10.0)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False

View File

@@ -6,7 +6,7 @@ from functools import partial
import json
import logging
import re
from typing import Any
from typing import Any, cast
import anthropic
import voluptuous as vol
@@ -54,17 +54,11 @@ from .const import (
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_TIMEZONE,
CONF_WEB_SEARCH_USER_LOCATION,
DEFAULT,
DEFAULT_AI_TASK_NAME,
DEFAULT_CONVERSATION_NAME,
DOMAIN,
NON_THINKING_MODELS,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
RECOMMENDED_WEB_SEARCH,
RECOMMENDED_WEB_SEARCH_MAX_USES,
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
WEB_SEARCH_UNSUPPORTED_MODELS,
)
@@ -76,13 +70,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
RECOMMENDED_CONVERSATION_OPTIONS = {
DEFAULT_CONVERSATION_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
RECOMMENDED_AI_TASK_OPTIONS = {
DEFAULT_AI_TASK_OPTIONS = {
CONF_RECOMMENDED: True,
}
@@ -136,13 +130,13 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
subentries=[
{
"subentry_type": "conversation",
"data": RECOMMENDED_CONVERSATION_OPTIONS,
"data": DEFAULT_CONVERSATION_OPTIONS,
"title": DEFAULT_CONVERSATION_NAME,
"unique_id": None,
},
{
"subentry_type": "ai_task_data",
"data": RECOMMENDED_AI_TASK_OPTIONS,
"data": DEFAULT_AI_TASK_OPTIONS,
"title": DEFAULT_AI_TASK_NAME,
"unique_id": None,
},
@@ -180,9 +174,9 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Add a subentry."""
if self._subentry_type == "ai_task_data":
self.options = RECOMMENDED_AI_TASK_OPTIONS.copy()
self.options = DEFAULT_AI_TASK_OPTIONS.copy()
else:
self.options = RECOMMENDED_CONVERSATION_OPTIONS.copy()
self.options = DEFAULT_CONVERSATION_OPTIONS.copy()
return await self.async_step_init()
async def async_step_reconfigure(
@@ -283,7 +277,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
step_schema: VolDictType = {
vol.Optional(
CONF_CHAT_MODEL,
default=RECOMMENDED_CHAT_MODEL,
default=DEFAULT[CONF_CHAT_MODEL],
): SelectSelector(
SelectSelectorConfig(
options=await self._get_model_list(), custom_value=True
@@ -291,11 +285,11 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
),
vol.Optional(
CONF_MAX_TOKENS,
default=RECOMMENDED_MAX_TOKENS,
default=DEFAULT[CONF_MAX_TOKENS],
): int,
vol.Optional(
CONF_TEMPERATURE,
default=RECOMMENDED_TEMPERATURE,
default=DEFAULT[CONF_TEMPERATURE],
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
}
@@ -325,12 +319,14 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
if not model.startswith(tuple(NON_THINKING_MODELS)):
step_schema[
vol.Optional(CONF_THINKING_BUDGET, default=RECOMMENDED_THINKING_BUDGET)
vol.Optional(
CONF_THINKING_BUDGET, default=DEFAULT[CONF_THINKING_BUDGET]
)
] = vol.All(
NumberSelector(
NumberSelectorConfig(
min=0,
max=self.options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
max=self.options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
)
),
vol.Coerce(int),
@@ -343,15 +339,15 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
{
vol.Optional(
CONF_WEB_SEARCH,
default=RECOMMENDED_WEB_SEARCH,
default=DEFAULT[CONF_WEB_SEARCH],
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=RECOMMENDED_WEB_SEARCH_MAX_USES,
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
): bool,
}
)
@@ -369,9 +365,10 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
user_input = {}
if user_input is not None:
if user_input.get(CONF_WEB_SEARCH, RECOMMENDED_WEB_SEARCH) and not errors:
if user_input.get(CONF_WEB_SEARCH, DEFAULT[CONF_WEB_SEARCH]) and not errors:
if user_input.get(
CONF_WEB_SEARCH_USER_LOCATION, RECOMMENDED_WEB_SEARCH_USER_LOCATION
CONF_WEB_SEARCH_USER_LOCATION,
DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
):
user_input.update(await self._get_location_data())
@@ -424,6 +421,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,
@@ -456,7 +455,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
}
)
response = await client.messages.create(
model=RECOMMENDED_CHAT_MODEL,
model=cast(str, DEFAULT[CONF_CHAT_MODEL]),
messages=[
{
"role": "user",
@@ -471,7 +470,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
"content": "{", # hints the model to skip any preamble
},
],
max_tokens=RECOMMENDED_MAX_TOKENS,
max_tokens=cast(int, DEFAULT[CONF_MAX_TOKENS]),
)
_LOGGER.debug("Model response: %s", response.content)
location_data = location_schema(

View File

@@ -11,25 +11,29 @@ DEFAULT_AI_TASK_NAME = "Claude AI Task"
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-5-haiku-latest"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 3000
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
CONF_WEB_SEARCH = "web_search"
RECOMMENDED_WEB_SEARCH = False
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
RECOMMENDED_WEB_SEARCH_MAX_USES = 5
CONF_WEB_SEARCH_CITY = "city"
CONF_WEB_SEARCH_REGION = "region"
CONF_WEB_SEARCH_COUNTRY = "country"
CONF_WEB_SEARCH_TIMEZONE = "timezone"
DEFAULT = {
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
CONF_MAX_TOKENS: 3000,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: 0,
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_USER_LOCATION: False,
CONF_WEB_SEARCH_MAX_USES: 5,
}
MIN_THINKING_BUDGET = 1024
NON_THINKING_MODELS = [
"claude-3-5", # Both sonnet and haiku
"claude-3-opus",

View File

@@ -84,14 +84,11 @@ from .const import (
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_TIMEZONE,
CONF_WEB_SEARCH_USER_LOCATION,
DEFAULT,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
NON_THINKING_MODELS,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
)
# Max number of back and forth with the LLM to generate a response
@@ -586,7 +583,7 @@ class AnthropicBaseLLMEntity(Entity):
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
entry_type=dr.DeviceEntryType.SERVICE,
)
@@ -604,17 +601,19 @@ class AnthropicBaseLLMEntity(Entity):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
model_args = MessageCreateParamsStreaming(
model=model,
messages=messages,
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
system=system.content,
stream=True,
)
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
thinking_budget = options.get(
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
)
if (
not model.startswith(tuple(NON_THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
@@ -625,7 +624,7 @@ class AnthropicBaseLLMEntity(Entity):
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
)
tools: list[ToolUnionParam] = []

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.73.0"]
"requirements": ["anthropic==0.75.0"]
}

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["zeroconf"],
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],

View File

@@ -1123,63 +1123,6 @@ class PipelineRun:
)
try:
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
if self.tts_stream and self.tts_stream.supports_streaming_input:
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
else:
@@ -1265,6 +1208,17 @@ class PipelineRun:
assert self.tts_stream is not None
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
with (
chat_session.async_get_chat_session(
self.hass, user_input.conversation_id
@@ -1276,6 +1230,53 @@ class PipelineRun:
chat_log_delta_listener=chat_log_delta_listener,
) as chat_log,
):
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input, chat_log
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
chat_log,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
# It was already handled, create response and add to chat history
if intent_response is not None:
speech: str = intent_response.speech.get("plain", {}).get(

View File

@@ -14,5 +14,19 @@
"start_conversation": {
"service": "mdi:forum"
}
},
"triggers": {
"idle": {
"trigger": "mdi:chat-sleep"
},
"listening": {
"trigger": "mdi:chat-question"
},
"processing": {
"trigger": "mdi:chat-processing"
},
"responding": {
"trigger": "mdi:chat-alert"
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.4.0"]
"requirements": ["hassil==3.5.0"]
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
"trigger_behavior_name": "Behavior"
},
"entity_component": {
"_": {
"name": "Assist satellite",
@@ -16,6 +20,13 @@
"id": "Answer ID",
"sentences": "Sentences"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
@@ -98,5 +109,47 @@
"name": "Start conversation"
}
},
"title": "Assist satellite"
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "Satellite started responding"
}
}
}

View File

@@ -0,0 +1,19 @@
"""Provides triggers for assist satellites."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
from .const import DOMAIN
from .entity import AssistSatelliteState
TRIGGERS: dict[str, type[Trigger]] = {
"idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
"listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
"processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
"responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for assist satellites."""
return TRIGGERS

View File

@@ -0,0 +1,20 @@
.trigger_common: &trigger_common
target:
entity:
domain: assist_satellite
fields:
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
idle: *trigger_common
listening: *trigger_common
processing: *trigger_common
responding: *trigger_common

View File

@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.1"]
}

View File

@@ -12,8 +12,9 @@ from typing import Any, Protocol, cast
from propcache.api import cached_property
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components import labs, websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.components.labs import async_listen as async_labs_listen
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@@ -114,6 +115,53 @@ ATTR_SOURCE = "source"
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"light",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"climate",
"cover",
"device_tracker",
"fan",
"lawn_mower",
"light",
"media_player",
"text",
"vacuum",
}
@callback
def is_disabled_experimental_condition(hass: HomeAssistant, platform: str) -> bool:
"""Check if the platform is a disabled experimental condition platform."""
return (
platform in _EXPERIMENTAL_CONDITION_PLATFORMS
and not labs.async_is_preview_feature_enabled(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
)
)
@callback
def is_disabled_experimental_trigger(hass: HomeAssistant, platform: str) -> bool:
"""Check if the platform is a disabled experimental trigger platform."""
return (
platform in _EXPERIMENTAL_TRIGGER_PLATFORMS
and not labs.async_is_preview_feature_enabled(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
)
)
class IfAction(Protocol):
"""Define the format of if_action."""
@@ -317,6 +365,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
schema=vol.Schema({vol.Optional(CONF_ID): str}),
)
@callback
def new_triggers_conditions_listener() -> None:
"""Handle new_triggers_conditions flag change."""
hass.async_create_task(
reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD))
)
async_labs_listen(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
new_triggers_conditions_listener,
)
websocket_api.async_register_command(hass, websocket_config)
return True

View File

@@ -6,5 +6,12 @@
"dependencies": ["blueprint", "trace"],
"documentation": "https://www.home-assistant.io/integrations/automation",
"integration_type": "system",
"preview_features": {
"new_triggers_conditions": {
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
}
},
"quality_scale": "internal"
}

View File

@@ -67,6 +67,14 @@
"title": "[%key:component::automation::common::validation_failed_title%]"
}
},
"preview_features": {
"new_triggers_conditions": {
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Purpose-specific triggers and conditions"
}
},
"services": {
"reload": {
"description": "Reloads the automation configuration.",

View File

@@ -1,7 +1,7 @@
{
"domain": "awair",
"name": "Awair",
"codeowners": ["@ahayworth", "@danielsjf"],
"codeowners": ["@ahayworth", "@ricohageman"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/awair",
"iot_class": "local_polling",

View File

@@ -0,0 +1,98 @@
rules:
# Bronze
action-setup:
status: exempt
comment: No actions defined
appropriate-polling:
status: done
comment: |
We fetch both user and devices, could probably slow one down
brands: done
common-modules: done
config-flow:
status: todo
comment: |
data_description fields are missing
Should not abort in cloud step when anything else than invalid access token
Find out why access token is optional
Discovered devices step is redundant
config-flow-test-coverage:
status: todo
comment: |
Move happy flow to the top and merge with `test_show_form`
Reuse `result`
Cloud tests should initialize with data directly
Tests should finish in CREATE_ENTRY
dependency-transparency:
status: todo
comment: |
Dependency is not built in the CI
docs-actions: todo
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: No explicit event subscription
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: No actions defined
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: done
test-coverage:
status: todo
comment: |
Patch objects where we use them
Use test helpers to load JSON
typo `no_devicess_fixture`
Make common config entries for cloud and local
Test setup of the integration
# Gold
devices:
status: done
comment: |
Can move to shorthand attribute
Can remove typecast
diagnostics: todo
discovery: done
discovery-update-info: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class:
status: done
comment: |
Can remove rounding
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: done
strict-typing: todo

View File

@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .websocket import BangOlufsenWebsocket
from .websocket import BeoWebsocket
@dataclass
class BangOlufsenData:
class BeoData:
"""Dataclass for API client and WebSocket client."""
websocket: BangOlufsenWebsocket
websocket: BeoWebsocket
client: MozartClient
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
type BeoConfigEntry = ConfigEntry[BeoData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove casts to str
assert entry.unique_id
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BangOlufsenWebsocket(hass, entry, client)
websocket = BeoWebsocket(hass, entry, client)
# Add the websocket and API client
entry.runtime_data = BangOlufsenData(websocket, client)
entry.runtime_data = BeoData(websocket, client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -82,9 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
"""Unload a config entry."""
# Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications()

View File

@@ -21,10 +21,10 @@ from .const import (
ATTR_ITEM_NUMBER,
ATTR_SERIAL_NUMBER,
ATTR_TYPE_NUMBER,
COMPATIBLE_MODELS,
CONF_SERIAL_NUMBER,
DEFAULT_MODEL,
DOMAIN,
SELECTABLE_MODELS,
)
from .util import get_serial_number_from_jid
@@ -47,7 +47,7 @@ _exception_map = {
}
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
_beolink_jid = ""
@@ -70,7 +70,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): SelectSelector(
SelectSelectorConfig(options=COMPATIBLE_MODELS)
SelectSelectorConfig(options=SELECTABLE_MODELS)
),
}
)

View File

@@ -14,15 +14,19 @@ from homeassistant.components.media_player import (
)
class BangOlufsenSource:
class BeoSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
BEO_STATES: dict[str, MediaPlayerState] = {
# Dict used for translating device states to Home Assistant states.
"started": MediaPlayerState.PLAYING,
"buffering": MediaPlayerState.PLAYING,
@@ -36,19 +40,19 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
}
# Dict used for translating Home Assistant settings to device repeat settings.
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BEO_REPEAT_FROM_HA.items()
}
# Media types for play_media
class BangOlufsenMediaType(StrEnum):
class BeoMediaType(StrEnum):
"""Bang & Olufsen specific media types."""
FAVOURITE = "favourite"
@@ -59,9 +63,10 @@ class BangOlufsenMediaType(StrEnum):
OVERLAY_TTS = "overlay_tts"
class BangOlufsenModel(StrEnum):
class BeoModel(StrEnum):
"""Enum for compatible model names."""
# Mozart devices
BEOCONNECT_CORE = "Beoconnect Core"
BEOLAB_8 = "BeoLab 8"
BEOLAB_28 = "BeoLab 28"
@@ -73,10 +78,22 @@ class BangOlufsenModel(StrEnum):
BEOSOUND_LEVEL = "Beosound Level"
BEOSOUND_PREMIERE = "Beosound Premiere"
BEOSOUND_THEATRE = "Beosound Theatre"
# Remote devices
BEOREMOTE_ONE = "Beoremote One"
class BeoAttribute(StrEnum):
"""Enum for extra_state_attribute keys."""
BEOLINK = "beolink"
BEOLINK_PEERS = "peers"
BEOLINK_SELF = "self"
BEOLINK_LEADER = "leader"
BEOLINK_LISTENERS = "listeners"
# Physical "buttons" on devices
class BangOlufsenButtons(StrEnum):
class BeoButtons(StrEnum):
"""Enum for device buttons."""
BLUETOOTH = "Bluetooth"
@@ -96,6 +113,7 @@ class WebsocketNotification(StrEnum):
"""Enum for WebSocket notification types."""
ACTIVE_LISTENING_MODE = "active_listening_mode"
BEO_REMOTE_BUTTON = "beo_remote_button"
BUTTON = "button"
PLAYBACK_ERROR = "playback_error"
PLAYBACK_METADATA = "playback_metadata"
@@ -113,6 +131,7 @@ class WebsocketNotification(StrEnum):
BEOLINK_AVAILABLE_LISTENERS = "beolinkAvailableListeners"
CONFIGURATION = "configuration"
NOTIFICATION = "notification"
REMOTE_CONTROL_DEVICES = "remoteControlDevices"
REMOTE_MENU_CHANGED = "remoteMenuChanged"
ALL = "all"
@@ -121,14 +140,18 @@ class WebsocketNotification(StrEnum):
DOMAIN: Final[str] = "bang_olufsen"
# Default values for configuration.
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
# Configuration.
CONF_SERIAL_NUMBER: Final = "serial_number"
CONF_BEOLINK_JID: Final = "jid"
# Models to choose from in manual configuration.
COMPATIBLE_MODELS: list[str] = [x.value for x in BangOlufsenModel]
SELECTABLE_MODELS: list[str] = [
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
]
MANUFACTURER: Final[str] = "Bang & Olufsen"
# Attribute names for zeroconf discovery.
ATTR_TYPE_NUMBER: Final[str] = "tn"
@@ -137,15 +160,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
ATTR_FRIENDLY_NAME: Final[str] = "fn"
# Power states.
BANG_OLUFSEN_ON: Final[str] = "on"
BEO_ON: Final[str] = "on"
VALID_MEDIA_TYPES: Final[tuple] = (
BangOlufsenMediaType.FAVOURITE,
BangOlufsenMediaType.DEEZER,
BangOlufsenMediaType.RADIO,
BangOlufsenMediaType.TTS,
BangOlufsenMediaType.TIDAL,
BangOlufsenMediaType.OVERLAY_TTS,
BeoMediaType.FAVOURITE,
BeoMediaType.DEEZER,
BeoMediaType.RADIO,
BeoMediaType.TTS,
BeoMediaType.TIDAL,
BeoMediaType.OVERLAY_TTS,
MediaType.MUSIC,
MediaType.URL,
MediaType.CHANNEL,
@@ -223,10 +246,14 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
)
# Device events
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = {
# Beoremote One
"KeyPress": "key_press",
"KeyRelease": "key_release",
# Physical "buttons"
"shortPress (Release)": "short_press_release",
"longPress (Timeout)": "long_press_timeout",
"longPress (Release)": "long_press_release",
@@ -236,7 +263,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
@@ -247,6 +274,70 @@ DEVICE_BUTTON_EVENTS: Final[list[str]] = [
"very_long_press_release",
]
BEO_REMOTE_SUBMENU_CONTROL: Final[str] = "Control"
BEO_REMOTE_SUBMENU_LIGHT: Final[str] = "Light"
# Common for both submenus
BEO_REMOTE_KEYS: Final[tuple[str, ...]] = (
"Blue",
"Digit0",
"Digit1",
"Digit2",
"Digit3",
"Digit4",
"Digit5",
"Digit6",
"Digit7",
"Digit8",
"Digit9",
"Down",
"Green",
"Left",
"Play",
"Red",
"Rewind",
"Right",
"Select",
"Stop",
"Up",
"Wind",
"Yellow",
"Func1",
"Func2",
"Func3",
"Func4",
"Func5",
"Func6",
"Func7",
"Func8",
"Func9",
"Func10",
"Func11",
"Func12",
"Func13",
"Func14",
"Func15",
"Func16",
"Func17",
)
# "keys" that are unique to the Control submenu
BEO_REMOTE_CONTROL_KEYS: Final[tuple[str, ...]] = (
"Func18",
"Func19",
"Func20",
"Func21",
"Func22",
"Func23",
"Func24",
"Func25",
"Func26",
"Func27",
)
BEO_REMOTE_KEY_EVENTS: Final[list[str]] = ["key_press", "key_release"]
# Beolink Converter NL/ML sources need to be transformed to upper case
BEOLINK_JOIN_SOURCES_TO_UPPER = (
"aux_a",

View File

@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import BangOlufsenConfigEntry
from . import BeoConfigEntry
from .const import DOMAIN
from .util import get_device_buttons
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
hass: HomeAssistant, config_entry: BeoConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""

View File

@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class BangOlufsenBase:
"""Base class for BangOlufsen Home Assistant objects."""
class BeoBase:
"""Base class for Bang & Olufsen Home Assistant objects."""
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the object."""
@@ -51,8 +51,8 @@ class BangOlufsenBase:
)
class BangOlufsenEntity(Entity, BangOlufsenBase):
"""Base Entity for BangOlufsen entities."""
class BeoEntity(Entity, BeoBase):
"""Base Entity for Bang & Olufsen entities."""
_attr_has_entity_name = True
_attr_should_poll = False

View File

@@ -2,43 +2,123 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from mozart_api.models import PairedRemote
from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BangOlufsenConfigEntry
from .const import CONNECTION_STATUS, DEVICE_BUTTON_EVENTS, WebsocketNotification
from .entity import BangOlufsenEntity
from .util import get_device_buttons
from . import BeoConfigEntry
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS,
BEO_REMOTE_KEYS,
BEO_REMOTE_SUBMENU_CONTROL,
BEO_REMOTE_SUBMENU_LIGHT,
CONNECTION_STATUS,
DEVICE_BUTTON_EVENTS,
DOMAIN,
MANUFACTURER,
BeoModel,
WebsocketNotification,
)
from .entity import BeoEntity
from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BangOlufsenConfigEntry,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sensor entities from config entry."""
"""Set up Event entities from config entry."""
entities: list[BeoEvent] = []
async_add_entities(
BangOlufsenButtonEvent(config_entry, button_type)
BeoButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
)
# Check for connected Beoremote One
remotes = await get_remotes(config_entry.runtime_data.client)
class BangOlufsenButtonEvent(BangOlufsenEntity, EventEntity):
"""Event class for Button events."""
for remote in remotes:
# Add Light keys
entities.extend(
[
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
)
for key_type in BEO_REMOTE_KEYS
]
)
# Add Control keys
entities.extend(
[
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
)
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
]
)
# If the remote is no longer available, then delete the device.
# The remote may appear as being available to the device after it has been unpaired on the remote
# As it has to be removed from the device on the app.
device_registry = dr.async_get(hass)
devices = device_registry.devices.get_devices_for_config_entry_id(
config_entry.entry_id
)
for device in devices:
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
remote.serial_number for remote in remotes
}:
device_registry.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id
)
async_add_entities(new_entities=entities)
class BeoEvent(BeoEntity, EventEntity):
"""Base Event class."""
_attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Initialize Event."""
super().__init__(config_entry, config_entry.runtime_data.client)
@callback
def _async_handle_event(self, event: str) -> None:
"""Handle event."""
self._trigger_event(event)
self.async_write_ha_state()
class BeoButtonEvent(BeoEvent):
"""Event class for Button events."""
_attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
"""Initialize Button."""
super().__init__(config_entry, config_entry.runtime_data.client)
super().__init__(config_entry)
self._attr_unique_id = f"{self._unique_id}_{button_type}"
@@ -52,20 +132,65 @@ class BangOlufsenButtonEvent(BangOlufsenEntity, EventEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{CONNECTION_STATUS}",
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.BUTTON}_{self._button_type}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BUTTON}_{self._button_type}",
self._async_handle_event,
)
)
@callback
def _async_handle_event(self, event: str) -> None:
"""Handle event."""
self._trigger_event(event)
self.async_write_ha_state()
class BeoRemoteKeyEvent(BeoEvent):
"""Event class for Beoremote One key events."""
_attr_event_types = BEO_REMOTE_KEY_EVENTS
def __init__(
self,
config_entry: BeoConfigEntry,
remote: PairedRemote,
key_type: str,
) -> None:
"""Initialize Beoremote One key."""
super().__init__(config_entry)
if TYPE_CHECKING:
assert remote.serial_number
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BeoModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,
via_device=(DOMAIN, self._unique_id),
)
# Make the native key name Home Assistant compatible
self._attr_translation_key = key_type.lower().replace("/", "_")
self._key_type = key_type
async def async_added_to_hass(self) -> None:
"""Listen to WebSocket Beoremote One key events."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BEO_REMOTE_BUTTON}_{self._key_type}",
self._async_handle_event,
)
)

View File

@@ -1,4 +1,278 @@
{
"entity": {
"event": {
"control_blue": {
"default": "mdi:remote"
},
"control_digit0": {
"default": "mdi:remote"
},
"control_digit1": {
"default": "mdi:remote"
},
"control_digit2": {
"default": "mdi:remote"
},
"control_digit3": {
"default": "mdi:remote"
},
"control_digit4": {
"default": "mdi:remote"
},
"control_digit5": {
"default": "mdi:remote"
},
"control_digit6": {
"default": "mdi:remote"
},
"control_digit7": {
"default": "mdi:remote"
},
"control_digit8": {
"default": "mdi:remote"
},
"control_digit9": {
"default": "mdi:remote"
},
"control_down": {
"default": "mdi:remote"
},
"control_func1": {
"default": "mdi:remote"
},
"control_func10": {
"default": "mdi:remote"
},
"control_func11": {
"default": "mdi:remote"
},
"control_func12": {
"default": "mdi:remote"
},
"control_func13": {
"default": "mdi:remote"
},
"control_func14": {
"default": "mdi:remote"
},
"control_func15": {
"default": "mdi:remote"
},
"control_func16": {
"default": "mdi:remote"
},
"control_func17": {
"default": "mdi:remote"
},
"control_func18": {
"default": "mdi:remote"
},
"control_func19": {
"default": "mdi:remote"
},
"control_func2": {
"default": "mdi:remote"
},
"control_func20": {
"default": "mdi:remote"
},
"control_func21": {
"default": "mdi:remote"
},
"control_func22": {
"default": "mdi:remote"
},
"control_func23": {
"default": "mdi:remote"
},
"control_func24": {
"default": "mdi:remote"
},
"control_func25": {
"default": "mdi:remote"
},
"control_func26": {
"default": "mdi:remote"
},
"control_func27": {
"default": "mdi:remote"
},
"control_func3": {
"default": "mdi:remote"
},
"control_func4": {
"default": "mdi:remote"
},
"control_func5": {
"default": "mdi:remote"
},
"control_func6": {
"default": "mdi:remote"
},
"control_func7": {
"default": "mdi:remote"
},
"control_func8": {
"default": "mdi:remote"
},
"control_func9": {
"default": "mdi:remote"
},
"control_green": {
"default": "mdi:remote"
},
"control_left": {
"default": "mdi:remote"
},
"control_play": {
"default": "mdi:remote"
},
"control_red": {
"default": "mdi:remote"
},
"control_rewind": {
"default": "mdi:remote"
},
"control_right": {
"default": "mdi:remote"
},
"control_select": {
"default": "mdi:remote"
},
"control_stop": {
"default": "mdi:remote"
},
"control_up": {
"default": "mdi:remote"
},
"control_wind": {
"default": "mdi:remote"
},
"control_yellow": {
"default": "mdi:remote"
},
"light_blue": {
"default": "mdi:remote"
},
"light_digit0": {
"default": "mdi:remote"
},
"light_digit1": {
"default": "mdi:remote"
},
"light_digit2": {
"default": "mdi:remote"
},
"light_digit3": {
"default": "mdi:remote"
},
"light_digit4": {
"default": "mdi:remote"
},
"light_digit5": {
"default": "mdi:remote"
},
"light_digit6": {
"default": "mdi:remote"
},
"light_digit7": {
"default": "mdi:remote"
},
"light_digit8": {
"default": "mdi:remote"
},
"light_digit9": {
"default": "mdi:remote"
},
"light_down": {
"default": "mdi:remote"
},
"light_func1": {
"default": "mdi:remote"
},
"light_func10": {
"default": "mdi:remote"
},
"light_func11": {
"default": "mdi:remote"
},
"light_func12": {
"default": "mdi:remote"
},
"light_func13": {
"default": "mdi:remote"
},
"light_func14": {
"default": "mdi:remote"
},
"light_func15": {
"default": "mdi:remote"
},
"light_func16": {
"default": "mdi:remote"
},
"light_func17": {
"default": "mdi:remote"
},
"light_func2": {
"default": "mdi:remote"
},
"light_func3": {
"default": "mdi:remote"
},
"light_func4": {
"default": "mdi:remote"
},
"light_func5": {
"default": "mdi:remote"
},
"light_func6": {
"default": "mdi:remote"
},
"light_func7": {
"default": "mdi:remote"
},
"light_func8": {
"default": "mdi:remote"
},
"light_func9": {
"default": "mdi:remote"
},
"light_green": {
"default": "mdi:remote"
},
"light_left": {
"default": "mdi:remote"
},
"light_play": {
"default": "mdi:remote"
},
"light_red": {
"default": "mdi:remote"
},
"light_rewind": {
"default": "mdi:remote"
},
"light_right": {
"default": "mdi:remote"
},
"light_select": {
"default": "mdi:remote"
},
"light_stop": {
"default": "mdi:remote"
},
"light_up": {
"default": "mdi:remote"
},
"light_wind": {
"default": "mdi:remote"
},
"light_yellow": {
"default": "mdi:remote"
}
}
},
"services": {
"beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" },
"beolink_expand": { "service": "mdi:location-enter" },

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

@@ -69,23 +69,25 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.util.dt import utcnow
from . import BangOlufsenConfigEntry
from . import BeoConfigEntry
from .const import (
BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
BEO_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA,
BEO_STATES,
BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID,
CONNECTION_STATUS,
DOMAIN,
FALLBACK_SOURCES,
MANUFACTURER,
VALID_MEDIA_TYPES,
BangOlufsenMediaType,
BangOlufsenSource,
BeoAttribute,
BeoMediaType,
BeoSource,
WebsocketNotification,
)
from .entity import BangOlufsenEntity
from .entity import BeoEntity
from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0
@@ -94,7 +96,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BANG_OLUFSEN_FEATURES = (
BEO_FEATURES = (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.GROUPING
@@ -117,15 +119,13 @@ BANG_OLUFSEN_FEATURES = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BangOlufsenConfigEntry,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Media Player entity from config entry."""
# Add MediaPlayer entity
async_add_entities(
new_entities=[
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
],
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)],
update_before_add=True,
)
@@ -185,7 +185,7 @@ async def async_setup_entry(
)
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
"""Representation of a media player."""
_attr_name = None
@@ -201,7 +201,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self._attr_device_info = DeviceInfo(
configuration_url=f"http://{self._host}/#/",
identifiers={(DOMAIN, self._unique_id)},
manufacturer="Bang & Olufsen",
manufacturer=MANUFACTURER,
model=self._model,
serial_number=self._unique_id,
)
@@ -223,7 +223,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Beolink compatible sources
self._beolink_sources: dict[str, bool] = {}
self._remote_leader: BeolinkLeader | None = None
# Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
# Extra state attributes:
# Beolink: peer(s), listener(s), leader and self
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
async def async_added_to_hass(self) -> None:
@@ -249,7 +250,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{signal}",
f"{DOMAIN}_{self._unique_id}_{signal}",
signal_handler,
)
)
@@ -285,7 +286,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None:
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
@@ -405,8 +406,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Check if source is line-in or optical and progress should be updated
if self._source_change.id in (
BangOlufsenSource.LINE_IN.id,
BangOlufsenSource.SPDIF.id,
BeoSource.LINE_IN.id,
BeoSource.SPDIF.id,
):
self._playback_progress = PlaybackProgress(progress=0)
@@ -435,7 +436,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
await self._async_update_beolink()
async def _async_update_beolink(self) -> None:
"""Update the current Beolink leader, listeners, peers and self."""
"""Update the current Beolink leader, listeners, peers and self.
Updates Home Assistant state.
"""
self._beolink_attributes = {}
@@ -444,18 +448,22 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Add Beolink self
self._beolink_attributes = {
"beolink": {"self": {self.device_entry.name: self._beolink_jid}}
BeoAttribute.BEOLINK: {
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid}
}
}
# Add Beolink peers
peers = await self._client.get_beolink_peers()
if len(peers) > 0:
self._beolink_attributes["beolink"]["peers"] = {}
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
] = {}
for peer in peers:
self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
peer.jid
)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
][peer.friendly_name] = peer.jid
# Add Beolink listeners / leader
self._remote_leader = self._playback_metadata.remote_leader
@@ -476,7 +484,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Add self
group_members.append(self.entity_id)
self._beolink_attributes["beolink"]["leader"] = {
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LEADER
] = {
self._remote_leader.friendly_name: self._remote_leader.jid,
}
@@ -513,9 +523,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
beolink_listener.jid
)
break
self._beolink_attributes["beolink"]["listeners"] = (
beolink_listeners_attribute
)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LISTENERS
] = beolink_listeners_attribute
self._attr_group_members = group_members
@@ -573,7 +583,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, 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
@@ -586,7 +596,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BANG_OLUFSEN_FEATURES
features = BEO_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
@@ -597,7 +607,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
return BANG_OLUFSEN_STATES[self._state]
return BEO_STATES[self._state]
@property
def volume_level(self) -> float | None:
@@ -614,11 +624,18 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
return None
@property
def media_content_type(self) -> str:
def media_content_type(self) -> MediaType | str | None:
"""Return the current media type."""
# Hard to determine content type
if self._source_change.id == BangOlufsenSource.URI_STREAMER.id:
return MediaType.URL
content_type = {
BeoSource.URI_STREAMER.id: MediaType.URL,
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
}
# Hard to determine content type.
if self._source_change.id in content_type:
return content_type[self._source_change.id]
return MediaType.MUSIC
@property
@@ -631,6 +648,11 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Return the current playback progress."""
return self._playback_progress.progress
@property
def media_content_id(self) -> str | None:
"""Return internal ID of Deezer, Tidal and radio stations."""
return self._playback_metadata.source_internal_id
@property
def media_image_url(self) -> str | None:
"""Return URL of the currently playing music."""
@@ -739,9 +761,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat])
)
async def async_set_shuffle(self, shuffle: bool) -> None:
@@ -845,7 +865,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self._volume.level.level + offset_volume, 100
)
if media_type == BangOlufsenMediaType.OVERLAY_TTS:
if media_type == BeoMediaType.OVERLAY_TTS:
# Bang & Olufsen cloud TTS
overlay_play_request.text_to_speech = (
OverlayPlayRequestTextToSpeechTextToSpeech(
@@ -862,14 +882,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# The "provider" media_type may not be suitable for overlay all the time.
# Use it for now.
elif media_type == BangOlufsenMediaType.TTS:
elif media_type == BeoMediaType.TTS:
await self._client.post_overlay_play(
overlay_play_request=OverlayPlayRequest(
uri=Uri(location=media_id),
)
)
elif media_type == BangOlufsenMediaType.RADIO:
elif media_type == BeoMediaType.RADIO:
await self._client.run_provided_scene(
scene_properties=SceneProperties(
action_list=[
@@ -881,13 +901,13 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
)
)
elif media_type == BangOlufsenMediaType.FAVOURITE:
elif media_type == BeoMediaType.FAVOURITE:
await self._client.activate_preset(id=int(media_id))
elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL):
try:
# Play Deezer flow.
if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
if media_id == "flow" and media_type == BeoMediaType.DEEZER:
deezer_id = None
if "id" in kwargs[ATTR_MEDIA_EXTRA]:

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,16 @@
from __future__ import annotations
from typing import cast
from mozart_api.models import PairedRemote
from mozart_api.mozart_client import MozartClient
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -23,16 +28,39 @@ def get_serial_number_from_jid(jid: str) -> str:
return jid.split(".")[2].split("@")[0]
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
async def get_remotes(client: MozartClient) -> list[PairedRemote]:
"""Get paired remotes."""
bluetooth_remote_list = await client.get_bluetooth_remotes()
return [
remote
for remote in cast(list[PairedRemote], bluetooth_remote_list.items)
if remote.serial_number is not None
]
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 == BangOlufsenModel.BEOSOUND_PREMIERE:
buttons.remove(BangOlufsenButtons.BLUETOOTH)
# Models that don't have a microphone button
if model in (
BeoModel.BEOSOUND_A5,
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Beoconnect Core does not have any buttons
elif model == BangOlufsenModel.BEOCONNECT_CORE:
buttons = []
# Models that don't have a Bluetooth button
if model in (
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
return buttons

View File

@@ -6,6 +6,7 @@ import logging
from typing import TYPE_CHECKING
from mozart_api.models import (
BeoRemoteButton,
ButtonEvent,
ListeningModeProps,
PlaybackContentMetadata,
@@ -26,18 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum
from .const import (
BANG_OLUFSEN_WEBSOCKET_EVENT,
BEO_WEBSOCKET_EVENT,
CONNECTION_STATUS,
DOMAIN,
EVENT_TRANSLATION_MAP,
BeoModel,
WebsocketNotification,
)
from .entity import BangOlufsenBase
from .util import get_device
from .entity import BeoBase
from .util import get_device, get_remotes
_LOGGER = logging.getLogger(__name__)
class BangOlufsenWebsocket(BangOlufsenBase):
class BeoWebsocket(BeoBase):
"""The WebSocket listeners."""
def __init__(
@@ -45,7 +48,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
) -> None:
"""Initialize the WebSocket listeners."""
BangOlufsenBase.__init__(self, entry, client)
BeoBase.__init__(self, entry, client)
self.hass = hass
self._device = get_device(hass, self._unique_id)
@@ -57,6 +60,9 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self._client.get_active_listening_mode_notifications(
self.on_active_listening_mode
)
self._client.get_beo_remote_button_notifications(
self.on_beo_remote_button_notification
)
self._client.get_button_notifications(self.on_button_notification)
self._client.get_playback_error_notifications(
@@ -87,7 +93,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Update all entities of the connection status."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{CONNECTION_STATUS}",
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._client.websocket_connected,
)
@@ -105,10 +111,22 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send active_listening_mode dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.ACTIVE_LISTENING_MODE}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.ACTIVE_LISTENING_MODE}",
notification,
)
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
"""Send beo_remote_button dispatch."""
if TYPE_CHECKING:
assert notification.type
# Send to event entity
async_dispatcher_send(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BEO_REMOTE_BUTTON}_{notification.key}",
EVENT_TRANSLATION_MAP[notification.type],
)
def on_button_notification(self, notification: ButtonEvent) -> None:
"""Send button dispatch."""
# State is expected to always be available.
@@ -118,11 +136,11 @@ class BangOlufsenWebsocket(BangOlufsenBase):
# Send to event entity
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.BUTTON}_{notification.button}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BUTTON}_{notification.button}",
EVENT_TRANSLATION_MAP[notification.state],
)
def on_notification_notification(
async def on_notification_notification(
self, notification: WebsocketNotificationTag
) -> None:
"""Send notification dispatch."""
@@ -136,24 +154,51 @@ class BangOlufsenWebsocket(BangOlufsenBase):
):
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.BEOLINK}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BEOLINK}",
)
elif notification_type is WebsocketNotification.CONFIGURATION:
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.CONFIGURATION}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.CONFIGURATION}",
)
elif notification_type is WebsocketNotification.REMOTE_MENU_CHANGED:
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}",
)
# This notification is triggered by a remote pairing, unpairing and connecting to a device
# So the current remote devices have to be compared to available remotes to determine action
elif notification_type is WebsocketNotification.REMOTE_CONTROL_DEVICES:
device_registry = dr.async_get(self.hass)
# Get remote devices connected to the device from Home Assistant
device_serial_numbers = [
device.serial_number
for device in device_registry.devices.get_devices_for_config_entry_id(
self.entry.entry_id
)
if device.serial_number is not None
and device.model == BeoModel.BEOREMOTE_ONE
]
# Get paired remotes from device
remote_serial_numbers = [
remote.serial_number
for remote in await get_remotes(self._client)
if remote.serial_number is not None
]
# Check if number of remote devices correspond to number of paired remotes
if len(remote_serial_numbers) != len(device_serial_numbers):
_LOGGER.info(
"A Beoremote One has been paired or unpaired to %s. Reloading config entry to add device and entities",
self.entry.title,
)
self.hass.config_entries.async_schedule_reload(self.entry.entry_id)
def on_playback_error_notification(self, notification: PlaybackError) -> None:
"""Send playback_error dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}",
notification,
)
@@ -163,7 +208,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send playback_metadata dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}",
notification,
)
@@ -171,7 +216,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send playback_progress dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}",
notification,
)
@@ -179,7 +224,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send playback_state dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}",
notification,
)
@@ -187,7 +232,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send playback_source dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_SOURCE}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.PLAYBACK_SOURCE}",
notification,
)
@@ -195,7 +240,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send source_change dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}",
notification,
)
@@ -203,7 +248,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
"""Send volume dispatch."""
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.VOLUME}",
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.VOLUME}",
notification,
)
@@ -229,4 +274,4 @@ class BangOlufsenWebsocket(BangOlufsenBase):
}
_LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification)

View File

@@ -174,5 +174,13 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -317,5 +321,36 @@
}
}
},
"title": "Binary sensor"
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
}

View File

@@ -0,0 +1,67 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domain: str = DOMAIN
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_state = to_state
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS

View File

@@ -0,0 +1,25 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
occupancy_cleared:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy
occupancy_detected:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy

View File

@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==1.0.1",
"bleak==2.0.0",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.0.0",
"habluetooth==5.7.0"
"dbus-fast==3.1.2",
"habluetooth==5.8.0"
]
}

View File

@@ -1 +0,0 @@
"""The bluetooth_tracker component."""

View File

@@ -1,10 +0,0 @@
"""Constants for the Bluetooth Tracker component."""
from typing import Final
DOMAIN: Final = "bluetooth_tracker"
SERVICE_UPDATE: Final = "update"
BT_PREFIX: Final = "BT_"
CONF_REQUEST_RSSI: Final = "request_rssi"
DEFAULT_DEVICE_ID: Final = -1

View File

@@ -1,213 +0,0 @@
"""Tracking for bluetooth devices."""
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
from typing import Final
import bluetooth
from bt_proximity import BluetoothRSSI
import voluptuous as vol
from homeassistant.components.device_tracker import (
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
DEFAULT_TRACK_NEW,
PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
SCAN_INTERVAL,
SourceType,
)
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES,
AsyncSeeCallback,
Device,
async_load_config,
)
from homeassistant.const import CONF_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
BT_PREFIX,
CONF_REQUEST_RSSI,
DEFAULT_DEVICE_ID,
DOMAIN,
SERVICE_UPDATE,
)
_LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_REQUEST_RSSI): cv.boolean,
vol.Optional(CONF_DEVICE_ID, default=DEFAULT_DEVICE_ID): vol.All(
vol.Coerce(int), vol.Range(min=-1)
),
}
)
def is_bluetooth_device(device: Device) -> bool:
"""Check whether a device is a bluetooth device by its mac."""
return device.mac is not None and device.mac[:3].upper() == BT_PREFIX
def discover_devices(device_id: int) -> list[tuple[str, str]]:
"""Discover Bluetooth devices."""
try:
result = bluetooth.discover_devices(
duration=8,
lookup_names=True,
flush_cache=True,
lookup_class=False,
device_id=device_id,
)
except OSError as ex:
# OSError is generally thrown if a bluetooth device isn't found
_LOGGER.error("Couldn't discover bluetooth devices: %s", ex)
return []
_LOGGER.debug("Bluetooth devices discovered = %d", len(result))
return result # type: ignore[no-any-return]
async def see_device(
hass: HomeAssistant,
async_see: AsyncSeeCallback,
mac: str,
device_name: str,
rssi: tuple[int] | None = None,
) -> None:
"""Mark a device as seen."""
attributes = {}
if rssi is not None:
attributes["rssi"] = rssi
await async_see(
mac=f"{BT_PREFIX}{mac}",
host_name=device_name,
attributes=attributes,
source_type=SourceType.BLUETOOTH,
)
async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]]:
"""Load all known devices.
We just need the devices so set consider_home and home range to 0
"""
yaml_path: str = hass.config.path(YAML_DEVICES)
devices = await async_load_config(yaml_path, hass, timedelta(0))
bluetooth_devices = [device for device in devices if is_bluetooth_device(device)]
devices_to_track: set[str] = {
device.mac[3:]
for device in bluetooth_devices
if device.track and device.mac is not None
}
devices_to_not_track: set[str] = {
device.mac[3:]
for device in bluetooth_devices
if not device.track and device.mac is not None
}
return devices_to_track, devices_to_not_track
def lookup_name(mac: str) -> str | None:
"""Lookup a Bluetooth device name."""
_LOGGER.debug("Scanning %s", mac)
return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return]
async def async_setup_scanner(
hass: HomeAssistant,
config: ConfigType,
async_see: AsyncSeeCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
"""Set up the Bluetooth Scanner."""
device_id: int = config[CONF_DEVICE_ID]
interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
request_rssi: bool = config.get(CONF_REQUEST_RSSI, False)
update_bluetooth_lock = asyncio.Lock()
# If track new devices is true discover new devices on startup.
track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
_LOGGER.debug("Tracking new devices is set to %s", track_new)
devices_to_track, devices_to_not_track = await get_tracking_devices(hass)
if not devices_to_track and not track_new:
_LOGGER.debug("No Bluetooth devices to track and not tracking new devices")
if request_rssi:
_LOGGER.debug("Detecting RSSI for devices")
async def perform_bluetooth_update() -> None:
"""Discover Bluetooth devices and update status."""
_LOGGER.debug("Performing Bluetooth devices discovery and update")
tasks: list[asyncio.Task[None]] = []
try:
if track_new:
devices = await hass.async_add_executor_job(discover_devices, device_id)
for mac, _device_name in devices:
if mac not in devices_to_track and mac not in devices_to_not_track:
devices_to_track.add(mac)
for mac in devices_to_track:
friendly_name = await hass.async_add_executor_job(lookup_name, mac)
if friendly_name is None:
# Could not lookup device name
continue
rssi = None
if request_rssi:
client = BluetoothRSSI(mac)
rssi = await hass.async_add_executor_job(client.request_rssi)
client.close()
tasks.append(
asyncio.create_task(
see_device(hass, async_see, mac, friendly_name, rssi)
)
)
if tasks:
await asyncio.wait(tasks)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
async def update_bluetooth(now: datetime | None = None) -> None:
"""Lookup Bluetooth devices and update status."""
# If an update is in progress, we don't do anything
if update_bluetooth_lock.locked():
_LOGGER.debug(
(
"Previous execution of update_bluetooth is taking longer than the"
" scheduled update of interval %s"
),
interval,
)
return
async with update_bluetooth_lock:
await perform_bluetooth_update()
async def handle_manual_update_bluetooth(call: ServiceCall) -> None:
"""Update bluetooth devices on demand."""
await update_bluetooth()
hass.async_create_task(update_bluetooth())
async_track_time_interval(hass, update_bluetooth, interval)
hass.services.async_register(DOMAIN, SERVICE_UPDATE, handle_manual_update_bluetooth)
return True

View File

@@ -1,7 +0,0 @@
{
"services": {
"update": {
"service": "mdi:update"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"domain": "bluetooth_tracker",
"name": "Bluetooth Tracker",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker",
"iot_class": "local_polling",
"loggers": ["bluetooth", "bt_proximity"],
"quality_scale": "legacy",
"requirements": ["bt-proximity==0.2.1", "PyBluez==0.22"]
}

View File

@@ -1,8 +0,0 @@
{
"services": {
"update": {
"description": "Triggers manual tracker update.",
"name": "Update"
}
}
}

View File

@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
model=panel.model,
model=panel.model.name,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -83,7 +83,7 @@ async def try_connect(
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
return (panel.model.name, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -20,7 +20,8 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"model": entry.runtime_data.model.name,
"family": entry.runtime_data.model.family.name,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,

View File

@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
)

View File

@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.6"]
"requirements": ["bosch-alarm-mode2==0.4.10"]
}

View File

@@ -36,6 +36,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/broadlink",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["broadlink"],
"requirements": ["broadlink==0.19.0"]

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:80",
"stun:stun.home-assistant.io:3478",
]
),
]
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

@@ -96,5 +96,22 @@
"turn_on": {
"service": "mdi:power-on"
}
},
"triggers": {
"started_cooling": {
"trigger": "mdi:snowflake"
},
"started_drying": {
"trigger": "mdi:water-percent"
},
"started_heating": {
"trigger": "mdi:fire"
},
"turned_off": {
"trigger": "mdi:power-off"
},
"turned_on": {
"trigger": "mdi:power-on"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"action_type": {
"set_hvac_mode": "Change HVAC mode on {entity_name}",
@@ -187,6 +191,13 @@
"heat_cool": "Heat/cool",
"off": "[%key:common::state::off%]"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
@@ -285,5 +296,57 @@
"name": "[%key:common::action::turn_on%]"
}
},
"title": "Climate"
"title": "Climate",
"triggers": {
"started_cooling": {
"description": "Triggers after one or more climate-control devices start cooling.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started cooling"
},
"started_drying": {
"description": "Triggers after one or more climate-control devices start drying.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started drying"
},
"started_heating": {
"description": "Triggers after one or more climate-control devices start heating.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started heating"
},
"turned_off": {
"description": "Triggers after one or more climate-control devices turn off.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned off"
},
"turned_on": {
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device turned on"
}
}
}

View File

@@ -0,0 +1,43 @@
"""Provides triggers for climates."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import (
Trigger,
make_conditional_entity_state_trigger,
make_entity_state_attribute_trigger,
make_entity_state_trigger,
)
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
TRIGGERS: dict[str, type[Trigger]] = {
"started_cooling": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
),
"started_drying": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
),
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_conditional_entity_state_trigger(
DOMAIN,
from_states={
HVACMode.OFF,
},
to_states={
HVACMode.AUTO,
HVACMode.COOL,
HVACMode.DRY,
HVACMode.FAN_ONLY,
HVACMode.HEAT,
HVACMode.HEAT_COOL,
},
),
"started_heating": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.HEATING
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for climates."""
return TRIGGERS

View File

@@ -0,0 +1,21 @@
.trigger_common: &trigger_common
target:
entity:
domain: climate
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
started_cooling: *trigger_common
started_drying: *trigger_common
started_heating: *trigger_common
turned_off: *trigger_common
turned_on: *trigger_common

View File

@@ -4,12 +4,13 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from contextlib import suppress
from datetime import datetime, timedelta
from enum import Enum
import logging
from typing import Any, cast
from hass_nabucasa import Cloud
from hass_nabucasa import Cloud, NabuCasaBaseError
import voluptuous as vol
from homeassistant.components import alexa, google_assistant
@@ -77,7 +78,16 @@ from .subscription import async_subscription_info
DEFAULT_MODE = MODE_PROD
PLATFORMS = [Platform.BINARY_SENSOR, Platform.STT, Platform.TTS]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.STT,
Platform.TTS,
]
LLM_PLATFORMS = [
Platform.AI_TASK,
Platform.CONVERSATION,
]
SERVICE_REMOTE_CONNECT = "remote_connect"
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
@@ -425,7 +435,14 @@ def _handle_prefs_updated(hass: HomeAssistant, cloud: Cloud[CloudClient]) -> Non
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
platforms = PLATFORMS.copy()
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
with suppress(NabuCasaBaseError):
await cloud.llm.async_ensure_token()
platforms += LLM_PLATFORMS
await hass.config_entries.async_forward_entry_setups(entry, platforms)
entry.runtime_data = {"platforms": platforms}
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
stt_tts_entities_added.set()
@@ -434,7 +451,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
return await hass.config_entries.async_unload_platforms(
entry, entry.runtime_data["platforms"]
)
@callback

View File

@@ -0,0 +1,195 @@
"""AI Task integration for Home Assistant Cloud."""
from __future__ import annotations
import io
from json import JSONDecodeError
import logging
from hass_nabucasa.llm import (
LLMAuthenticationError,
LLMError,
LLMImageAttachment,
LLMRateLimitError,
LLMResponseError,
LLMServiceError,
)
from PIL import Image
from homeassistant.components import ai_task, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
from .const import AI_TASK_ENTITY_UNIQUE_ID, DATA_CLOUD
from .entity import BaseCloudLLMEntity
_LOGGER = logging.getLogger(__name__)
def _convert_image_for_editing(data: bytes) -> tuple[bytes, str]:
"""Ensure the image data is in a format accepted by OpenAI image edits."""
stream = io.BytesIO(data)
with Image.open(stream) as img:
mode = img.mode
if mode not in ("RGBA", "LA", "L"):
img = img.convert("RGBA")
output = io.BytesIO()
if img.mode in ("RGBA", "LA", "L"):
img.save(output, format="PNG")
return output.getvalue(), "image/png"
img.save(output, format=img.format or "PNG")
return output.getvalue(), f"image/{(img.format or 'png').lower()}"
async def async_prepare_image_generation_attachments(
hass: HomeAssistant, attachments: list[conversation.Attachment]
) -> list[LLMImageAttachment]:
"""Load attachment data for image generation."""
def prepare() -> list[LLMImageAttachment]:
items: list[LLMImageAttachment] = []
for attachment in attachments:
if not attachment.mime_type or not attachment.mime_type.startswith(
"image/"
):
raise HomeAssistantError(
"Only image attachments are supported for image generation"
)
path = attachment.path
if not path.exists():
raise HomeAssistantError(f"`{path}` does not exist")
data = path.read_bytes()
mime_type = attachment.mime_type
try:
data, mime_type = _convert_image_for_editing(data)
except HomeAssistantError:
raise
except Exception as err:
raise HomeAssistantError("Failed to process image attachment") from err
items.append(
LLMImageAttachment(
filename=path.name,
mime_type=mime_type,
data=data,
)
)
return items
return await hass.async_add_executor_job(prepare)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Home Assistant Cloud AI Task entity."""
cloud = hass.data[DATA_CLOUD]
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
"""Home Assistant Cloud AI Task entity."""
_attr_has_entity_name = True
_attr_supported_features = (
ai_task.AITaskEntityFeature.GENERATE_DATA
| ai_task.AITaskEntityFeature.GENERATE_IMAGE
| ai_task.AITaskEntityFeature.SUPPORT_ATTACHMENTS
)
_attr_translation_key = "cloud_ai"
_attr_unique_id = AI_TASK_ENTITY_UNIQUE_ID
@property
def available(self) -> bool:
"""Return if the entity is available."""
return self._cloud.is_logged_in and self._cloud.valid_subscription
async def _async_generate_data(
self,
task: ai_task.GenDataTask,
chat_log: conversation.ChatLog,
) -> ai_task.GenDataTaskResult:
"""Handle a generate data task."""
await self._async_handle_chat_log(
"ai_task", chat_log, task.name, task.structure
)
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
raise HomeAssistantError(
"Last content in chat log is not an AssistantContent"
)
text = chat_log.content[-1].content or ""
if not task.structure:
return ai_task.GenDataTaskResult(
conversation_id=chat_log.conversation_id,
data=text,
)
try:
data = json_loads(text)
except JSONDecodeError as err:
_LOGGER.error(
"Failed to parse JSON response: %s. Response: %s",
err,
text,
)
raise HomeAssistantError("Error with OpenAI structured response") from err
return ai_task.GenDataTaskResult(
conversation_id=chat_log.conversation_id,
data=data,
)
async def _async_generate_image(
self,
task: ai_task.GenImageTask,
chat_log: conversation.ChatLog,
) -> ai_task.GenImageTaskResult:
"""Handle a generate image task."""
attachments: list[LLMImageAttachment] | None = None
if task.attachments:
attachments = await async_prepare_image_generation_attachments(
self.hass, task.attachments
)
try:
if attachments is None:
image = await self._cloud.llm.async_generate_image(
prompt=task.instructions,
)
else:
image = await self._cloud.llm.async_edit_image(
prompt=task.instructions,
attachments=attachments,
)
except LLMAuthenticationError as err:
raise HomeAssistantError("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:
raise HomeAssistantError(str(err)) from err
except LLMServiceError as err:
raise HomeAssistantError("Error talking to Cloud LLM") from err
except LLMError as err:
raise HomeAssistantError(str(err)) from err
return ai_task.GenImageTaskResult(
conversation_id=chat_log.conversation_id,
mime_type=image["mime_type"],
image_data=image["image_data"],
model=image.get("model"),
width=image.get("width"),
height=image.get("height"),
revised_prompt=image.get("revised_prompt"),
)

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