Compare commits

..

173 Commits

Author SHA1 Message Date
Franck Nijhof
9c640fe0fa 2026.2.3 (#163683) 2026-02-20 21:43:32 +01:00
Sid
62145e5f9e Bump eheimdigital to 1.6.0 (#161961) 2026-02-20 19:51:10 +00:00
Franck Nijhof
c0fc414bb9 Fix nrgkick tests for rc 2026-02-20 19:49:27 +00:00
Franck Nijhof
69411a05ff Bump version to 2026.2.3 2026-02-20 19:39:05 +00:00
Marc Mueller
06c9ec861d Fix hassfest requirements check (#163681) 2026-02-20 19:38:58 +00:00
Joost Lekkerkerker
946df1755f Bump pySmartThings to 3.5.3 (#163375)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-20 19:38:56 +00:00
Thomas Sejr Madsen
d0678e0641 Fix touchline_sl zone availability when alarm state is set (#163338) 2026-02-20 19:38:55 +00:00
Allen Porter
ec56f183da Bump pyrainbird to 6.0.5 (#163333) 2026-02-20 19:38:53 +00:00
Åke Strandberg
033005e0de Add Miele dishwasher program code (#163308) 2026-02-20 19:38:52 +00:00
Andreas Jakl
91f9f5a826 NRGkick: do not update vehicle connected timestamp when vehicle is not connected (#163292) 2026-02-20 19:38:51 +00:00
David Recordon
ac4fcab827 Fix Control4 HVAC action mapping for multi-stage and idle states (#163222) 2026-02-20 19:38:49 +00:00
Allen Porter
d0eea77178 Fix remote calendar event handling of events within the same update period (#163186)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-20 19:38:48 +00:00
Markus Adrario
fb38fa3844 Add Lux to homee units (#163180) 2026-02-20 19:38:47 +00:00
Allen Porter
440efb953e Bump ical to 13.2.0 (#163123) 2026-02-20 19:38:45 +00:00
Manu
7ce47cca0d Fix blocking call in Xbox config flow (#163122) 2026-02-20 19:38:44 +00:00
Andre Lengwenus
a5f607bb91 Bump pypck to 0.9.11 (#163043) 2026-02-20 19:38:42 +00:00
Andre Lengwenus
b03043aa6f Bump pypck to 0.9.10 (#162333) 2026-02-20 19:38:41 +00:00
Robert Resch
0f3c7ca277 Block redirect to localhost (#162941) 2026-02-20 19:37:03 +00:00
Martin Hjelmare
3abf7c22f3 Fix Z-Wave climate set preset (#162728) 2026-02-20 19:37:01 +00:00
hbludworth
292e1de126 Show progress indicator during backup stage of Core/App update (#162683) 2026-02-20 19:37:00 +00:00
Christian Lackas
2d776a8193 Fix HomematicIP entity recovery after access point cloud reconnect (#162575) 2026-02-20 19:36:58 +00:00
Sid
039bbbb48c Fix dynamic entity creation in eheimdigital (#161155) 2026-02-20 19:36:56 +00:00
Luke Lashley
ad5565df95 Add the ability to select region for Roborock (#160898) 2026-02-20 19:36:55 +00:00
Franck Nijhof
3e6bc29a6a 2026.2.2 (#162950) 2026-02-13 21:05:06 +01:00
Franck Nijhof
ec8067a5a8 Bump version to 2026.2.2 2026-02-13 19:25:16 +00:00
Josef Zweck
6f47716d0a Log remaining token duration in onedrive (#162933) 2026-02-13 19:24:25 +00:00
puddly
efba5c6bcc Bump ZHA to 0.0.90 (#162894) 2026-02-13 19:24:24 +00:00
Sammy [Andrei Marinache]
d10e78079f Add Miele TQ1000WP tumble dryer programs and program phases (#162871)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Åke Strandberg <ake@strandberg.eu>
2026-02-13 19:24:23 +00:00
Jon Seager
6d4581580f Bump pytouchlinesl to 0.6.0 (#162856) 2026-02-13 19:24:21 +00:00
Yoshi Walsh
0d9a41a540 Bump pydaikin to 2.17.2 (#162846) 2026-02-13 19:24:20 +00:00
Vicx
cd69e6db73 Bump slixmpp to 1.13.2 (#162837) 2026-02-13 19:24:19 +00:00
Xitee
1320367d0d Filter out transient zero values from qBittorrent alltime stats (#162821)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:24:18 +00:00
Joost Lekkerkerker
dfa4698887 Bump pySmartThings to 3.5.2 (#162809)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-13 19:24:17 +00:00
Robert Resch
b426115de7 Bump cryptography to 46.0.5 (#162783) 2026-02-13 19:24:15 +00:00
hanwg
fb79fa37f8 Fix bug in edit_message_media action for Telegram bot (#162762) 2026-02-13 19:24:14 +00:00
Simone Chemelli
6a5f7bf424 Fix image platform state for Vodafone Station (#162747)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-13 19:24:13 +00:00
Simone Chemelli
142ca6dec1 Fix alarm refresh warning for Comelit SimpleHome (#162710) 2026-02-13 19:24:12 +00:00
epenet
0f986c24d0 Fix unavailable status in Tuya (#162709) 2026-02-13 19:24:11 +00:00
Josef Zweck
01f2b7b6f6 Bump onedrive-personal-sdk to 0.1.2 (#162689) 2026-02-13 19:24:09 +00:00
Michael
b9469027f5 Fix handling when FRITZ!Box reboots in FRITZ!Box Tools (#162679) 2026-02-13 19:24:08 +00:00
Tomás Correia
fbb94af748 fix to cloudflare r2 setup screen info (#162677) 2026-02-13 19:24:07 +00:00
Michael
148bdf6e3a Fix handling when FRITZ!Box reboots in FRITZ!Smarthome (#162676) 2026-02-13 19:24:05 +00:00
starkillerOG
91999f8871 Bump reolink-aio to 0.19.0 (#162672) 2026-02-13 19:24:04 +00:00
Jeef
aecca4eb99 Bump intellifire4py to 4.3.1 (#162659) 2026-02-13 19:24:03 +00:00
Allen Porter
bf8aa49bae Improve MCP SSE fallback error handling (#162655) 2026-02-13 19:24:02 +00:00
Joost Lekkerkerker
4423425683 Pin setuptools to 81.0.0 (#162589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 19:24:01 +00:00
Aaron Godfrey
44202da53d Increase max tasks retrieved per page to prevent timeout (#162587) 2026-02-13 19:23:59 +00:00
Thomas55555
9f7dfb72c4 Bump aioautomower to 2.7.3 (#162583)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-13 19:23:58 +00:00
Michael
de07a69e4f Bump aioimmich to 0.12.0 (#162573)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-13 19:23:57 +00:00
Maikel Punie
bbf4c38115 migrate velbus config entries (#162565) 2026-02-13 19:23:56 +00:00
ElCruncharino
e1bb5d52ef Add timeout to B2 metadata downloads to prevent backup hang (#162562) 2026-02-13 19:23:54 +00:00
hanwg
eb64b6bdee Fix config flow bug for Telegram bot (#162555) 2026-02-13 19:23:53 +00:00
Andrea Turri
ecb288b735 Add new Miele mappings (#162544) 2026-02-13 19:23:52 +00:00
Norbert Rittel
a419c9c420 Sentence-case "speech-to-text" in google_cloud (#162534) 2026-02-13 19:23:51 +00:00
Brett Adams
dd29133324 Fix Tesla Fleet partner registration to use all regions (#162525)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:23:50 +00:00
Allen Porter
90f22ea516 Bump grpc to 1.78.0 (#162520) 2026-02-13 19:23:48 +00:00
Peter Grauvogel
9db1428265 Fix Green Planet Energy price unit conversion (#162511) 2026-02-13 19:23:47 +00:00
Denis Shulyaka
a696b05b0d Fix JSON serialization of time objects in Cloud conversation tool results (#162506) 2026-02-13 19:23:46 +00:00
Denis Shulyaka
77ddb63b73 Fix JSON serialization of time objects in Open Router tool results (#162505) 2026-02-13 19:23:44 +00:00
Denis Shulyaka
4180a6e176 Fix JSON serialization of time objects in Ollama tool results (#162502)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 19:23:43 +00:00
Denis Shulyaka
6d74c912d2 Fix JSON serialization of datetime objects in Google Generative AI tool results (#162495) 2026-02-13 19:23:42 +00:00
Denis Shulyaka
8a01dfcc00 Fix JSON serialization of time objects in OpenAI tool results (#162490) 2026-02-13 19:23:40 +00:00
Brett Adams
9722898dc6 Fix device_class of backup reserve sensor in Tessie (#162459) 2026-02-13 19:23:39 +00:00
Brett Adams
7438c71fcb Fix device_class of backup reserve sensor in teslemetry (#162458) 2026-02-13 19:23:38 +00:00
Christian Lackas
0b5e55b923 Fix absolute humidity sensor on HmIP-WGT glass thermostats (#162455) 2026-02-13 19:23:37 +00:00
ElCruncharino
61ed959e8e Fix AsyncIteratorReader blocking after stream exhaustion (#161731) 2026-02-13 19:17:20 +00:00
Jaap Pieroen
3989532465 Bump essent-dynamic-pricing to 0.3.1 (#160958)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-13 19:17:18 +00:00
Franck Nijhof
28027ddca4 2026.2.1 (#162450) 2026-02-06 22:44:07 +01:00
Franck Nijhof
fe0d7b3cca Bump version to 2026.2.1 2026-02-06 20:49:26 +00:00
jameson_uk
0dcc4e9527 dep: bump aioamazondevices to 11.1.3 (#162437) 2026-02-06 20:47:38 +00:00
Artur Pragacz
b13b189703 Make bad entity ID detection more lenient (#162425) 2026-02-06 20:47:37 +00:00
epenet
150829f599 Fix invalid yardian snaphots (#162422) 2026-02-06 20:47:36 +00:00
Joost Lekkerkerker
57dd9d9c23 Remove double unit of measurement for yardian (#162412) 2026-02-06 20:47:34 +00:00
Sab44
e2056cb12c Bump librehardwaremonitor-api to version 1.9.1 (#162409) 2026-02-06 20:47:33 +00:00
Joost Lekkerkerker
fa2c8992cf Remove entity id overwrite for ambient station (#162403) 2026-02-06 20:47:32 +00:00
Matt Zimmerman
ddf5c7fe3a Add missing config flow strings to SmartTub (#162375)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:31 +00:00
Matt Zimmerman
7034ed6d3f Bump python-smarttub to 0.0.47 (#162367)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:29 +00:00
Aaron Godfrey
9015b53c1b Fix conversion of data for todo.* actions (#162366) 2026-02-06 20:47:28 +00:00
Jordan Harvey
1cfa6561f7 Update pynintendoparental requirement to version 2.3.2.1 (#162362) 2026-02-06 20:47:27 +00:00
Shay Levy
eead02dcca Fix Shelly Linkedgo Thermostat status update (#162339) 2026-02-06 20:47:26 +00:00
Arie Catsman
456e51a221 Bump pyenphase to 2.4.5 (#162324) 2026-02-06 20:47:25 +00:00
Luo Chen
5d984ce186 Fix unicode escaping in MCP server tool response (#162319)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-06 20:47:24 +00:00
Oliver
61f45489ac Add mapping for stopped state to denonavr media player (#162283) 2026-02-06 20:47:23 +00:00
Tomás Correia
f72c643b38 Fix multipart upload to use consistent part sizes for R2/S3 (#162278) 2026-02-06 20:47:22 +00:00
Oliver
27bc26e886 Bump denonavr to 1.3.2 (#162271) 2026-02-06 20:47:20 +00:00
Thomas55555
0e9f03cbc1 Bump google_air_quality_api to 3.0.1 (#162233) 2026-02-06 20:47:19 +00:00
David Bonnes
9480c33fb0 Bump evohome-async to 1.1.3 (#162232) 2026-02-06 20:47:18 +00:00
Jonathan
3e6b8663e8 Fix device_class of backup reserve sensor (#161178) 2026-02-06 20:47:17 +00:00
epenet
1c69a83793 Fix redundant off preset in Tuya climate (#161040) 2026-02-06 20:47:16 +00:00
Franck Nijhof
3e8923f105 2026.2.0 (#162224) 2026-02-04 20:35:11 +01:00
Franck Nijhof
17cca3e69d Bump version to 2026.2.0 2026-02-04 18:53:49 +00:00
Franck Nijhof
12714c489f Bump version to 2026.2.0b5 2026-02-04 18:45:36 +00:00
Robert Resch
f788d61b4a Revert "Bump intents (#162205)" (#162226) 2026-02-04 18:36:12 +00:00
Simone Chemelli
5c726af00b Fix logic and tests for Alexa Devices utils module (#162223) 2026-02-04 18:36:10 +00:00
Joost Lekkerkerker
d1d207fbb2 Add guard for Apple TV text focus state (#162207) 2026-02-04 18:36:09 +00:00
David Bonnes
6c7f8df7f7 Fix evohome not updating scheduled setpoints in state attrs (#162043) 2026-02-04 18:36:07 +00:00
Kevin Stillhammer
6f8c9b1504 Bump fressnapftracker to 0.2.2 (#161913) 2026-02-04 18:36:06 +00:00
Kevin Stillhammer
4f9aedbc84 Filter out invalid trackers in fressnapf_tracker (#161670) 2026-02-04 18:36:04 +00:00
Franck Nijhof
52fb0343e4 Bump version to 2026.2.0b4 2026-02-04 16:14:23 +00:00
Bram Kragten
1050b4580a Update frontend to 20260128.6 (#162214) 2026-02-04 16:10:08 +00:00
Åke Strandberg
344c42172e Add missing codes for Miele coffe systems (#162206) 2026-02-04 16:10:06 +00:00
Michael Hansen
93cc0fd7f1 Bump intents (#162205) 2026-02-04 16:10:05 +00:00
andreimoraru
05fe636b55 Bump yt-dlp to 2026.02.04 (#162204)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-04 16:10:03 +00:00
Marc Mueller
f22467d099 Pin auth0-python to <5.0 (#162203) 2026-02-04 16:10:01 +00:00
TheJulianJES
4bc3899b32 Bump ZHA to 0.0.89 (#162195) 2026-02-04 16:10:00 +00:00
Oliver
fc4d6bf5f1 Bump denonavr to 1.3.1 (#162183) 2026-02-04 16:09:58 +00:00
johanzander
8ed0672a8f Bump growattServer to 1.9.0 (#162179)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 16:09:57 +00:00
Norbert Rittel
282e347a1b Clarify action descriptions in media_player (#162172) 2026-02-04 16:09:55 +00:00
Erik Montnemery
1bfb02b440 Bump python-otbr-api to 2.8.0 (#162167) 2026-02-04 16:09:54 +00:00
Przemko92
71b03bd9ae Bump compit-inext-api to 0.8.0 (#162166) 2026-02-04 16:09:52 +00:00
Przemko92
cbd69822eb Update compit-inext-api to 0.7.0 (#162020) 2026-02-04 16:09:51 +00:00
Denis Shulyaka
db900f4dd2 Anthropic repair deprecated models (#162162)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 16:00:14 +00:00
Jonathan Bangert
a707e695bc Bump bleak-esphome to 3.6.0 (#162028) 2026-02-04 16:00:12 +00:00
Liquidmasl
4feceac205 Jellyfin native client controls (#161982) 2026-02-04 16:00:11 +00:00
Petro31
10c20faaca Fix template weather humidity (#161945) 2026-02-04 16:00:09 +00:00
Robert Svensson
abcd512401 Add missing OUI to Axis integration, discovery would abort with unsup… (#161943) 2026-02-04 16:00:07 +00:00
Bram Kragten
fdf8edf474 Bump version to 2026.2.0b3 2026-02-03 18:03:54 +01:00
Bram Kragten
47e1a98bee Update frontend to 20260128.5 (#162156) 2026-02-03 18:03:04 +01:00
Joost Lekkerkerker
2d8572b943 Add Heatit virtual brand (#162155) 2026-02-03 18:03:02 +01:00
Joost Lekkerkerker
660cfdbd50 Add Heiman virtual brand (#162152) 2026-02-03 18:03:00 +01:00
Steven Travers
4208595da6 Modify Analytics text on feature labs (#162151) 2026-02-03 18:02:59 +01:00
Paul Bottein
b6b2d2fc6f Update title and description of YAML dashboard repair (#162138) 2026-02-03 18:02:58 +01:00
victorigualada
6c4c632848 Handle chat log attachments in Cloud integration (#162121) 2026-02-03 18:02:57 +01:00
Shay Levy
78cf62176f Fix Shelly xpercent sensor state_class (#162107) 2026-02-03 18:02:56 +01:00
Denis Shulyaka
df971c7a42 Anthropic: Switch default model to Haiku 4.5 (#162093) 2026-02-03 18:02:55 +01:00
mezz64
1fcabb7f2d Bump pyhik to 0.4.2 (#162092)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-03 18:02:53 +01:00
Åke Strandberg
9fb60c9ea2 Update Senz temperature sensor (#162016) 2026-02-03 18:02:52 +01:00
J. Diego Rodríguez Royo
9c11a4646f Remove coffee machine's hot water sensor's state class at Home Connect (#161246) 2026-02-03 17:58:47 +01:00
jameson_uk
b036a78776 Remove invalid notification sensors for Alexa devices (#160422)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2026-02-03 17:58:45 +01:00
Kamil Breguła
60bb3cb704 Handle missing battery stats in systemmonitor (#158287)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-03 17:58:43 +01:00
Bram Kragten
0e770958ac Bump version to 2026.2.0b2 2026-02-02 19:12:33 +01:00
Bram Kragten
2a54c71b6c Update frontend to 20260128.4 (#162096) 2026-02-02 19:11:59 +01:00
Steven Travers
50463291ab Add learn more data for Analytics in labs (#162094) 2026-02-02 19:11:59 +01:00
Andrea Turri
43cc34042a Fix Miele dishwasher PowerDisk filling level sensor not showing up (#162048) 2026-02-02 19:11:58 +01:00
Jan Bouwhuis
a02244ccda Bump incomfort-client to 0.6.12 (#162037) 2026-02-02 19:11:57 +01:00
Adrián Moreno
a739619121 Bump pymeteoclimatic to 0.1.1 (#162029) 2026-02-02 19:11:56 +01:00
Åke Strandberg
5db97a5f1c Improved error checking during startup of SENZ (#162026) 2026-02-02 19:11:54 +01:00
Josef Zweck
804ba9c9cc Remove file description dependency in onedrive (#162012) 2026-02-02 19:11:53 +01:00
Filip Bårdsnes Tomren
5ecbcea946 Update ical requirement version to 12.1.3 (#162010) 2026-02-02 19:11:52 +01:00
hanwg
11be2b6289 Fix parse_mode for Telegram bot actions (#162006) 2026-02-02 19:11:51 +01:00
cdnninja
eefae0307b Add integration type of hub to vesync (#162004) 2026-02-02 19:11:50 +01:00
Matthias Alphart
d397ee28ea Fix KNX fan unique_id for switch-only fans (#162002) 2026-02-02 19:11:49 +01:00
starkillerOG
02c821128e Bump reolink-aio to 0.18.2 (#161998) 2026-02-02 19:11:48 +01:00
Shay Levy
71dc15d45f Fix Shelly CoIoT repair issue (#161973)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-02 19:11:47 +01:00
Raphael Hehl
1078387b22 Bump uiprotect to version 10.1.0 (#161967)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-02-02 19:11:46 +01:00
tronikos
35fab27d15 Bump opower to 0.17.0 (#161962) 2026-02-02 19:11:45 +01:00
Yuxin Wang
915dc7a908 Mark datetime sensors as unknown when parsing fails (#161952) 2026-02-02 19:11:44 +01:00
mvn23
e5a9738983 Fix OpenTherm Gateway button availability (#161933) 2026-02-02 19:11:43 +01:00
mvn23
2ff73219a2 Bump pyotgw to 2.2.3 (#161928) 2026-02-02 19:11:42 +01:00
epenet
5dc1270ed1 Fix mired warning in template light (#161923) 2026-02-02 19:11:41 +01:00
J. Diego Rodríguez Royo
9e95ad5a85 Restore the Home Connect program option entities (#156401)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-02 19:11:40 +01:00
Franck Nijhof
9a5d4610f7 Bump version to 2026.2.0b1 2026-01-30 11:45:08 +00:00
Paul Bottein
41c524fce4 Update frontend to 20260128.3 (#161918) 2026-01-30 11:44:54 +00:00
David Recordon
5f9fa95554 Fix Control4 HVAC state-to-action mapping (#161916) 2026-01-30 11:44:51 +00:00
Simone Chemelli
6950be8ea9 Handle hostname resolution for Shelly repair issue (#161914) 2026-01-30 11:44:47 +00:00
puddly
c5a8bf64d0 Bump ZHA to 0.0.88 (#161904) 2026-01-30 11:44:44 +00:00
hanwg
a2b9a6e9df Update translations for Telegram bot (#161903) 2026-01-30 11:44:43 +00:00
Marc Mueller
a0c567f0da Update fritzconnection to 1.15.1 (#161887) 2026-01-30 11:44:40 +00:00
Bram Kragten
c7feafdde6 Update frontend to 20260128.2 (#161881) 2026-01-30 11:44:38 +00:00
Björn Dalfors
e1e74b0aeb Bump nibe to 2.22.0 (#161873) 2026-01-30 11:44:36 +00:00
Sebastiaan Speck
673411ef97 Bump renault-api to 0.5.3 (#161857) 2026-01-30 11:44:34 +00:00
epenet
f7e5af7cb1 Fix incorrect entity_description class in radarr (#161856) 2026-01-30 11:44:32 +00:00
Norbert Rittel
0ee56ce708 Fix action descriptions of alarm_control_panel (#161852) 2026-01-30 11:44:30 +00:00
Manu
f93a176398 Fix string in Namecheap DynamicDNS integration (#161821) 2026-01-30 11:44:28 +00:00
Paul Bottein
cd2394bc12 Allow lovelace path for dashboard in yaml and fix yaml dashboard migration (#161816) 2026-01-30 11:44:26 +00:00
Michael Hansen
5c20b8eaff Bump intents to 2026.1.28 (#161813) 2026-01-30 11:44:25 +00:00
Aaron Godfrey
4bd499d3a6 Update todoist-api-python to 3.1.0 (#161811)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 11:44:23 +00:00
Jan Bouwhuis
8a53b94c5a Fix use of ambiguous units for reactive power and energy (#161810) 2026-01-30 11:44:20 +00:00
victorigualada
d5aff326e3 Use OpenAI schema dataclasses for cloud stream responses (#161663) 2026-01-30 11:44:18 +00:00
Gage Benne
22f66abbe7 Bump pydexcom to 0.5.1 (#161549) 2026-01-30 11:44:16 +00:00
Mattia Monga
f635228b1f Make viaggiatreno work by fixing some critical bugs (#160093)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-30 11:44:14 +00:00
Artur Pragacz
4c708c143d Fix validation of actions config in intent_script (#158266) 2026-01-30 11:44:12 +00:00
Franck Nijhof
3369459d41 Bump version to 2026.2.0b0 2026-01-28 20:00:19 +00:00
498 changed files with 17277 additions and 14342 deletions

View File

@@ -10,12 +10,12 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.14.2"
DEFAULT_PYTHON: "3.13"
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.01.0"
BASE_IMAGE_VERSION: "2025.12.0"
ARCHITECTURES: '["amd64", "aarch64"]'
jobs:
@@ -100,7 +100,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -111,7 +111,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package
@@ -184,7 +184,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -287,7 +287,7 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -358,13 +358,13 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -522,7 +522,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -37,12 +37,12 @@ on:
type: boolean
env:
CACHE_VERSION: 2
CACHE_VERSION: 3
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.3"
DEFAULT_PYTHON: "3.14.2"
ALL_PYTHON_VERSIONS: "['3.14.2']"
HA_SHORT_VERSION: "2026.2"
DEFAULT_PYTHON: "3.13.11"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
# 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
@@ -310,7 +310,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: &actions-cache actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
key: &key-python-venv >-
@@ -374,7 +374,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: &actions-cache-save actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: *path-apt-cache
key: *key-apt-cache
@@ -425,7 +425,7 @@ jobs:
steps:
- &cache-restore-apt
name: Restore apt cache
uses: &actions-cache-restore actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: &actions-cache-restore actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: *path-apt-cache
fail-on-cache-miss: true

View File

@@ -10,7 +10,7 @@ on:
- "**strings.json"
env:
DEFAULT_PYTHON: "3.14.2"
DEFAULT_PYTHON: "3.13"
jobs:
upload:

View File

@@ -17,7 +17,7 @@ on:
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.14.2"
DEFAULT_PYTHON: "3.13"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}}

View File

@@ -1 +1 @@
3.14
3.13

View File

@@ -389,7 +389,6 @@ homeassistant.components.onkyo.*
homeassistant.components.open_meteo.*
homeassistant.components.open_router.*
homeassistant.components.openai_conversation.*
homeassistant.components.openevse.*
homeassistant.components.openexchangerates.*
homeassistant.components.opensky.*
homeassistant.components.openuv.*

4
CODEOWNERS generated
View File

@@ -921,8 +921,6 @@ build.json @home-assistant/supervisor
/tests/components/libre_hardware_monitor/ @Sab44
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/liebherr/ @mettolen
/tests/components/liebherr/ @mettolen
/homeassistant/components/lifx/ @Djelibeybi
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
@@ -1880,8 +1878,6 @@ build.json @home-assistant/supervisor
/tests/components/worldclock/ @fabaff
/homeassistant/components/ws66i/ @ssaenger
/tests/components/ws66i/ @ssaenger
/homeassistant/components/wsdot/ @ucodery
/tests/components/wsdot/ @ucodery
/homeassistant/components/wyoming/ @synesthesiam
/tests/components/wyoming/ @synesthesiam
/homeassistant/components/xbox/ @hunterjm @tr4nt0r

View File

@@ -52,9 +52,6 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.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
# Claude Code native install
RUN curl -fsSL https://claude.ai/install.sh | bash
WORKDIR /workspaces
# Set the default shell to bash instead of sh

View File

@@ -0,0 +1,5 @@
{
"domain": "heatit",
"name": "Heatit",
"iot_standards": ["zwave"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "heiman",
"name": "Heiman",
"iot_standards": ["matter", "zigbee"]
}

View File

@@ -7,12 +7,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, SERVER_URL
from .services import async_setup_services
ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
@@ -21,14 +19,6 @@ PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
AgentDVRConfigEntry = ConfigEntry[Agent]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(
hass: HomeAssistant, config_entry: AgentDVRConfigEntry

View File

@@ -9,7 +9,10 @@ from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from . import AgentDVRConfigEntry
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
@@ -18,6 +21,20 @@ SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
_LOGGER = logging.getLogger(__name__)
_DEV_EN_ALT = "enable_alerts"
_DEV_DS_ALT = "disable_alerts"
_DEV_EN_REC = "start_recording"
_DEV_DS_REC = "stop_recording"
_DEV_SNAP = "snapshot"
CAMERA_SERVICES = {
_DEV_EN_ALT: "async_enable_alerts",
_DEV_DS_ALT: "async_disable_alerts",
_DEV_EN_REC: "async_start_recording",
_DEV_DS_REC: "async_stop_recording",
_DEV_SNAP: "async_snapshot",
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -40,6 +57,10 @@ async def async_setup_entry(
async_add_entities(cameras)
platform = async_get_current_platform()
for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, None, method)
class AgentCamera(MjpegCamera):
"""Representation of an Agent Device Stream."""

View File

@@ -1,38 +0,0 @@
"""Services for Agent DVR."""
from __future__ import annotations
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import service
from .const import DOMAIN
_DEV_EN_ALT = "enable_alerts"
_DEV_DS_ALT = "disable_alerts"
_DEV_EN_REC = "start_recording"
_DEV_DS_REC = "stop_recording"
_DEV_SNAP = "snapshot"
CAMERA_SERVICES = {
_DEV_EN_ALT: "async_enable_alerts",
_DEV_DS_ALT: "async_disable_alerts",
_DEV_EN_REC: "async_start_recording",
_DEV_DS_REC: "async_stop_recording",
_DEV_SNAP: "async_snapshot",
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
for service_name, method in CAMERA_SERVICES.items():
service.async_register_platform_entity_service(
hass,
DOMAIN,
service_name,
entity_domain=CAMERA_DOMAIN,
schema=None,
func=method,
)

View File

@@ -18,15 +18,12 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_DEVICE_BAUD,
CONF_DEVICE_PATH,
DOMAIN,
PROTOCOL_SERIAL,
PROTOCOL_SOCKET,
SIGNAL_PANEL_MESSAGE,
@@ -35,11 +32,9 @@ from .const import (
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE,
)
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
@@ -59,12 +54,6 @@ class AlarmDecoderData:
restart: bool
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(
hass: HomeAssistant, entry: AlarmDecoderConfigEntry
) -> bool:

View File

@@ -2,13 +2,17 @@
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat,
)
from homeassistant.const import ATTR_CODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,6 +27,11 @@ from .const import (
)
from .entity import AlarmDecoderEntity
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
async def async_setup_entry(
hass: HomeAssistant,
@@ -41,6 +50,23 @@ async def async_setup_entry(
)
async_add_entities([entity])
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_ALARM_TOGGLE_CHIME,
{
vol.Required(ATTR_CODE): cv.string,
},
"alarm_toggle_chime",
)
platform.async_register_entity_service(
SERVICE_ALARM_KEYPRESS,
{
vol.Required(ATTR_KEYPRESS): cv.string,
},
"alarm_keypress",
)
class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
"""Representation of an AlarmDecoder-based alarm panel."""

View File

@@ -1,46 +0,0 @@
"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC)."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
)
from homeassistant.const import ATTR_CODE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ALARM_TOGGLE_CHIME,
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
schema={
vol.Required(ATTR_CODE): cv.string,
},
func="alarm_toggle_chime",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ALARM_KEYPRESS,
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
schema={
vol.Required(ATTR_KEYPRESS): cv.string,
},
func="alarm_keypress",
)

View File

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

View File

@@ -28,6 +28,7 @@ from homeassistant.helpers.typing import StateType
from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import async_remove_unsupported_notification_sensors
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -105,6 +106,9 @@ async def async_setup_entry(
coordinator = entry.runtime_data
# Remove notification sensors from unsupported devices
await async_remove_unsupported_notification_sensors(hass, coordinator)
known_devices: set[str] = set()
def _check_device() -> None:
@@ -122,6 +126,7 @@ async def async_setup_entry(
AmazonSensorEntity(coordinator, serial_num, notification_desc)
for notification_desc in NOTIFICATIONS
for serial_num in new_devices
if coordinator.data[serial_num].notifications_supported
]
async_add_entities(sensors_list + notifications_list)

View File

@@ -59,13 +59,15 @@ async def async_setup_entry(
coordinator = entry.runtime_data
# Replace unique id for "DND" switch and remove from Speaker Group
await async_update_unique_id(
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
)
# DND keys
old_key = "do_not_disturb"
new_key = "dnd"
# Remove DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator)
# Remove old DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
# Replace unique id for DND switch
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
known_devices: set[str] = set()

View File

@@ -5,8 +5,14 @@ from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.const.schedules import (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -48,7 +54,7 @@ def alexa_api_call[_T: AmazonEntity, **_P](
async def async_update_unique_id(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
domain: str,
platform: str,
old_key: str,
new_key: str,
) -> None:
@@ -57,7 +63,9 @@ async def async_update_unique_id(
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{old_key}"
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
if entity_id := entity_registry.async_get_entity_id(
DOMAIN, platform, unique_id
):
_LOGGER.debug("Updating unique_id for %s", entity_id)
new_unique_id = unique_id.replace(old_key, new_key)
@@ -68,12 +76,13 @@ async def async_update_unique_id(
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
key: str,
) -> None:
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-do_not_disturb"
unique_id = f"{serial_num}-{key}"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
@@ -81,3 +90,27 @@ async def async_remove_dnd_from_virtual_group(
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
async def async_remove_unsupported_notification_sensors(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove notification sensors from unsupported devices."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
for notification_key in (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
):
unique_id = f"{serial_num}-{notification_key}"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SENSOR_DOMAIN, unique_id=unique_id
)
is_unsupported = not coordinator.data[serial_num].notifications_supported
if entity_id and is_unsupported:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed unsupported notification sensor %s", entity_id)

View File

@@ -26,10 +26,9 @@ from homeassistant.const import (
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AmbientStation, AmbientStationConfigEntry
from . import AmbientStationConfigEntry
from .const import ATTR_LAST_DATA, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX
from .entity import AmbientWeatherEntity
@@ -683,22 +682,6 @@ async def async_setup_entry(
class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
"""Define an Ambient sensor."""
def __init__(
self,
ambient: AmbientStation,
mac_address: str,
station_name: str,
description: EntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(ambient, mac_address, station_name, description)
if description.key == TYPE_SOLARRADIATION_LX:
# Since TYPE_SOLARRADIATION and TYPE_SOLARRADIATION_LX will have the same
# name in the UI, we influence the entity ID of TYPE_SOLARRADIATION_LX here
# to differentiate them:
self.entity_id = f"sensor.{station_name}_solar_rad_lx"
@callback
def update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor."""

View File

@@ -10,6 +10,7 @@
"preview_features": {
"snapshots": {
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
}
},

View File

@@ -1,7 +1,7 @@
{
"preview_features": {
"snapshots": {
"description": "This free, open source device database of the Open Home Foundation helps users find useful information about smart home devices used in real installations.\n\nYou can help build it by anonymously sharing data about your devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).\n\nLearn more about the device database and how we process your data in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement), which you accept by opting in.",
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
"disable_confirmation": "Your data will no longer be shared with the Open Home Foundation's device database.",
"enable_confirmation": "This feature is still in development and may change. The device database is being refined based on user feedback and is not yet complete.",
"name": "Device database"

View File

@@ -14,10 +14,18 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT_CONVERSATION_NAME,
DEPRECATED_MODELS,
DOMAIN,
LOGGER,
)
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -27,6 +35,7 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Anthropic."""
hass.data.setdefault(DOMAIN, {}).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
await async_migrate_integration(hass)
return True
@@ -50,6 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
entry.async_on_unload(entry.add_update_listener(async_update_options))
for subentry in entry.subentries.values():
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model.startswith(
tuple(DEPRECATED_MODELS)
):
ir.async_create_issue(
hass,
DOMAIN,
"model_deprecated",
is_fixable=True,
is_persistent=False,
learn_more_url="https://platform.claude.com/docs/en/about-claude/model-deprecations",
severity=ir.IssueSeverity.WARNING,
translation_key="model_deprecated",
)
break
return True
@@ -62,6 +87,11 @@ async def async_update_options(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> None:
"""Update options."""
defer_reload_entries: set[str] = hass.data.setdefault(DOMAIN, {}).setdefault(
DATA_REPAIR_DEFER_RELOAD, set()
)
if entry.entry_id in defer_reload_entries:
return
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -92,6 +92,40 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
await client.models.list(timeout=10.0)
async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionDict]:
"""Get list of available models."""
try:
models = (await client.models.list()).data
except anthropic.AnthropicError:
models = []
_LOGGER.debug("Available models: %s", models)
model_options: list[SelectOptionDict] = []
short_form = re.compile(r"[^\d]-\d$")
for model_info in models:
# Resolve alias from versioned model name:
model_alias = (
model_info.id[:-9]
if model_info.id
not in (
"claude-3-haiku-20240307",
"claude-3-5-haiku-20241022",
"claude-3-opus-20240229",
)
else model_info.id
)
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,
value=model_alias,
)
)
return model_options
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
@@ -401,38 +435,13 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
async def _get_model_list(self) -> list[SelectOptionDict]:
"""Get list of available models."""
try:
client = await self.hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=self._get_entry().data[CONF_API_KEY],
)
client = await self.hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=self._get_entry().data[CONF_API_KEY],
)
models = (await client.models.list()).data
except anthropic.AnthropicError:
models = []
_LOGGER.debug("Available models: %s", models)
model_options: list[SelectOptionDict] = []
short_form = re.compile(r"[^\d]-\d$")
for model_info in models:
# Resolve alias from versioned model name:
model_alias = (
model_info.id[:-9]
if model_info.id
not in ("claude-3-haiku-20240307", "claude-3-opus-20240229")
else model_info.id
)
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,
value=model_alias,
)
)
return model_options
)
return await get_model_list(client)
async def _get_location_data(self) -> dict[str, str]:
"""Get approximate location data of the user."""

View File

@@ -22,8 +22,10 @@ CONF_WEB_SEARCH_REGION = "region"
CONF_WEB_SEARCH_COUNTRY = "country"
CONF_WEB_SEARCH_TIMEZONE = "timezone"
DATA_REPAIR_DEFER_RELOAD = "repair_defer_reload"
DEFAULT = {
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_MAX_TOKENS: 3000,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: 0,
@@ -46,3 +48,10 @@ WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
]
DEPRECATED_MODELS = [
"claude-3-5-haiku",
"claude-3-7-sonnet",
"claude-3-5-sonnet",
"claude-3-opus",
]

View File

@@ -600,16 +600,6 @@ class AnthropicBaseLLMEntity(Entity):
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
# System prompt with caching enabled
system_prompt: list[TextBlockParam] = [
TextBlockParam(
type="text",
text=system.content,
cache_control={"type": "ephemeral"},
)
]
messages = _convert_content(chat_log.content[1:])
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
@@ -618,7 +608,7 @@ class AnthropicBaseLLMEntity(Entity):
model=model,
messages=messages,
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
system=system_prompt,
system=system.content,
stream=True,
)
@@ -705,6 +695,10 @@ class AnthropicBaseLLMEntity(Entity):
type="auto",
)
if isinstance(model_args["system"], str):
model_args["system"] = [
TextBlockParam(type="text", text=model_args["system"])
]
model_args["system"].append( # type: ignore[union-attr]
TextBlockParam(
type="text",

View File

@@ -0,0 +1,275 @@
"""Issue repair flow for Anthropic."""
from __future__ import annotations
from collections.abc import Iterator
from typing import cast
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from .config_flow import get_model_list
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT,
DEPRECATED_MODELS,
DOMAIN,
)
class ModelDeprecatedRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
_subentry_iter: Iterator[tuple[str, str]] | None
_current_entry_id: str | None
_current_subentry_id: str | None
_reload_pending: set[str]
_pending_updates: dict[str, dict[str, str]]
def __init__(self) -> None:
"""Initialize the flow."""
super().__init__()
self._subentry_iter = None
self._current_entry_id = None
self._current_subentry_id = None
self._reload_pending = set()
self._pending_updates = {}
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
previous_entry_id: str | None = None
if user_input is not None:
previous_entry_id = self._async_update_current_subentry(user_input)
self._clear_current_target()
target = await self._async_next_target()
next_entry_id = target[0].entry_id if target else None
if previous_entry_id and previous_entry_id != next_entry_id:
await self._async_apply_pending_updates(previous_entry_id)
if target is None:
await self._async_apply_all_pending_updates()
return self.async_create_entry(data={})
entry, subentry, model = target
client = entry.runtime_data
model_list = [
model_option
for model_option in await get_model_list(client)
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
]
if "opus" in model:
suggested_model = "claude-opus-4-5"
elif "haiku" in model:
suggested_model = "claude-haiku-4-5"
elif "sonnet" in model:
suggested_model = "claude-sonnet-4-5"
else:
suggested_model = cast(str, DEFAULT[CONF_CHAT_MODEL])
schema = vol.Schema(
{
vol.Required(
CONF_CHAT_MODEL,
default=suggested_model,
): SelectSelector(
SelectSelectorConfig(options=model_list, custom_value=True)
),
}
)
return self.async_show_form(
step_id="init",
data_schema=schema,
description_placeholders={
"entry_name": entry.title,
"model": model,
"subentry_name": subentry.title,
"subentry_type": self._format_subentry_type(subentry.subentry_type),
},
)
def _iter_deprecated_subentries(self) -> Iterator[tuple[str, str]]:
"""Yield entry/subentry pairs that use deprecated models."""
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.state is not ConfigEntryState.LOADED:
continue
for subentry in entry.subentries.values():
model = subentry.data.get(CONF_CHAT_MODEL)
if model and model.startswith(tuple(DEPRECATED_MODELS)):
yield entry.entry_id, subentry.subentry_id
async def _async_next_target(
self,
) -> tuple[ConfigEntry, ConfigSubentry, str] | None:
"""Return the next deprecated subentry target."""
if self._subentry_iter is None:
self._subentry_iter = self._iter_deprecated_subentries()
while True:
try:
entry_id, subentry_id = next(self._subentry_iter)
except StopIteration:
return None
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
continue
subentry = entry.subentries.get(subentry_id)
if subentry is None:
continue
model = self._pending_model(entry_id, subentry_id)
if model is None:
model = subentry.data.get(CONF_CHAT_MODEL)
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
continue
self._current_entry_id = entry_id
self._current_subentry_id = subentry_id
return entry, subentry, model
def _async_update_current_subentry(self, user_input: dict[str, str]) -> str | None:
"""Update the currently selected subentry."""
if not self._current_entry_id or not self._current_subentry_id:
return None
entry = self.hass.config_entries.async_get_entry(self._current_entry_id)
if entry is None:
return None
subentry = entry.subentries.get(self._current_subentry_id)
if subentry is None:
return None
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: user_input[CONF_CHAT_MODEL],
}
if updated_data == subentry.data:
return entry.entry_id
self._queue_pending_update(
entry.entry_id,
subentry.subentry_id,
updated_data[CONF_CHAT_MODEL],
)
return entry.entry_id
def _clear_current_target(self) -> None:
"""Clear current target tracking."""
self._current_entry_id = None
self._current_subentry_id = None
def _format_subentry_type(self, subentry_type: str) -> str:
"""Return a user-friendly subentry type label."""
if subentry_type == "conversation":
return "Conversation agent"
if subentry_type in ("ai_task", "ai_task_data"):
return "AI task"
return subentry_type
def _queue_pending_update(
self, entry_id: str, subentry_id: str, model: str
) -> None:
"""Store a pending model update for a subentry."""
self._pending_updates.setdefault(entry_id, {})[subentry_id] = model
def _pending_model(self, entry_id: str, subentry_id: str) -> str | None:
"""Return a pending model update if one exists."""
return self._pending_updates.get(entry_id, {}).get(subentry_id)
def _mark_entry_for_reload(self, entry_id: str) -> None:
"""Prevent reload until repairs are complete for the entry."""
self._reload_pending.add(entry_id)
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.add(entry_id)
async def _async_reload_entry(self, entry_id: str) -> None:
"""Reload an entry once all repairs are completed."""
if entry_id not in self._reload_pending:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is not None and entry.state is not ConfigEntryState.LOADED:
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
return
if entry is not None:
await self.hass.config_entries.async_reload(entry_id)
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
def _clear_defer_reload(self, entry_id: str) -> None:
"""Remove entry from the deferred reload set."""
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.discard(entry_id)
async def _async_apply_pending_updates(self, entry_id: str) -> None:
"""Apply pending subentry updates for a single entry."""
updates = self._pending_updates.pop(entry_id, None)
if not updates:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None or entry.state is not ConfigEntryState.LOADED:
return
changed = False
for subentry_id, model in updates.items():
subentry = entry.subentries.get(subentry_id)
if subentry is None:
continue
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: model,
}
if updated_data == subentry.data:
continue
if not changed:
self._mark_entry_for_reload(entry_id)
changed = True
self.hass.config_entries.async_update_subentry(
entry,
subentry,
data=updated_data,
)
if not changed:
return
await self._async_reload_entry(entry_id)
async def _async_apply_all_pending_updates(self) -> None:
"""Apply all pending updates across entries."""
for entry_id in list(self._pending_updates):
await self._async_apply_pending_updates(entry_id)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if issue_id == "model_deprecated":
return ModelDeprecatedRepairFlow()
raise HomeAssistantError("Unknown issue ID")

View File

@@ -109,5 +109,21 @@
}
}
}
},
"issues": {
"model_deprecated": {
"fix_flow": {
"step": {
"init": {
"data": {
"chat_model": "[%key:common::generic::model%]"
},
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
"title": "Update model"
}
}
},
"title": "Model deprecated"
}
}
}

View File

@@ -2,15 +2,16 @@
from __future__ import annotations
from pyatv.const import KeyboardFocusState
from pyatv.const import FeatureName, FeatureState, KeyboardFocusState
from pyatv.interface import AppleTV, KeyboardListener
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AppleTvConfigEntry
from . import SIGNAL_CONNECTED, AppleTvConfigEntry
from .entity import AppleTVEntity
@@ -21,10 +22,22 @@ async def async_setup_entry(
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
manager = config_entry.runtime_data
async_add_entities([AppleTVKeyboardFocused(name, config_entry.unique_id, manager)])
cb: CALLBACK_TYPE
def setup_entities(atv: AppleTV) -> None:
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
async_add_entities(
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
)
cb()
cb = async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
)
config_entry.async_on_unload(cb)
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):

View File

@@ -52,7 +52,7 @@ from .const import (
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]

View File

@@ -16,12 +16,18 @@ CONNECTION_TIMEOUT = 120 # 2 minutes
# Default TIMEOUT_FOR_UPLOAD is 128 seconds, which is too short for large backups
TIMEOUT_FOR_UPLOAD = 43200 # 12 hours
# Reduced retry count for download operations
# Default is 20 retries with exponential backoff, which can hang for 30+ minutes
# when there are persistent connection errors (e.g., SSL failures)
TRY_COUNT_DOWNLOAD = 3
class B2Http(BaseB2Http): # type: ignore[misc]
"""B2Http with extended timeouts for backup operations."""
CONNECTION_TIMEOUT = CONNECTION_TIMEOUT
TIMEOUT_FOR_UPLOAD = TIMEOUT_FOR_UPLOAD
TRY_COUNT_DOWNLOAD = TRY_COUNT_DOWNLOAD
class B2Session(BaseB2Session): # type: ignore[misc]

View File

@@ -40,6 +40,10 @@ CACHE_TTL = 300
# This prevents uploads from hanging indefinitely
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
# Timeout for metadata download operations (in seconds)
# This prevents the backup system from hanging when B2 connections fail
METADATA_DOWNLOAD_TIMEOUT = 60
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
"""Return the suggested filenames for the backup and metadata files."""
@@ -413,12 +417,21 @@ class BackblazeBackupAgent(BackupAgent):
backups = {}
for file_name, file_version in all_files_in_prefix.items():
if file_name.endswith(METADATA_FILE_SUFFIX):
backup = await self._hass.async_add_executor_job(
self._process_metadata_file_sync,
file_name,
file_version,
all_files_in_prefix,
)
try:
backup = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._process_metadata_file_sync,
file_name,
file_version,
all_files_in_prefix,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
_LOGGER.warning(
"Timeout downloading metadata file %s", file_name
)
continue
if backup:
backups[backup.backup_id] = backup
self._backup_list_cache = backups
@@ -442,10 +455,18 @@ class BackblazeBackupAgent(BackupAgent):
if not file or not metadata_file_version:
raise BackupNotFound(f"Backup {backup_id} not found")
metadata_content = await self._hass.async_add_executor_job(
self._download_and_parse_metadata_sync,
metadata_file_version,
)
try:
metadata_content = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._download_and_parse_metadata_sync,
metadata_file_version,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
raise BackupAgentError(
f"Timeout downloading metadata for backup {backup_id}"
) from None
_LOGGER.debug(
"Successfully retrieved metadata for backup ID %s from file %s",
@@ -468,16 +489,27 @@ class BackblazeBackupAgent(BackupAgent):
# Process metadata files sequentially to avoid exhausting executor pool
for file_name, file_version in all_files_in_prefix.items():
if file_name.endswith(METADATA_FILE_SUFFIX):
(
result_backup_file,
result_metadata_file_version,
) = await self._hass.async_add_executor_job(
self._process_metadata_file_for_id_sync,
file_name,
file_version,
backup_id,
all_files_in_prefix,
)
try:
(
result_backup_file,
result_metadata_file_version,
) = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._process_metadata_file_for_id_sync,
file_name,
file_version,
backup_id,
all_files_in_prefix,
),
timeout=METADATA_DOWNLOAD_TIMEOUT,
)
except TimeoutError:
_LOGGER.warning(
"Timeout downloading metadata file %s while searching for backup %s",
file_name,
backup_id,
)
continue
if result_backup_file and result_metadata_file_version:
return result_backup_file, result_metadata_file_version

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.3.1.108.2"],
"requirements": ["mozart-api==5.3.1.108.0"],
"zeroconf": ["_bangolufsen._tcp.local."]
}

View File

@@ -8,7 +8,6 @@ from datetime import timedelta
import json
import logging
from typing import TYPE_CHECKING, Any, cast
from uuid import UUID
from aiohttp import ClientConnectorError
from mozart_api import __version__ as MOZART_API_VERSION
@@ -736,7 +735,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
await self._client.set_active_source(source_id=key)
else:
# Video
await self._client.post_remote_trigger(id=UUID(key))
await self._client.post_remote_trigger(id=key)
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select a sound mode."""
@@ -895,7 +894,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
translation_key="play_media_error",
translation_placeholders={
"media_type": media_type,
"error_message": json.loads(cast(str, error.body))["message"],
"error_message": json.loads(error.body)["message"],
},
) from error

View File

@@ -15,7 +15,7 @@ from homeassistant.components.bluetooth import (
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.signal_type import SignalType
@@ -36,45 +36,6 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SE
_LOGGER = logging.getLogger(__name__)
def get_encryption_issue_id(entry_id: str) -> str:
"""Return the repair issue id for encryption removal."""
return f"encryption_removed_{entry_id}"
def _async_create_encryption_downgrade_issue(
hass: HomeAssistant, entry: BTHomeConfigEntry, issue_id: str
) -> None:
"""Create a repair issue for encryption downgrade."""
_LOGGER.warning(
"BTHome device %s was previously encrypted but is now sending "
"unencrypted data. This could be a spoofing attempt. "
"Data will be ignored until resolved",
entry.title,
)
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=True,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
translation_key="encryption_removed",
translation_placeholders={"name": entry.title},
data={"entry_id": entry.entry_id},
)
def _async_clear_encryption_downgrade_issue(
hass: HomeAssistant, entry: BTHomeConfigEntry, issue_id: str
) -> None:
"""Clear the encryption downgrade repair issue."""
ir.async_delete_issue(hass, DOMAIN, issue_id)
_LOGGER.info(
"BTHome device %s is now sending encrypted data again. Resuming normal operation",
entry.title,
)
def process_service_info(
hass: HomeAssistant,
entry: BTHomeConfigEntry,
@@ -84,26 +45,7 @@ def process_service_info(
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
coordinator = entry.runtime_data
data = coordinator.device_data
issue_registry = ir.async_get(hass)
issue_id = get_encryption_issue_id(entry.entry_id)
update = data.update(service_info)
# Block unencrypted payloads for devices that were previously verified as encrypted.
if entry.data.get(CONF_BINDKEY) and data.downgrade_detected:
if not coordinator.encryption_downgrade_logged:
coordinator.encryption_downgrade_logged = True
if not issue_registry.async_get_issue(DOMAIN, issue_id):
_async_create_encryption_downgrade_issue(hass, entry, issue_id)
return SensorUpdate(title=None, devices={})
if data.bindkey_verified and (
(existing_issue := issue_registry.async_get_issue(DOMAIN, issue_id))
or coordinator.encryption_downgrade_logged
):
coordinator.encryption_downgrade_logged = False
if existing_issue:
_async_clear_encryption_downgrade_issue(hass, entry, issue_id)
discovered_event_classes = coordinator.discovered_event_classes
if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device:
hass.config_entries.async_update_entry(
@@ -208,8 +150,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_entry(hass: HomeAssistant, entry: BTHomeConfigEntry) -> None:
"""Remove a config entry."""
ir.async_delete_issue(hass, DOMAIN, get_encryption_issue_id(entry.entry_id))

View File

@@ -41,8 +41,6 @@ class BTHomePassiveBluetoothProcessorCoordinator(
self.discovered_event_classes = discovered_event_classes
self.device_data = device_data
self.entry = entry
# Track whether we've already logged the encryption downgrade this session.
self.encryption_downgrade_logged = False
@property
def sleepy_device(self) -> bool:

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.17.0"]
"requirements": ["bthome-ble==3.16.0"]
}

View File

@@ -1,65 +0,0 @@
"""Repairs for the BTHome integration."""
from __future__ import annotations
from typing import Any
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from . import get_encryption_issue_id
from .const import CONF_BINDKEY, DOMAIN
class EncryptionRemovedRepairFlow(RepairsFlow):
"""Handle the repair flow when encryption is disabled."""
def __init__(self, entry_id: str, entry_title: str) -> None:
"""Initialize the repair flow."""
self._entry_id = entry_id
self._entry_title = entry_title
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the initial step of the repair flow."""
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
"""Handle confirmation, remove the bindkey, and reload the entry."""
if user_input is not None:
entry = self.hass.config_entries.async_get_entry(self._entry_id)
if not entry:
return self.async_abort(reason="entry_removed")
new_data = {k: v for k, v in entry.data.items() if k != CONF_BINDKEY}
self.hass.config_entries.async_update_entry(entry, data=new_data)
ir.async_delete_issue(
self.hass, DOMAIN, get_encryption_issue_id(self._entry_id)
)
await self.hass.config_entries.async_reload(self._entry_id)
return self.async_create_entry(data={})
return self.async_show_form(
step_id="confirm",
description_placeholders={"name": self._entry_title},
)
async def async_create_fix_flow(
hass: HomeAssistant, issue_id: str, data: dict[str, Any] | None
) -> RepairsFlow:
"""Create the repair flow for removing the encryption key."""
if not data or "entry_id" not in data:
raise ValueError("Missing data for repair flow")
entry_id = data["entry_id"]
entry = hass.config_entries.async_get_entry(entry_id)
entry_title = entry.title if entry else "Unknown device"
return EncryptionRemovedRepairFlow(entry_id, entry_title)

View File

@@ -117,21 +117,5 @@
"name": "UV Index"
}
}
},
"issues": {
"encryption_removed": {
"fix_flow": {
"abort": {
"entry_removed": "The device has been removed"
},
"step": {
"confirm": {
"description": "The BTHome device **{name}** was configured with encryption but is now broadcasting unencrypted data. Data from this device is being ignored until this is resolved.\n\nIf you disabled encryption on the device, select **Submit** to remove the encryption key and resume receiving data.\n\nIf you did not disable encryption, someone may be attempting to spoof your device. Do not submit this form and the unencrypted data will continue to be ignored.",
"title": "Remove encryption key for {name}"
}
}
},
"title": "Encryption disabled on {name}"
}
}
}

View File

@@ -506,8 +506,6 @@ def is_offset_reached(
class CalendarEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes calendar entities."""
initial_color: str | None = None
class CalendarEntity(Entity):
"""Base class for calendar event entities."""
@@ -518,16 +516,12 @@ class CalendarEntity(Entity):
_alarm_unsubs: list[CALLBACK_TYPE] | None = None
_attr_initial_color: str | None
_attr_initial_color: str | None = None
@property
def initial_color(self) -> str | None:
"""Return the initial color for the calendar entity."""
if hasattr(self, "_attr_initial_color"):
return self._attr_initial_color
if hasattr(self, "entity_description"):
return self.entity_description.initial_color
return None
return self._attr_initial_color
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
"""Return initial entity options."""

View File

@@ -50,11 +50,11 @@
"selector": {},
"services": {
"disable_motion_detection": {
"description": "Disables the motion detection of a camera.",
"description": "Disables the motion detection.",
"name": "Disable motion detection"
},
"enable_motion_detection": {
"description": "Enables the motion detection of a camera.",
"description": "Enables the motion detection.",
"name": "Enable motion detection"
},
"play_stream": {
@@ -100,11 +100,11 @@
"name": "Take snapshot"
},
"turn_off": {
"description": "Turns off a camera.",
"description": "Turns off the camera.",
"name": "[%key:common::action::turn_off%]"
},
"turn_on": {
"description": "Turns on a camera.",
"description": "Turns on the camera.",
"name": "[%key:common::action::turn_on%]"
}
},

View File

@@ -50,6 +50,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps
from homeassistant.util import slugify
from .client import CloudClient
@@ -93,7 +94,7 @@ def _convert_content_to_param(
{
"type": "function_call_output",
"call_id": content.tool_call_id,
"output": json.dumps(content.tool_result),
"output": json_dumps(content.tool_result),
}
)
continue
@@ -125,7 +126,7 @@ def _convert_content_to_param(
{
"type": "function_call",
"name": tool_call.tool_name,
"arguments": json.dumps(tool_call.tool_args),
"arguments": json_dumps(tool_call.tool_args),
"call_id": tool_call.id,
}
)
@@ -459,8 +460,17 @@ class BaseCloudLLMEntity(Entity):
last_content: Any = chat_log.content[-1]
if last_content.role == "user" and last_content.attachments:
files = await self._async_prepare_files_for_prompt(last_content.attachments)
current_content = last_content.content
last_content = [*(current_content or []), *files]
last_message = cast(dict[str, Any], messages[-1])
assert (
last_message["type"] == "message"
and last_message["role"] == "user"
and isinstance(last_message["content"], str)
)
last_message["content"] = [
{"type": "input_text", "text": last_message["content"]},
*files,
]
tools: list[ToolParam] = []
tool_choice: str | None = None

View File

@@ -196,44 +196,46 @@ class R2BackupAgent(BackupAgent):
)
upload_id = multipart_upload["UploadId"]
try:
parts = []
parts: list[dict[str, Any]] = []
part_number = 1
buffer_size = 0 # bytes
buffer: list[bytes] = []
buffer = bytearray() # bytes buffer to store the data
stream = await open_stream()
async for chunk in stream:
buffer_size += len(chunk)
buffer.append(chunk)
buffer.extend(chunk)
# upload parts of exactly MULTIPART_MIN_PART_SIZE_BYTES to ensure
# all non-trailing parts have the same size (required by S3/R2)
while len(buffer) >= MULTIPART_MIN_PART_SIZE_BYTES:
part_data = bytes(buffer[:MULTIPART_MIN_PART_SIZE_BYTES])
del buffer[:MULTIPART_MIN_PART_SIZE_BYTES]
# If buffer size meets minimum part size, upload it as a part
if buffer_size >= MULTIPART_MIN_PART_SIZE_BYTES:
_LOGGER.debug(
"Uploading part number %d, size %d", part_number, buffer_size
"Uploading part number %d, size %d",
part_number,
len(part_data),
)
part = await self._client.upload_part(
Bucket=self._bucket,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=b"".join(buffer),
Body=part_data,
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
part_number += 1
buffer_size = 0
buffer = []
# Upload the final buffer as the last part (no minimum size requirement)
if buffer:
_LOGGER.debug(
"Uploading final part number %d, size %d", part_number, buffer_size
"Uploading final part number %d, size %d", part_number, len(buffer)
)
part = await self._client.upload_part(
Bucket=self._bucket,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=b"".join(buffer),
Body=bytes(buffer),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})

View File

@@ -19,11 +19,11 @@
"secret_access_key": "Secret access key"
},
"data_description": {
"access_key_id": "Access key ID to connect to Cloudflare R2 (this is your Account ID)",
"access_key_id": "Access key ID to connect to Cloudflare R2",
"bucket": "Bucket must already exist and be writable by the provided credentials.",
"endpoint_url": "Cloudflare R2 S3-compatible endpoint.",
"prefix": "Optional folder path inside the bucket. Example: backups/homeassistant",
"secret_access_key": "Secret access key to connect to Cloudflare R2. See [Docs]({auth_docs_url})"
"secret_access_key": "Secret access key to connect to Cloudflare R2. See [Cloudflare documentation]({auth_docs_url})"
},
"title": "Add Cloudflare R2 bucket"
}

View File

@@ -144,7 +144,7 @@ class ComelitAlarmEntity(
"""Update state after action."""
self._area.human_status = area_state
self._area.armed = armed
await self.async_update_ha_state()
self.async_write_ha_state()
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.7.0"]
"requirements": ["compit-inext-api==0.8.0"]
}

View File

@@ -58,11 +58,12 @@ C4_TO_HA_HVAC_MODE = {
HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()}
# Map the five known Control4 HVAC states to Home Assistant HVAC actions
# Map Control4 HVAC states to Home Assistant HVAC actions
C4_TO_HA_HVAC_ACTION = {
"off": HVACAction.OFF,
"heat": HVACAction.HEATING,
"cool": HVACAction.COOLING,
"idle": HVACAction.IDLE,
"dry": HVACAction.DRYING,
"fan": HVACAction.FAN,
}
@@ -236,8 +237,14 @@ class Control4Climate(Control4Entity, ClimateEntity):
c4_state = data.get(CONTROL4_HVAC_STATE)
if c4_state is None:
return None
# Convert state to lowercase for mapping
action = C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
# Substring match for multi-stage systems that report
# e.g. "Stage 1 Heat", "Stage 2 Cool"
if action is None:
if "heat" in str(c4_state).lower():
action = HVACAction.HEATING
elif "cool" in str(c4_state).lower():
action = HVACAction.COOLING
if action is None:
_LOGGER.debug("Unknown HVAC state received from Control4: %s", c4_state)
return action

View File

@@ -335,18 +335,20 @@ def _get_config_intents(config: ConfigType, hass_config_path: str) -> dict[str,
"""Return config intents."""
intents = config.get(DOMAIN, {}).get("intents", {})
return {
intent_name: {
"data": [
{
"sentences": sentences,
"metadata": {
METADATA_CUSTOM_SENTENCE: True,
METADATA_CUSTOM_FILE: hass_config_path,
},
}
]
"intents": {
intent_name: {
"data": [
{
"sentences": sentences,
"metadata": {
METADATA_CUSTOM_SENTENCE: True,
METADATA_CUSTOM_FILE: hass_config_path,
},
}
]
}
for intent_name, sentences in intents.items()
}
for intent_name, sentences in intents.items()
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Callable
import dataclasses
import logging
from typing import TYPE_CHECKING, Any
@@ -19,7 +18,7 @@ from homeassistant.core import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent, singleton
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT, IntentSource
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT
from .entity import ConversationEntity
from .models import (
AbstractConversationAgent,
@@ -35,11 +34,9 @@ from .trace import (
_LOGGER = logging.getLogger(__name__)
TRIGGER_INTENT_NAME_PREFIX = "HassSentenceTrigger"
if TYPE_CHECKING:
from .default_agent import DefaultAgent
from .trigger import TRIGGER_CALLBACK_TYPE
from .trigger import TriggerDetails
@singleton.singleton("conversation_agent")
@@ -142,10 +139,6 @@ async def async_converse(
return result
type IntentSourceConfig = dict[str, dict[str, Any]]
type IntentsCallback = Callable[[dict[IntentSource, IntentSourceConfig]], None]
class AgentManager:
"""Class to manage conversation agents."""
@@ -154,13 +147,8 @@ class AgentManager:
self.hass = hass
self._agents: dict[str, AbstractConversationAgent] = {}
self.default_agent: DefaultAgent | None = None
self._intents: dict[IntentSource, IntentSourceConfig] = {
IntentSource.CONFIG: {"intents": {}},
IntentSource.TRIGGER: {"intents": {}},
}
self._intents_subscribers: list[IntentsCallback] = []
self._trigger_callbacks: dict[int, TRIGGER_CALLBACK_TYPE] = {}
self._trigger_callback_counter: int = 0
self.config_intents: dict[str, Any] = {}
self.triggers_details: list[TriggerDetails] = []
@callback
def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None:
@@ -212,75 +200,27 @@ class AgentManager:
async def async_setup_default_agent(self, agent: DefaultAgent) -> None:
"""Set up the default agent."""
agent.update_config_intents(self.config_intents)
agent.update_triggers(self.triggers_details)
self.default_agent = agent
@callback
def subscribe_intents(self, subscriber: IntentsCallback) -> CALLBACK_TYPE:
"""Subscribe to intents updates.
The subscriber callback is called immediately with all intent sources
and whenever intents are updated (only with the changed source).
"""
subscriber(self._intents)
self._intents_subscribers.append(subscriber)
@callback
def unsubscribe() -> None:
"""Unsubscribe from intents updates."""
self._intents_subscribers.remove(subscriber)
return unsubscribe
def _notify_intents_subscribers(self, source: IntentSource) -> None:
"""Notify all intents subscribers of a change to a specific source."""
update = {source: self._intents[source]}
for subscriber in self._intents_subscribers:
subscriber(update)
def update_config_intents(self, intents: dict[str, Any]) -> None:
"""Update config intents."""
self._intents[IntentSource.CONFIG]["intents"] = intents
self._notify_intents_subscribers(IntentSource.CONFIG)
self.config_intents = intents
if self.default_agent is not None:
self.default_agent.update_config_intents(intents)
def register_trigger(
self, sentences: list[str], trigger_callback: TRIGGER_CALLBACK_TYPE
) -> CALLBACK_TYPE:
def register_trigger(self, trigger_details: TriggerDetails) -> CALLBACK_TYPE:
"""Register a trigger."""
trigger_id = self._trigger_callback_counter
self._trigger_callback_counter += 1
trigger_intent_name = f"{TRIGGER_INTENT_NAME_PREFIX}{trigger_id}"
trigger_intents = self._intents[IntentSource.TRIGGER]
trigger_intents["intents"][trigger_intent_name] = {
"data": [{"sentences": sentences}]
}
self._trigger_callbacks[trigger_id] = trigger_callback
self._notify_intents_subscribers(IntentSource.TRIGGER)
self.triggers_details.append(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
@callback
def unregister_trigger() -> None:
"""Unregister the trigger."""
del trigger_intents["intents"][trigger_intent_name]
del self._trigger_callbacks[trigger_id]
self._notify_intents_subscribers(IntentSource.TRIGGER)
self.triggers_details.remove(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
return unregister_trigger
@property
def trigger_sentences(self) -> list[str]:
"""Get all trigger sentences."""
sentences: list[str] = []
trigger_intents = self._intents[IntentSource.TRIGGER]
for trigger_intent in trigger_intents.get("intents", {}).values():
for data in trigger_intent.get("data", []):
sentences.extend(data.get("sentences", []))
return sentences
def get_trigger_callback(
self, trigger_intent_name: str
) -> TRIGGER_CALLBACK_TYPE | None:
"""Get the callback for a trigger from its intent name."""
if not trigger_intent_name.startswith(TRIGGER_INTENT_NAME_PREFIX):
return None
trigger_id = int(trigger_intent_name[len(TRIGGER_INTENT_NAME_PREFIX) :])
return self._trigger_callbacks.get(trigger_id)

View File

@@ -36,13 +36,6 @@ METADATA_CUSTOM_SENTENCE = "hass_custom_sentence"
METADATA_CUSTOM_FILE = "hass_custom_file"
class IntentSource(StrEnum):
"""Source of intents."""
CONFIG = "config"
TRIGGER = "trigger"
class ChatLogEventType(StrEnum):
"""Chat log event type."""

View File

@@ -76,18 +76,18 @@ from homeassistant.helpers.event import async_track_state_added_domain
from homeassistant.util import language as language_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent_manager import IntentSourceConfig, get_agent_manager
from .agent_manager import get_agent_manager
from .chat_log import AssistantContent, ChatLog, ToolResultContent
from .const import (
DOMAIN,
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
ConversationEntityFeature,
IntentSource,
)
from .entity import ConversationEntity
from .models import ConversationInput, ConversationResult
from .trace import ConversationTraceEventType, async_conversation_trace_append
from .trigger import TriggerDetails
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +126,7 @@ class SentenceTriggerResult:
sentence: str
sentence_template: str | None
matched_triggers: dict[str, RecognizeResult]
matched_triggers: dict[int, RecognizeResult]
class IntentMatchingStage(Enum):
@@ -236,19 +236,15 @@ class DefaultAgent(ConversationEntity):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the default agent."""
self.hass = hass
self._lang_intents: dict[str, LanguageIntents | object] = {}
self._load_intents_lock = asyncio.Lock()
# Intents from common conversation config
self._config_intents_config: IntentSourceConfig = {}
self._config_intents: dict[str, Any] = {}
# Intents from conversation triggers
# Sentences that will trigger a callback (skipping intent recognition)
self._triggers_details: list[TriggerDetails] = []
self._trigger_intents: Intents | None = None
self._trigger_intents_config: IntentSourceConfig = {}
# Subscription to intents updates
self._unsub_intents: Callable[[], None] | None = None
# Slot lists for entities, areas, etc.
self._slot_lists: dict[str, SlotList] | None = None
@@ -265,33 +261,6 @@ class DefaultAgent(ConversationEntity):
self.fuzzy_matching = True
self._fuzzy_config: FuzzyConfig | None = None
async def async_added_to_hass(self) -> None:
"""Subscribe to intents updates when added to hass."""
self._unsub_intents = get_agent_manager(self.hass).subscribe_intents(
self._update_intents
)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from intents updates when removed from hass."""
if self._unsub_intents is not None:
self._unsub_intents()
self._unsub_intents = None
@callback
def _update_intents(
self, intents_update: dict[IntentSource, IntentSourceConfig]
) -> None:
"""Handle intents update from agent_manager subscription."""
if IntentSource.CONFIG in intents_update:
self._config_intents_config = intents_update[IntentSource.CONFIG]
# Intents have changed, so we must clear the cache
self._intent_cache.clear()
if IntentSource.TRIGGER in intents_update:
self._trigger_intents_config = intents_update[IntentSource.TRIGGER]
# Force rebuild on next use
self._trigger_intents = None
@property
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
@@ -1090,6 +1059,14 @@ class DefaultAgent(ConversationEntity):
# Intents have changed, so we must clear the cache
self._intent_cache.clear()
@callback
def update_config_intents(self, intents: dict[str, Any]) -> None:
"""Update config intents."""
self._config_intents = intents
# Intents have changed, so we must clear the cache
self._intent_cache.clear()
async def async_prepare(self, language: str | None = None) -> None:
"""Load intents for a language."""
if language is None:
@@ -1216,7 +1193,7 @@ class DefaultAgent(ConversationEntity):
merge_dict(
intents_dict,
self._config_intents_config,
self._config_intents,
)
if not intents_dict:
@@ -1484,12 +1461,27 @@ class DefaultAgent(ConversationEntity):
return response_template.async_render(response_args)
@callback
def update_triggers(self, triggers_details: list[TriggerDetails]) -> None:
"""Update triggers."""
self._triggers_details = triggers_details
# Force rebuild on next use
self._trigger_intents = None
def _rebuild_trigger_intents(self) -> None:
"""Rebuild the HassIL intents object from the trigger intents dict."""
"""Rebuild the HassIL intents object from the current trigger sentences."""
intents_dict = {
"language": self.hass.config.language,
**self._trigger_intents_config,
"intents": {
# Use trigger data index as a virtual intent name for HassIL.
# This works because the intents are rebuilt on every
# register/unregister.
str(trigger_id): {"data": [{"sentences": trigger_details.sentences}]}
for trigger_id, trigger_details in enumerate(self._triggers_details)
},
}
trigger_intents = Intents.from_dict(intents_dict)
# Assume slot list references are wildcards
@@ -1504,7 +1496,7 @@ class DefaultAgent(ConversationEntity):
self._trigger_intents = trigger_intents
_LOGGER.debug("Rebuilt trigger intents: %s", self._trigger_intents_config)
_LOGGER.debug("Rebuilt trigger intents: %s", intents_dict)
async def async_recognize_sentence_trigger(
self, user_input: ConversationInput
@@ -1514,7 +1506,7 @@ class DefaultAgent(ConversationEntity):
Calls the registered callbacks if there's a match and returns a sentence
trigger result.
"""
if not self._trigger_intents_config.get("intents"):
if not self._triggers_details:
# No triggers registered
return None
@@ -1524,18 +1516,18 @@ class DefaultAgent(ConversationEntity):
assert self._trigger_intents is not None
matched_triggers: dict[str, RecognizeResult] = {}
matched_triggers: dict[int, RecognizeResult] = {}
matched_template: str | None = None
for result in recognize_all(user_input.text, self._trigger_intents):
if result.intent_sentence is not None:
matched_template = result.intent_sentence.text
trigger_intent_name = result.intent.name
if trigger_intent_name in matched_triggers:
trigger_id = int(result.intent.name)
if trigger_id in matched_triggers:
# Already matched a sentence from this trigger
break
matched_triggers[trigger_intent_name] = result
matched_triggers[trigger_id] = result
if not matched_triggers:
# Sentence did not match any trigger sentences
@@ -1559,14 +1551,10 @@ class DefaultAgent(ConversationEntity):
chat_log: ChatLog,
) -> str:
"""Run sentence trigger callbacks and return response text."""
manager = get_agent_manager(self.hass)
# Gather callback responses in parallel
trigger_callbacks = [
trigger_callback(user_input, trigger_result)
for trigger_intent_name, trigger_result in result.matched_triggers.items()
if (trigger_callback := manager.get_trigger_callback(trigger_intent_name))
is not None
self._triggers_details[trigger_id].callback(user_input, trigger_result)
for trigger_id, trigger_result in result.matched_triggers.items()
]
tool_input = llm.ToolInput(

View File

@@ -165,7 +165,11 @@ async def websocket_list_sentences(
"""List custom registered sentences."""
manager = get_agent_manager(hass)
connection.send_result(msg["id"], {"trigger_sentences": manager.trigger_sentences})
sentences = []
for trigger_details in manager.triggers_details:
sentences.extend(trigger_details.sentences)
connection.send_result(msg["id"], {"trigger_sentences": sentences})
@websocket_api.websocket_command(

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from hassil.recognize import RecognizeResult
@@ -30,6 +31,14 @@ TRIGGER_CALLBACK_TYPE = Callable[
]
@dataclass(slots=True)
class TriggerDetails:
"""List of sentences and the callback for a trigger."""
sentences: list[str]
callback: TRIGGER_CALLBACK_TYPE
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
for sentence in value:
@@ -140,5 +149,5 @@ async def async_attach_trigger(
return None
return get_agent_manager(hass).register_trigger(
sentences=sentences, trigger_callback=call_action
TriggerDetails(sentences=sentences, callback=call_action)
)

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.17.1"],
"requirements": ["pydaikin==2.17.2"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@@ -3,8 +3,9 @@
import logging
from datadog import DogStatsd, initialize
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
@@ -15,15 +16,53 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, state as state_helper
from homeassistant.helpers.typing import ConfigType
from . import config_flow as config_flow
from .const import CONF_RATE, DOMAIN
from .const import (
CONF_RATE,
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_RATE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
type DatadogConfigEntry = ConfigEntry[DogStatsd]
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
vol.Optional(CONF_RATE, default=DEFAULT_RATE): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Datadog integration from YAML, initiating config flow import."""
if DOMAIN not in config:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config[DOMAIN],
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> bool:

View File

@@ -12,7 +12,8 @@ from homeassistant.config_entries import (
OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import (
CONF_RATE,
@@ -70,6 +71,22 @@ class DatadogConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle import from configuration.yaml."""
# Check for duplicates
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
result = await self.async_step_user(user_input)
if errors := result.get("errors"):
await deprecate_yaml_issue(self.hass, False)
return self.async_abort(reason=errors["base"])
await deprecate_yaml_issue(self.hass, True)
return result
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
@@ -146,3 +163,41 @@ async def validate_datadog_connection(
return False
else:
return True
async def deprecate_yaml_issue(
hass: HomeAssistant,
import_success: bool,
) -> None:
"""Create an issue to deprecate YAML config."""
if import_success:
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
is_fixable=False,
issue_domain=DOMAIN,
breaks_in_ha_version="2026.2.0",
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Datadog",
},
)
else:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_connection_error",
breaks_in_ha_version="2026.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_connection_error",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Datadog",
"url": f"/config/integrations/dashboard/add?domain={DOMAIN}",
},
)

View File

@@ -25,6 +25,12 @@
}
}
},
"issues": {
"deprecated_yaml_import_connection_error": {
"description": "There was an error connecting to the Datadog Agent when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the {domain} configuration from your `configuration.yaml` file and continue to [set up the integration]({url}) manually.",
"title": "{domain} YAML configuration import failed"
}
},
"options": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.2.0"],
"requirements": ["denonavr==1.3.2"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -17,6 +17,7 @@ from denonavr.const import (
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_STOPPED,
)
from denonavr.exceptions import (
AvrCommandError,
@@ -103,6 +104,7 @@ DENON_STATE_MAPPING = {
STATE_OFF: MediaPlayerState.OFF,
STATE_PLAYING: MediaPlayerState.PLAYING,
STATE_PAUSED: MediaPlayerState.PAUSED,
STATE_STOPPED: MediaPlayerState.IDLE,
}

View File

@@ -7,7 +7,10 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import async_entity_id_to_device_id
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
async_remove_helper_config_entry_from_source_device,
@@ -19,6 +22,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Derivative from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass, entry.entry_id, entry.options[CONF_SOURCE]
)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime
import logging
import urllib.error
import urllib
from pyW215.pyW215 import SmartPlug

View File

@@ -46,12 +46,13 @@ class DSMRSensor(SensorEntity):
@callback
def message_received(message):
"""Handle new MQTT messages."""
if not (payload := message.payload):
if message.payload == "":
self._attr_native_value = None
elif (state := self.entity_description.state) is not None:
self._attr_native_value = state(payload)
elif self.entity_description.state is not None:
# Perform optional additional parsing
self._attr_native_value = self.entity_description.state(message.payload)
else:
self._attr_native_value = payload
self._attr_native_value = message.payload
self.async_write_ha_state()

View File

@@ -41,20 +41,13 @@ class UKFloodsFlowHandler(ConfigFlow, domain=DOMAIN):
self.stations = {}
for station in stations:
label = station["label"]
rloId = station["RLOIid"]
# API annoyingly sometimes returns a list and some times returns a string
# E.g. L3121 has a label of ['Scurf Dyke', 'Scurf Dyke Dyke Level']
if isinstance(label, list):
label = label[-1]
# Similar for RLOIid
# E.g. 0018 has an RLOIid of ['10427', '9154']
if isinstance(rloId, list):
rloId = rloId[-1]
fullName = label + " - " + rloId
self.stations[fullName] = station["stationReference"]
self.stations[label] = station["stationReference"]
if not self.stations:
return self.async_abort(reason="no_stations")

View File

@@ -53,6 +53,7 @@ class EheimDigitalUpdateCoordinator(
main_device_added_event=self.main_device_added_event,
)
self.known_devices: set[str] = set()
self.incomplete_devices: set[str] = set()
self.platform_callbacks: set[AsyncSetupDeviceEntitiesCallback] = set()
def add_platform_callback(
@@ -70,11 +71,26 @@ class EheimDigitalUpdateCoordinator(
This function is called from the library whenever a new device is added.
"""
if device_address not in self.known_devices:
if self.hub.devices[device_address].is_missing_data:
self.incomplete_devices.add(device_address)
return
if (
device_address not in self.known_devices
or device_address in self.incomplete_devices
):
for platform_callback in self.platform_callbacks:
platform_callback({device_address: self.hub.devices[device_address]})
if device_address in self.incomplete_devices:
self.incomplete_devices.remove(device_address)
async def _async_receive_callback(self) -> None:
if any(self.incomplete_devices):
for device_address in self.incomplete_devices.copy():
if not self.hub.devices[device_address].is_missing_data:
await self._async_device_found(
device_address, EheimDeviceType.VERSION_UNDEFINED
)
self.async_set_updated_data(self.hub.devices)
async def _async_setup(self) -> None:

View File

@@ -8,11 +8,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .const import DATA_ENOCEAN, DOMAIN, ENOCEAN_DONGLE
from .dongle import EnOceanDongle
type EnOceanConfigEntry = ConfigEntry[EnOceanDongle]
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.string})}, extra=vol.ALLOW_EXTRA
)
@@ -38,23 +36,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(
hass: HomeAssistant, config_entry: EnOceanConfigEntry
) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up an EnOcean dongle for the given entry."""
enocean_data = hass.data.setdefault(DATA_ENOCEAN, {})
usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE])
await usb_dongle.async_setup()
config_entry.runtime_data = usb_dongle
enocean_data[ENOCEAN_DONGLE] = usb_dongle
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: EnOceanConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload EnOcean config entry."""
enocean_dongle = config_entry.runtime_data
enocean_dongle = hass.data[DATA_ENOCEAN][ENOCEAN_DONGLE]
enocean_dongle.unload()
hass.data.pop(DATA_ENOCEAN)
return True

View File

@@ -5,6 +5,8 @@ import logging
from homeassistant.const import Platform
DOMAIN = "enocean"
DATA_ENOCEAN = "enocean"
ENOCEAN_DONGLE = "dongle"
ERROR_INVALID_DONGLE_PATH = "invalid_dongle_path"

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.3"],
"requirements": ["pyenphase==2.4.5"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==43.14.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.5.0"
"bleak-esphome==3.6.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["essent-dynamic-pricing==0.2.7"],
"requirements": ["essent-dynamic-pricing==0.3.1"],
"single_config_entry": true
}

View File

@@ -181,7 +181,7 @@ class EvoChild(EvoEntity):
self._device_state_attrs = {
"activeFaults": self._evo_device.active_faults,
"setpoints": self._setpoints,
"setpoints": self.setpoints,
}
super()._handle_coordinator_update()

View File

@@ -4,7 +4,7 @@
"codeowners": ["@zxdavb"],
"documentation": "https://www.home-assistant.io/integrations/evohome",
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.6"]
"requirements": ["evohome-async==1.1.3"]
}

View File

@@ -4,8 +4,6 @@ import asyncio.exceptions
from typing import Any
from flexit_bacnet import (
OPERATION_MODE_FIREPLACE,
OPERATION_MODE_OFF,
VENTILATION_MODE_AWAY,
VENTILATION_MODE_HOME,
VENTILATION_MODE_STOP,
@@ -14,6 +12,7 @@ from flexit_bacnet.bacnet import DecodingError
from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_HOME,
ClimateEntity,
ClimateEntityFeature,
@@ -29,10 +28,8 @@ from .const import (
DOMAIN,
MAX_TEMP,
MIN_TEMP,
OPERATION_TO_PRESET_MODE_MAP,
PRESET_FIREPLACE,
PRESET_HIGH,
PRESET_TO_VENTILATION_MODE_MAP,
VENTILATION_TO_PRESET_MODE_MAP,
)
from .coordinator import FlexitConfigEntry, FlexitCoordinator
from .entity import FlexitEntity
@@ -54,7 +51,6 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
"""Flexit air handling unit."""
_attr_name = None
_attr_translation_key = "flexit_bacnet"
_attr_hvac_modes = [
HVACMode.OFF,
@@ -64,8 +60,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
_attr_preset_modes = [
PRESET_AWAY,
PRESET_HOME,
PRESET_HIGH,
PRESET_FIREPLACE,
PRESET_BOOST,
]
_attr_supported_features = (
@@ -132,29 +127,20 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
Requires ClimateEntityFeature.PRESET_MODE.
"""
return OPERATION_TO_PRESET_MODE_MAP[self.device.operation_mode]
return VENTILATION_TO_PRESET_MODE_MAP[self.device.ventilation_mode]
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
try:
if preset_mode == PRESET_FIREPLACE:
# Use trigger method for fireplace mode
await self.device.trigger_fireplace_mode()
else:
# If currently in fireplace mode, toggle it off first
# trigger_fireplace_mode() acts as a toggle
if self.device.operation_mode == OPERATION_MODE_FIREPLACE:
await self.device.trigger_fireplace_mode()
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
# Set the desired ventilation mode
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
await self.device.set_ventilation_mode(ventilation_mode)
try:
await self.device.set_ventilation_mode(ventilation_mode)
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_preset_mode",
translation_placeholders={
"preset": preset_mode,
"preset": str(ventilation_mode),
},
) from exc
finally:
@@ -163,7 +149,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode."""
if self.device.operation_mode == OPERATION_MODE_OFF:
if self.device.ventilation_mode == VENTILATION_MODE_STOP:
return HVACMode.OFF
return HVACMode.FAN_ONLY

View File

@@ -1,40 +1,34 @@
"""Constants for the Flexit Nordic (BACnet) integration."""
from flexit_bacnet import (
OPERATION_MODE_AWAY,
OPERATION_MODE_FIREPLACE,
OPERATION_MODE_HIGH,
OPERATION_MODE_HOME,
OPERATION_MODE_OFF,
VENTILATION_MODE_AWAY,
VENTILATION_MODE_HIGH,
VENTILATION_MODE_HOME,
VENTILATION_MODE_STOP,
)
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME, PRESET_NONE
from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_HOME,
PRESET_NONE,
)
DOMAIN = "flexit_bacnet"
MAX_TEMP = 30
MIN_TEMP = 10
PRESET_HIGH = "high"
PRESET_FIREPLACE = "fireplace"
# Map operation mode (what device reports) to Home Assistant preset
OPERATION_TO_PRESET_MODE_MAP = {
OPERATION_MODE_OFF: PRESET_NONE,
OPERATION_MODE_AWAY: PRESET_AWAY,
OPERATION_MODE_HOME: PRESET_HOME,
OPERATION_MODE_HIGH: PRESET_HIGH,
OPERATION_MODE_FIREPLACE: PRESET_FIREPLACE,
VENTILATION_TO_PRESET_MODE_MAP = {
VENTILATION_MODE_STOP: PRESET_NONE,
VENTILATION_MODE_AWAY: PRESET_AWAY,
VENTILATION_MODE_HOME: PRESET_HOME,
VENTILATION_MODE_HIGH: PRESET_BOOST,
}
# Map preset to ventilation mode (for setting standard modes)
PRESET_TO_VENTILATION_MODE_MAP = {
PRESET_NONE: VENTILATION_MODE_STOP,
PRESET_AWAY: VENTILATION_MODE_AWAY,
PRESET_HOME: VENTILATION_MODE_HOME,
PRESET_HIGH: VENTILATION_MODE_HIGH,
PRESET_BOOST: VENTILATION_MODE_HIGH,
}

View File

@@ -1,17 +1,5 @@
{
"entity": {
"climate": {
"flexit_bacnet": {
"state_attributes": {
"preset_mode": {
"state": {
"fireplace": "mdi:fireplace",
"high": "mdi:fan-speed-3"
}
}
}
}
},
"number": {
"away_extract_fan_setpoint": {
"default": "mdi:fan-minus"

View File

@@ -26,18 +26,6 @@
"name": "Air filter polluted"
}
},
"climate": {
"flexit_bacnet": {
"state_attributes": {
"preset_mode": {
"state": {
"fireplace": "Fireplace",
"high": "[%key:common::state::high%]"
}
}
}
}
},
"number": {
"away_extract_fan_setpoint": {
"name": "Away extract fan setpoint"
@@ -151,11 +139,5 @@
"switch_turn": {
"message": "Failed to turn the switch {state}."
}
},
"issues": {
"deprecated_fireplace_switch": {
"description": "The fireplace mode switch entity `{entity_id}` is deprecated and will be removed in a future version.\n\nFireplace mode has been moved to a climate preset on the climate entity to better match the device interface.\n\nPlease update your automations to use the `climate.set_preset_mode` action with preset mode `fireplace` instead of using the switch entity.\n\nAfter updating your automations, you can safely disable this switch entity.",
"title": "Fireplace mode switch is deprecated"
}
}
}

View File

@@ -13,12 +13,9 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN
from .coordinator import FlexitConfigEntry, FlexitCoordinator
@@ -42,13 +39,6 @@ SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = (
turn_on_fn=lambda data: data.enable_electric_heater(),
turn_off_fn=lambda data: data.disable_electric_heater(),
),
FlexitSwitchEntityDescription(
key="cooker_hood_mode",
translation_key="cooker_hood_mode",
is_on_fn=lambda data: data.cooker_hood_status,
turn_on_fn=lambda data: data.activate_cooker_hood(),
turn_off_fn=lambda data: data.deactivate_cooker_hood(),
),
FlexitSwitchEntityDescription(
key="fireplace_mode",
translation_key="fireplace_mode",
@@ -56,6 +46,13 @@ SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = (
turn_on_fn=lambda data: data.trigger_fireplace_mode(),
turn_off_fn=lambda data: data.trigger_fireplace_mode(),
),
FlexitSwitchEntityDescription(
key="cooker_hood_mode",
translation_key="cooker_hood_mode",
is_on_fn=lambda data: data.cooker_hood_status,
turn_on_fn=lambda data: data.activate_cooker_hood(),
turn_off_fn=lambda data: data.deactivate_cooker_hood(),
),
)
@@ -67,42 +64,9 @@ async def async_setup_entry(
"""Set up Flexit (bacnet) switch from a config entry."""
coordinator = config_entry.runtime_data
entities: list[FlexitSwitch] = []
for description in SWITCHES:
if description.key == "fireplace_mode":
# Check if deprecated fireplace switch is enabled and create repair issue
entity_reg = er.async_get(hass)
fireplace_switch_unique_id = (
f"{coordinator.device.serial_number}-fireplace_mode"
)
# Look up the fireplace switch entity by unique_id
fireplace_switch_entity_id = entity_reg.async_get_entity_id(
Platform.SWITCH, DOMAIN, fireplace_switch_unique_id
)
if not fireplace_switch_entity_id:
continue
entity_registry_entry = entity_reg.async_get(fireplace_switch_entity_id)
if entity_registry_entry:
if entity_registry_entry.disabled:
entity_reg.async_remove(fireplace_switch_entity_id)
else:
async_create_issue(
hass,
DOMAIN,
f"deprecated_switch_{fireplace_switch_unique_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_fireplace_switch",
translation_placeholders={
"entity_id": fireplace_switch_entity_id,
},
)
entities.append(FlexitSwitch(coordinator, description))
else:
entities.append(FlexitSwitch(coordinator, description))
async_add_entities(entities)
async_add_entities(
FlexitSwitch(coordinator, description) for description in SWITCHES
)
PARALLEL_UPDATES = 1

View File

@@ -1,11 +1,22 @@
"""The Fressnapf Tracker integration."""
from fressnapftracker import AuthClient, FressnapfTrackerAuthenticationError
import logging
from fressnapftracker import (
ApiClient,
AuthClient,
Device,
FressnapfTrackerAuthenticationError,
FressnapfTrackerError,
FressnapfTrackerInvalidTrackerResponseError,
Tracker,
)
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import CONF_USER_ID, DOMAIN
from .coordinator import (
@@ -21,6 +32,43 @@ PLATFORMS: list[Platform] = [
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)
async def _get_valid_tracker(hass: HomeAssistant, device: Device) -> Tracker | None:
"""Test if the tracker returns valid data and return it.
Malformed data might indicate the tracker is broken or hasn't been properly registered with the app.
"""
client = ApiClient(
serial_number=device.serialnumber,
device_token=device.token,
client=get_async_client(hass),
)
try:
return await client.get_tracker()
except FressnapfTrackerInvalidTrackerResponseError:
_LOGGER.warning(
"Tracker with serialnumber %s is invalid. Consider removing it via the App",
device.serialnumber,
)
async_create_issue(
hass,
DOMAIN,
f"invalid_fressnapf_tracker_{device.serialnumber}",
issue_domain=DOMAIN,
learn_more_url="https://www.home-assistant.io/integrations/fressnapf_tracker/",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="invalid_fressnapf_tracker",
translation_placeholders={
"tracker_id": device.serialnumber,
},
)
return None
except FressnapfTrackerError as err:
raise ConfigEntryNotReady(err) from err
async def async_setup_entry(
hass: HomeAssistant, entry: FressnapfTrackerConfigEntry
@@ -40,12 +88,15 @@ async def async_setup_entry(
coordinators: list[FressnapfTrackerDataUpdateCoordinator] = []
for device in devices:
tracker = await _get_valid_tracker(hass, device)
if tracker is None:
continue
coordinator = FressnapfTrackerDataUpdateCoordinator(
hass,
entry,
device,
initial_data=tracker,
)
await coordinator.async_config_entry_first_refresh()
coordinators.append(coordinator)
entry.runtime_data = coordinators

View File

@@ -34,6 +34,7 @@ class FressnapfTrackerDataUpdateCoordinator(DataUpdateCoordinator[Tracker]):
hass: HomeAssistant,
config_entry: FressnapfTrackerConfigEntry,
device: Device,
initial_data: Tracker,
) -> None:
"""Initialize."""
super().__init__(
@@ -49,6 +50,7 @@ class FressnapfTrackerDataUpdateCoordinator(DataUpdateCoordinator[Tracker]):
device_token=device.token,
client=get_async_client(hass),
)
self.data = initial_data
async def _async_update_data(self) -> Tracker:
try:

View File

@@ -92,5 +92,11 @@
"not_seen_recently": {
"message": "The flashlight cannot be activated when the tracker has not moved recently."
}
},
"issues": {
"invalid_fressnapf_tracker": {
"description": "The Fressnapf GPS tracker with the serial number `{tracker_id}` is sending invalid data. This most likely indicates the tracker is broken or hasn't been properly registered with the app. Please remove the tracker via the Fressnapf app. You can then close this issue.",
"title": "Invalid Fressnapf GPS tracker detected"
}
}
}

View File

@@ -278,6 +278,12 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
"call_deflections"
] = await self.async_update_call_deflections()
except FRITZ_EXCEPTIONS as ex:
_LOGGER.debug(
"Reload %s due to error '%s' to ensure proper re-login",
self.config_entry.title,
ex,
)
self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature
from homeassistant.core import Event, HomeAssistant
@@ -57,7 +59,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> bool:
"""Unloading the AVM FRITZ!SmartHome platforms."""
await hass.async_add_executor_job(entry.runtime_data.fritz.logout)
try:
await hass.async_add_executor_job(entry.runtime_data.fritz.logout)
except (RequestConnectionError, HTTPError) as ex:
LOGGER.debug("logout failed with '%s', anyway continue with unload", ex)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -121,26 +121,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
def _update_fritz_devices(self) -> FritzboxCoordinatorData:
"""Update all fritzbox device data."""
try:
self.fritz.update_devices(ignore_removed=False)
if self.has_templates:
self.fritz.update_templates(ignore_removed=False)
if self.has_triggers:
self.fritz.update_triggers(ignore_removed=False)
except RequestConnectionError as ex:
raise UpdateFailed from ex
except HTTPError:
# If the device rebooted, login again
try:
self.fritz.login()
except LoginError as ex:
raise ConfigEntryAuthFailed from ex
self.fritz.update_devices(ignore_removed=False)
if self.has_templates:
self.fritz.update_templates(ignore_removed=False)
if self.has_triggers:
self.fritz.update_triggers(ignore_removed=False)
self.fritz.update_devices(ignore_removed=False)
if self.has_templates:
self.fritz.update_templates(ignore_removed=False)
if self.has_triggers:
self.fritz.update_triggers(ignore_removed=False)
devices = self.fritz.get_devices()
device_data = {}
@@ -193,7 +178,18 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
async def _async_update_data(self) -> FritzboxCoordinatorData:
"""Fetch all device data."""
new_data = await self.hass.async_add_executor_job(self._update_fritz_devices)
try:
new_data = await self.hass.async_add_executor_job(
self._update_fritz_devices
)
except (RequestConnectionError, HTTPError) as ex:
LOGGER.debug(
"Reload %s due to error '%s' to ensure proper re-login",
self.config_entry.title,
ex,
)
self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id)
raise UpdateFailed from ex
for device in new_data.devices.values():
# create device registry entry for new main devices

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260128.3"]
"requirements": ["home-assistant-frontend==20260128.6"]
}

View File

@@ -13,7 +13,10 @@ from homeassistant.helpers import (
discovery,
entity_registry as er,
)
from homeassistant.helpers.device import async_entity_id_to_device_id
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
@@ -93,6 +96,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_HUMIDIFIER],
)
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,

View File

@@ -5,7 +5,10 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device import async_entity_id_to_device_id
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
@@ -20,6 +23,13 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_HEATER],
)
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,

View File

@@ -101,6 +101,7 @@ class GoogleCalendarEntityDescription(CalendarEntityDescription):
search: str | None
local_sync: bool
device_id: str
initial_color: str | None = None
event_type: EventTypeEnum | None = None
@@ -360,6 +361,7 @@ class GoogleCalendarEntity(
if entity_description.entity_id:
self.entity_id = entity_description.entity_id
self._attr_unique_id = unique_id
self._attr_initial_color = entity_description.initial_color
if not entity_description.read_only:
self._attr_supported_features = (
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.3"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==3.0.0"]
"requirements": ["google_air_quality_api==3.0.1"]
}

View File

@@ -23,7 +23,7 @@
"pitch": "Default pitch of the voice",
"profiles": "Default audio profiles",
"speed": "Default rate/speed of the voice",
"stt_model": "Speech-to-Text model",
"stt_model": "Speech-to-text model",
"text_type": "Default text type",
"voice": "Default voice name (overrides language and gender)"
}

View File

@@ -7,6 +7,7 @@ import base64
import codecs
from collections.abc import AsyncGenerator, AsyncIterator, Callable
from dataclasses import dataclass, replace
import datetime
import mimetypes
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, cast
@@ -181,13 +182,25 @@ def _escape_decode(value: Any) -> Any:
return value
def _validate_tool_results(value: Any) -> Any:
"""Recursively convert non-json-serializable types."""
if isinstance(value, (datetime.time, datetime.date)):
return value.isoformat()
if isinstance(value, list):
return [_validate_tool_results(item) for item in value]
if isinstance(value, dict):
return {k: _validate_tool_results(v) for k, v in value.items()}
return value
def _create_google_tool_response_parts(
parts: list[conversation.ToolResultContent],
) -> list[Part]:
"""Create Google tool response parts."""
return [
Part.from_function_response(
name=tool_result.tool_name, response=tool_result.tool_result
name=tool_result.tool_name,
response=_validate_tool_results(tool_result.tool_result),
)
for tool_result in parts
]

View File

@@ -38,7 +38,11 @@ SENSOR_DESCRIPTIONS: list[GreenPlanetEnergySensorEntityDescription] = [
translation_key="highest_price_today",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=4,
value_fn=lambda api, data: api.get_highest_price_today(data),
value_fn=lambda api, data: (
price / 100
if (price := api.get_highest_price_today(data)) is not None
else None
),
),
GreenPlanetEnergySensorEntityDescription(
key="gpe_lowest_price_day",
@@ -46,7 +50,11 @@ SENSOR_DESCRIPTIONS: list[GreenPlanetEnergySensorEntityDescription] = [
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=4,
translation_placeholders={"time_range": "(06:00-18:00)"},
value_fn=lambda api, data: api.get_lowest_price_day(data),
value_fn=lambda api, data: (
price / 100
if (price := api.get_lowest_price_day(data)) is not None
else None
),
),
GreenPlanetEnergySensorEntityDescription(
key="gpe_lowest_price_night",
@@ -54,14 +62,22 @@ SENSOR_DESCRIPTIONS: list[GreenPlanetEnergySensorEntityDescription] = [
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=4,
translation_placeholders={"time_range": "(18:00-06:00)"},
value_fn=lambda api, data: api.get_lowest_price_night(data),
value_fn=lambda api, data: (
price / 100
if (price := api.get_lowest_price_night(data)) is not None
else None
),
),
GreenPlanetEnergySensorEntityDescription(
key="gpe_current_price",
translation_key="current_price",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=4,
value_fn=lambda api, data: api.get_current_price(data, dt_util.now().hour),
value_fn=lambda api, data: (
price / 100
if (price := api.get_current_price(data, dt_util.now().hour)) is not None
else None
),
),
]

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["growattServer"],
"requirements": ["growattServer==1.7.1"]
"requirements": ["growattServer==1.9.0"]
}

View File

@@ -152,6 +152,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
**kwargs: Any,
) -> None:
"""Install an update."""
self._attr_in_progress = True
self.async_write_ha_state()
await update_addon(
self.hass, self._addon_slug, backup, self.title, self.installed_version
)
@@ -308,6 +310,8 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
self._attr_in_progress = True
self.async_write_ha_state()
await update_core(self.hass, version, backup)
@callback

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/hdfury",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "gold",
"quality_scale": "silver",
"requirements": ["hdfury==1.4.2"],
"zeroconf": [
{ "name": "diva-*", "type": "_http._tcp.local." },

View File

@@ -46,26 +46,24 @@ rules:
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Device type integration.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
comment: The integration doesn't have any repair cases.
repair-issues: todo
stale-devices:
status: exempt
comment: Device type integration.

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.4.1"]
"requirements": ["pyHik==0.4.2"]
}

View File

@@ -8,7 +8,10 @@ import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import async_entity_id_to_device_id
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
async_remove_helper_config_entry_from_source_device,
@@ -50,6 +53,13 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_ENTITY_ID],
)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,

View File

@@ -115,7 +115,6 @@ SENSORS = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
translation_key="hot_water_counter",
),
HomeConnectSensorEntityDescription(

View File

@@ -31,6 +31,7 @@ HOMEE_UNIT_TO_HA_UNIT = {
"n/a": None,
"text": None,
"%": PERCENTAGE,
"Lux": LIGHT_LUX,
"lx": LIGHT_LUX,
"klx": LIGHT_LUX,
"1/min": REVOLUTIONS_PER_MINUTE,

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