Compare commits

..

205 Commits

Author SHA1 Message Date
Bram Kragten 456f992b7e 2025.7.3 (#149024) 2025-07-22 10:30:09 +02:00
Franck Nijhof 0675e34c62 Bump version to 2025.7.3 2025-07-18 17:05:52 +00:00
Simone Chemelli 190c98f5a8 Bump aioamazondevices to 3.5.0 (#149011) 2025-07-18 17:04:01 +00:00
Jan Bouwhuis c6bb26be89 Ignore MQTT sensor unit of measurement if it is an empty string (#149006) 2025-07-18 17:02:02 +00:00
J. Nick Koston d57c5ffa8f Bump PySwitchbot to 0.68.2 (#148996) 2025-07-18 17:02:01 +00:00
Bram Kragten 68889e1790 Update frontend to 20250702.3 (#148994) 2025-07-18 17:02:00 +00:00
Joost Lekkerkerker 8fdc50a29f Pass Syncthru entry to coordinator (#148974) 2025-07-18 17:01:58 +00:00
Steven Looman 5656b4c20d Bump async-upnp-client to 0.45.0 (#148961) 2025-07-18 17:01:57 +00:00
Maciej Bieniek b6edcc9422 Bump gios to version 6.1.2 (#148884) 2025-07-18 17:01:56 +00:00
Maciej Bieniek 7a3eb53453 Bump gios to version 6.1.1 (#148414) 2025-07-18 17:01:54 +00:00
Arie Catsman 11a2c73e8a Bump pyenphase to 2.2.2 (#148870) 2025-07-18 17:00:32 +00:00
Brett Adams 1644484c92 Fix button platform parent class in Teslemetry (#148863) 2025-07-18 17:00:30 +00:00
Pete Sage 8e0a89dc2f Add guard to prevent exception in Sonos Favorites (#148854) 2025-07-18 17:00:29 +00:00
Robert Resch 9e4b8df344 Use ffmpeg for generic cameras in go2rtc (#148818) 2025-07-18 17:00:28 +00:00
Brett Adams 69fdc1d269 Bump Tesla Fleet API to 1.2.2 (#148776) 2025-07-18 17:00:26 +00:00
Joost Lekkerkerker 56e0aa103d Bump pySmartThings to 3.2.8 (#148761) 2025-07-18 17:00:25 +00:00
Maciej Bieniek caf0492009 Fix Shelly n_current sensor removal condition (#148740) 2025-07-18 17:00:24 +00:00
hahn-th c6d0aad3d3 Handle connection issues after websocket reconnected in homematicip_cloud (#147731) 2025-07-18 17:00:22 +00:00
Franck Nijhof 1f59b735c6 2025.7.2 (#148725) 2025-07-14 13:12:29 +02:00
Franck Nijhof 87af9fc8ba Bump version to 2025.7.2 2025-07-14 10:30:35 +00:00
Simone Chemelli 691a0ca065 Bump aioamazondevices to 3.2.10 (#148709) 2025-07-14 10:27:45 +00:00
Shay Levy 80384b89a5 Bump aioshelly to 13.7.2 (#148706) 2025-07-14 10:25:24 +00:00
Jan Bouwhuis f7672985ed Fix hide empty sections in mqtt subentry flows (#148692) 2025-07-14 10:25:23 +00:00
Christopher Fenner d4374dbcc7 Bump PyViCare to 2.50.0 (#148679) 2025-07-14 10:25:22 +00:00
Brett Adams c4ddcd64c8 Fix Charge Cable binary sensor in Teslemetry (#148675) 2025-07-14 10:25:21 +00:00
0xEF c802430066 Bump nyt_games to 0.5.0 (#148654) 2025-07-14 10:25:20 +00:00
falconindy 649fbfc729 snoo: use correct value for right safety clip binary sensor (#148647) 2025-07-14 10:25:18 +00:00
Jan Bouwhuis 80c52ad8ea Fix - only enable AlexaModeController if at least one mode is offered (#148614) 2025-07-14 10:25:17 +00:00
Lưu Quang Vũ 150d4716fa Fix Google Cloud 504 Deadline Exceeded (#148589) 2025-07-14 10:25:16 +00:00
Bram Kragten dc2736580f Update frontend to 20250702.2 (#148573) 2025-07-14 10:25:15 +00:00
J. Nick Koston f1272ef513 Bump aiohttp to 3.12.14 (#148565) 2025-07-14 10:25:14 +00:00
Åke Strandberg 3c2fa023b4 Remove vg argument from miele auth flow (#148541) 2025-07-14 10:25:12 +00:00
Kristof Mariën 5cf5be8c9c Fix for Renson set Breeze fan speed (#148537) 2025-07-14 10:25:11 +00:00
Jan-Philipp Benecke 63b21fda1a Ensure response is fully read to prevent premature connection closure in rest command (#148532) 2025-07-14 10:25:10 +00:00
J. Diego Rodríguez Royo d87379d083 Use the link to the issue instead of creating new issues at Home Connect (#148523) 2025-07-14 10:25:09 +00:00
J. Diego Rodríguez Royo 0990cef917 Add Home Connect resume command button when an appliance is paused (#148512) 2025-07-14 10:25:08 +00:00
Michael 962ad99c20 Add workaround for sub units without main device in AVM Fritz!SmartHome (#148507) 2025-07-14 10:25:07 +00:00
Michael 9c9836defd Bump aioimmich to 0.10.2 (#148503) 2025-07-14 10:25:06 +00:00
Jan Bouwhuis e951fc401c Fix entity_id should be based on object_id the first time an entity is added (#148484) 2025-07-14 10:25:05 +00:00
Robert Resch 00e2a177a5 Revert "Deprecate hddtemp" (#148482) 2025-07-14 10:25:04 +00:00
Joakim Sørensen b6d316c8f2 Bump hass-nabucasa from 0.105.0 to 0.106.0 (#148473) 2025-07-14 10:25:02 +00:00
Raphael Hehl b8425de0d0 Bump uiprotect to version 7.14.2 (#148453) 2025-07-14 10:25:01 +00:00
Joost Lekkerkerker d51a44acbc Bump pySmartThings to 3.2.7 (#148394) 2025-07-14 10:25:00 +00:00
Josef Zweck 435465e569 Bump pylamarzocco to 2.0.11 (#148386) 2025-07-14 10:24:59 +00:00
Josef Zweck 3b047859f9 Create own clientsession for lamarzocco (#148385) 2025-07-14 10:24:58 +00:00
Simone Chemelli 91cdf1a367 Bump aioamazondevices to 3.2.8 (#148365)
Co-authored-by: Joakim Plate <elupus@ecce.se>
2025-07-14 10:24:57 +00:00
Joakim Plate 2377b136f3 Handle binary coils with non default mappings in nibe heatpump (#148354) 2025-07-14 10:24:56 +00:00
Retha Runolfsson 186c4e7038 Bump pyswitchbot to 0.68.1 (#148335) 2025-07-14 10:24:55 +00:00
Samuel Xiao d303a7d17e Fix Switchbot cloud plug mini current unit Issue (#148314) 2025-07-14 10:24:54 +00:00
jvits227 14f059c766 Add lamp states to smartthings selector (#148302)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-07-14 10:23:55 +00:00
Arie Catsman 4a10370932 Bump pyenphase to 2.2.1 (#148292) 2025-07-14 10:13:43 +00:00
J. Nick Koston 672ffa5984 Restore httpx compatibility for non-primitive REST query parameters (#148286) 2025-07-14 10:13:42 +00:00
Maciej Bieniek 3d3f2527cb Bump gios to version 6.1.0 (#148274) 2025-07-14 10:13:41 +00:00
Shay Levy 5c3b279f95 Bump aiowebostv to 0.7.4 (#148273) 2025-07-14 10:13:40 +00:00
starkillerOG 59bcf1167a bump motionblinds to 0.6.29 (#148265) 2025-07-14 10:13:39 +00:00
Mark Adkins b4d789f8e2 Bump sharkiq to 1.1.1 (#148244) 2025-07-14 10:12:00 +00:00
Josef Zweck f4ca56052b Bump pylamarzocco to 2.0.10 (#148233) 2025-07-14 10:11:59 +00:00
J. Nick Koston 74f9549431 Fix UTF-8 encoding for REST basic authentication (#148225) 2025-07-14 10:11:58 +00:00
J. Nick Koston 9650727515 Fix REST sensor charset handling to respect Content-Type header (#148223) 2025-07-14 10:11:57 +00:00
TimL c965da6559 Bump pysmlight to v0.2.7 (#148101)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-14 10:09:19 +00:00
Sören Beye 9077965214 Squeezebox: Fix tracks not having thumbnails (#147187) 2025-07-14 10:09:18 +00:00
Sören Beye 2b7992e849 Squeezebox: Fix track selection in media browser (#147185) 2025-07-14 10:09:16 +00:00
Franck Nijhof 5d6b02f470 2025.7.1 (#148171) 2025-07-04 22:00:18 +02:00
Franck Nijhof a274961593 Bump version to 2025.7.1 2025-07-04 19:22:41 +00:00
Michael Freeman 4e163c4591 Bump venstarcolortouch to 0.21 (#148152) 2025-07-04 19:21:33 +00:00
Marc Mueller 3ffec2a655 [ci] Fix typing issue with aiohttp and aiosignal (#148141) 2025-07-04 19:21:31 +00:00
Bram Kragten c646658643 Update frontend to 20250702.1 (#148131) 2025-07-04 19:21:30 +00:00
Simone Chemelli 342b4c3442 Bump aioamazondevices to 3.2.3 (#148082) 2025-07-04 19:21:28 +00:00
Arie Catsman eb58c10e5e Cancel enphase mac verification on unload. (#148072) 2025-07-04 19:21:27 +00:00
Arie Catsman f42e7d982f Bump pyenphase to 2.2.0 (#148070) 2025-07-04 19:21:25 +00:00
hanwg 898ef43750 Fix Telegram bots using plain text parser failing to load on restart (#148050) 2025-07-04 19:21:24 +00:00
Joakim Sørensen f806e6ba49 Bump hass-nabucasa from 0.104.0 to 0.105.0 (#148040) 2025-07-04 19:21:23 +00:00
Marcel van der Veldt c23bfb1b39 Fix state being incorrectly reported in some situations on Music Assistant players (#147997) 2025-07-04 19:21:22 +00:00
Robert Svensson a2ffe32b02 Bump aiounifi to v84 (#147987) 2025-07-04 19:21:21 +00:00
puddly 0f32b6331d Bump ZHA to 0.0.62 (#147966) 2025-07-04 19:21:19 +00:00
epenet 9a4959560e Fix missing port in samsungtv (#147962)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-04 19:21:18 +00:00
Thomas55555 41ab7b346c Set timeout for remote calendar (#147024) 2025-07-04 19:21:17 +00:00
Franck Nijhof 4bc2951f44 2025.7.0 (#147533) 2025-07-02 18:01:06 +02:00
Franck Nijhof 8334a0398c Bump version to 2025.7.0 2025-07-02 15:12:16 +00:00
Franck Nijhof 8fc3fa51a8 Bump version to 2025.7.0b9 2025-07-02 13:30:51 +00:00
c0ffeeca7 4eb688b560 Z-Wave JS: rename controller to adapter according to term decision (#147955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 13:30:31 +00:00
Simone Chemelli 9472ff5d36 Bump aioamazondevices to 3.2.2 (#147953) 2025-07-02 13:30:29 +00:00
Bram Kragten 12e8b81ec7 Update frontend to 20250702.0 (#147952) 2025-07-02 13:30:28 +00:00
Paulus Schoutsen ec5e543c09 Ollama: Migrate pick model to subentry (#147944) 2025-07-02 13:30:27 +00:00
Paulus Schoutsen 116c745872 Split Ollama entity (#147769) 2025-07-02 13:30:26 +00:00
Robert Resch 1fdf152292 Bump deebot-client to 13.5.0 (#147938) 2025-07-02 13:27:47 +00:00
G Johansson b816f1a408 Handle additional errors in Nord Pool (#147937) 2025-07-02 13:27:46 +00:00
John Hess eb351e6505 Bump thermopro-ble to 0.13.1 (#147924) 2025-07-02 13:27:45 +00:00
Maciej Bieniek 2f27d55495 Open repair issue when outbound WebSocket is enabled for Shelly non-sleeping RPC device (#147901) 2025-07-02 13:26:03 +00:00
Space fa1bed1849 Skip processing request body for HTTP HEAD requests (#147899)
* Skip processing request body for HTTP HEAD requests

* Use aiohttp's must_be_empty_body() to check whether ingress requests should be streamed

* Only call must_be_empty_body() once per request

* Fix incorrect use of walrus operator
2025-07-02 13:26:01 +00:00
Raphael Hehl b8c19f23f3 UnifiProtect Change log level from debug to error for connection exceptions in ProtectFlowHandler (#147730) 2025-07-02 13:26:00 +00:00
Erwin Douna b677ce6c90 SMA add DHCP strictness (#145753)
* Add DHCP strictness (needs beta check)

* Update to check on CONF_MAC

* Update to check on CONF_HOST

* Update hostname

* Polish it a bit

* Update to CONF_HOST, again

* Add split

* Add CONF_MAC add upon detection

* epenet feedback

* epenet round II
2025-07-02 13:25:59 +00:00
Franck Nijhof 0e6bbb30c1 Bump version to 2025.7.0b8 2025-07-02 06:04:14 +00:00
J. Nick Koston fdba791f18 Bump bluetooth-data-tools to 1.28.2 (#147920) 2025-07-02 06:03:56 +00:00
Ivan Lopez Hernandez d4dec6c7a9 Swap the Models label for the model name not it's display name, (#147918)
Swap display name for name.
2025-07-02 06:03:55 +00:00
Simone Chemelli f838e85a79 Manager wrong country selection in Alexa Devices (#147914)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-07-02 06:03:54 +00:00
Simone Chemelli 04ae966544 Bump aioamazondevices to 3.2.1 (#147912) 2025-07-02 06:03:53 +00:00
Franck Nijhof b2c393db72 Bump version to 2025.7.0b7 2025-07-01 20:11:01 +00:00
Marcel van der Veldt 3ed440a3af Bump Music Assistant Client to 1.2.3 (#147885) 2025-07-01 20:08:45 +00:00
Jamin 01e7efc7b4 Bump VoIP utils to 0.3.3 (#147880) 2025-07-01 20:08:44 +00:00
avee87 60a930554a Fix station name sensor for metoffice (#145500) 2025-07-01 20:08:43 +00:00
Franck Nijhof c707bf6264 Bump version to 2025.7.0b6 2025-07-01 14:26:59 +00:00
Paul Bottein 3548ab70fd Update frontend to 20250701.0 (#147879) 2025-07-01 14:10:30 +00:00
Erik Montnemery e272ab1885 Initialize EsphomeEntity._has_state (#147877) 2025-07-01 14:10:29 +00:00
Erik Montnemery d5d1b620d0 Correct openai conversation config entry migration (#147859) 2025-07-01 14:10:28 +00:00
Erik Montnemery 8b2f4f0f86 Correct ollama config entry migration (#147858) 2025-07-01 14:10:26 +00:00
Erik Montnemery 725269ecda Correct anthropic config entry migration (#147857) 2025-07-01 14:10:25 +00:00
Erik Montnemery c42fc818bf Correct Google generative AI config entry migration (#147856) 2025-07-01 14:10:23 +00:00
Jesse Hills 5554e38171 Implement suggested_display_precision for ESPHome (#147849) 2025-07-01 14:10:22 +00:00
Jan Bouwhuis b25acfe823 Fix invalid configuration of MQTT device QoS option in subentry flow (#147837) 2025-07-01 14:10:21 +00:00
micha91 ff25948e37 fix: Create new aiohttp session with DummyCookieJar (#147827) 2025-07-01 14:10:19 +00:00
Maciej Bieniek f85fc7173f Bump Nettigo Air Monitor backend library to version 5.0.0 (#147812) 2025-07-01 14:10:18 +00:00
Bob Laz 748cc6386d fix state_class for water used today sensor (#147787) 2025-07-01 14:10:17 +00:00
Manu 47b232db49 Add more mac address prefixes for discovery to PlayStation Network (#147739) 2025-07-01 14:10:15 +00:00
hanwg c61935fc41 Include chat ID in Telegram bot subentry title (#147643) 2025-07-01 14:10:14 +00:00
Jan-Philipp Benecke 414318f3fb Catch access denied errors in webdav and display proper message (#147093) 2025-07-01 14:10:12 +00:00
Paul Bottein 08985d783f Fix Meteo france Ciel clair condition mapping (#146965)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-01 14:10:11 +00:00
Thomas55555 e4bcde7d20 Fix wrong state in Husqvarna Automower (#146075) 2025-07-01 14:10:10 +00:00
Franck Nijhof db04c77e62 Bump version to 2025.7.0b5 2025-06-30 19:39:34 +00:00
puddly e8204e5f8e Await firmware installation task when flashing ZBT-1/Yellow firmware (#147824) 2025-06-30 19:39:03 +00:00
starkillerOG 66cf9c4ed5 Bump reolink_aio to 0.14.2 (#147797) 2025-06-30 19:39:02 +00:00
mkmer 1f6d28dcbf Honeywell: Don't use shared session (#147772) 2025-06-30 19:39:02 +00:00
Paulus Schoutsen 328e838351 Use media selector for Assist Satellite actions (#147767)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-06-30 19:39:01 +00:00
cdnninja 62a1c8af11 Fix Vesync set_percentage error (#147751) 2025-06-30 19:39:00 +00:00
tronikos b50e599517 Move the async_reload on updates in async_setup_entry in Google Generative AI (#147748)
Move the async_reload on updates in async_setup_entry
2025-06-30 19:38:59 +00:00
Manu 3c7c9176d2 Fix sensor displaying unknown when getting readings from heat meters in ista EcoTrend (#147741) 2025-06-30 19:37:54 +00:00
J. Nick Koston c771f5fe1e Preserve httpx boolean behavior in REST integration after aiohttp conversion (#147738) 2025-06-30 19:35:31 +00:00
hanwg 6dc464ad73 Fix Telegram bot proxy URL not initialized when creating a new bot (#147707) 2025-06-30 19:35:30 +00:00
Marc Hörsken ae48e3716e Update pywmspro to 0.3.0 to wait for short-lived actions (#147679)
Replace action delays with detailed action responses.
2025-06-30 19:35:29 +00:00
Hessel 1543726095 Wallbox Integration, Reduce API impact by limiting the amount of API calls made (#147618) 2025-06-30 19:35:27 +00:00
Evan Severson adbace95c3 Fixed pushbullet handling of fields longer than 255 characters (#146993) 2025-06-30 19:35:26 +00:00
Shay Levy 578b43cf61 Bump aioshelly to 13.7.1 (#146221)
* Bump aioshelly to 13.8.0

* Change version to 13.7.1
2025-06-30 19:35:25 +00:00
mvn23 a8b5d1511d Populate hvac_modes list in opentherm_gw (#142074) 2025-06-30 19:35:24 +00:00
Pete Sage 5a0a1bbbf4 Person ble_trackers for non-home zones not processed correctly (#138475)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-30 19:35:23 +00:00
Paulus Schoutsen cf2e69ed74 Bump version to 2025.7.0b4 2025-06-28 20:27:42 +00:00
J. Nick Koston c32b44b774 Improve rest error logging (#147736)
* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* Improve rest error logging

* top level
2025-06-28 20:27:20 +00:00
Florian von Garrel 2f69ed4a8a bump pypaperless to 4.1.1 (#147735) 2025-06-28 20:27:19 +00:00
Marc Hörsken 4b3449fe0c Fix error if cover position is not available or unknown (#147732) 2025-06-28 20:27:18 +00:00
starkillerOG 33e1c6de68 Reduce idle timeout of HLS stream to conserve camera battery life (#147728)
* Reduce IDLE timeout of HLS stream to conserve camera battery life

* adjust tests
2025-06-28 20:27:17 +00:00
Daniel Hjelseth Høyer 81e712ea49 Bump pytibber to 0.31.6 (#147703) 2025-06-28 20:27:16 +00:00
Shay Levy d3c5684cd0 Fix Shelly Block entity removal (#147694) 2025-06-28 20:27:16 +00:00
Jan Bouwhuis 862b7460b5 Move MQTT device sw and hw version to collapsed section in subentry flow (#147685)
Move MQTT device sw and hw version to collapsed section
2025-06-28 20:27:15 +00:00
Samuel Xiao a65eb57539 Add lock models to switchbot cloud (#147569) 2025-06-28 20:27:14 +00:00
Antoni Czaplicki b537850f52 Bump vulcan-api to 2.4.2 (#146857) 2025-06-28 20:27:13 +00:00
Franck Nijhof 16c6bd08f8 Bump version to 2025.7.0b3 2025-06-27 17:55:31 +00:00
Simone Chemelli 18834849c2 Bump aioamazondevices to 3.1.22 (#147681) 2025-06-27 17:54:40 +00:00
hanwg e4d820799f Add codeowner for Telegram bot (#147680) 2025-06-27 17:54:38 +00:00
mkmer 013a35176a Bump aiosomecomfort to 0.0.33 (#147673) 2025-06-27 17:54:37 +00:00
Norbert Rittel 8230557aef Fix sentence-casing and spacing of button in thermopro (#147671) 2025-06-27 17:54:36 +00:00
Paul Bottein 5451063714 Update frontend to 20250627.0 (#147668) 2025-06-27 17:54:35 +00:00
Shay Levy 8cdc7523a4 Fix Shelly entity removal (#147665) 2025-06-27 17:54:33 +00:00
Josef Zweck 77ccfbd3a9 Fix: Unhandled NoneType sessions in jellyfin (#147659) 2025-06-27 17:54:32 +00:00
Josef Zweck 4977ee4998 Bump jellyfin-apiclient-python to 1.11.0 (#147658) 2025-06-27 17:54:31 +00:00
Josef Zweck 5c0f2d37f0 Make jellyfin not single config entry (#147656) 2025-06-27 17:54:29 +00:00
Thomas55555 0b5d2ab8e4 Respect availability of parent class in Husqvarna Automower (#147649) 2025-06-27 17:54:28 +00:00
Brett Adams 47f3bf29dd Fix energy history in Teslemetry (#147646) 2025-06-27 17:54:26 +00:00
Manu 62f7cbb51e Remove dweet.io integration (#147645) 2025-06-27 17:54:25 +00:00
Bernardus Jansen b9e2c5d34c Add previously missing state classes to dsmr sensors (#147633) 2025-06-27 17:54:24 +00:00
Petar Petrov 1829acd0e1 Z-WaveJS config flow: Change keys question (#147518)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 17:54:22 +00:00
Franck Nijhof 41b9a7a9a3 Bump version to 2025.7.0b2 2025-06-27 08:08:02 +00:00
Norbert Rittel 9782637ec8 Clarify descriptions of subaru.unlock_specific_door action (#147655) 2025-06-27 08:05:06 +00:00
Manu 6bd6fa65d2 Bump pynecil to v4.1.1 (#147648) 2025-06-27 08:05:05 +00:00
Joost Lekkerkerker 85343a9f53 Make sure Ollama integration migration is clean (#147630) 2025-06-27 08:05:04 +00:00
Joost Lekkerkerker bc607dd013 Make sure Anthropic integration migration is clean (#147629) 2025-06-27 08:05:02 +00:00
Joost Lekkerkerker c2c388e0cc Make sure OpenAI integration migration is clean (#147627) 2025-06-27 08:05:01 +00:00
Joost Lekkerkerker 3fc154e1d7 Make sure Google Generative AI integration migration is clean (#147625) 2025-06-27 08:05:00 +00:00
Jack Powell efb29d024e Add Diagnostics to PlayStation Network (#147607)
* Add Diagnostics support to PlayStation_Network

* Remove unused constant

* minor cleanup

* Redact additional data

* Redact additional data
2025-06-27 08:04:58 +00:00
Michael 263823c92c Fix config schema to make credentials optional in NUT flows (#147593) 2025-06-27 08:04:57 +00:00
hanwg e5e6ed601b Fix Telegram bot yaml import for webhooks containing None value for URL (#147586) 2025-06-27 08:04:56 +00:00
Petar Petrov 28dfc997f3 Do not factory reset old Z-Wave controller during migration (#147576)
* Do not factory reset old Z-Wave controller during migration

* PR comments

* remove obsolete test
2025-06-27 08:04:55 +00:00
puddly f93ab8d519 Allow setup of Zigbee/Thread for ZBT-1 and Yellow without internet access (#147549)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-27 08:04:54 +00:00
Josef Zweck cb359da79e Make entities unavailable when machine is physically off in lamarzocco (#147426) 2025-06-27 08:04:52 +00:00
Franck Nijhof 6a7385590a Bump version to 2025.7.0b1 2025-06-26 18:03:11 +00:00
Joost Lekkerkerker c0ec987b07 Fix meaters not being added after a reload (#147614) 2025-06-26 18:02:49 +00:00
Joost Lekkerkerker 26521f8cc0 Hide Telegram bot proxy URL behind section (#147613)
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
2025-06-26 18:02:48 +00:00
Manu 4df1f702bf Fix asset url in Habitica integration (#147612) 2025-06-26 18:02:46 +00:00
Joost Lekkerkerker c8422c9fb8 Improve explanation on how to get API token in Telegram (#147605) 2025-06-26 18:02:45 +00:00
Luca Angemi f8207a2e0e Remove default icon for wind direction sensor for Buienradar (#147603)
* Fix wind direction state class sensor

* Remove default icon for wind direction sensor
2025-06-26 18:02:44 +00:00
Bram Kragten 9cc75f3458 Update frontend to 20250626.0 (#147601) 2025-06-26 18:02:43 +00:00
Joost Lekkerkerker a233b6b1e3 Add default title to migrated Ollama entry (#147599) 2025-06-26 18:02:42 +00:00
Joost Lekkerkerker c7677b91da Add default title to migrated Claude entry (#147598) 2025-06-26 18:02:40 +00:00
Joost Lekkerkerker 1f57bba9cd Add default conversation name for OpenAI integration (#147597) 2025-06-26 18:02:39 +00:00
Joost Lekkerkerker 4cc10ca2e2 Set Google AI model as device model (#147582)
* Set Google AI model as device model

* fix
2025-06-26 18:02:38 +00:00
Marcel van der Veldt 153e1e43e8 Do not make the favorite button unavailable when no content playing on a Music Assistant player (#147579) 2025-06-26 18:02:36 +00:00
Joost Lekkerkerker 398dd3ae46 Set right model in OpenAI conversation (#147575) 2025-06-26 18:02:35 +00:00
Petar Petrov 17fd850fa6 Hide unnamed paths when selecting a USB Z-Wave adapter (#147571)
* Hide unnamed paths when selecting a USB Z-Wave adapter

* remove pointless sorting
2025-06-26 18:02:34 +00:00
Petar Petrov ae062b230c Remove obsolete routing info when migrating a Z-Wave network (#147568) 2025-06-26 18:02:33 +00:00
Marcel van der Veldt d523f85404 Fix sending commands to Matter vacuum (#147567) 2025-06-26 18:02:31 +00:00
tronikos f28d6582c6 Refactor in Google AI TTS in preparation for STT (#147562) 2025-06-26 18:02:30 +00:00
Petar Petrov 1e81e5990e Bump zwave-js-server-python to 0.65.0 (#147561)
* Bump zwave-js-server-python to 0.65.0

* update tests
2025-06-26 18:02:29 +00:00
tronikos 5fe2e4b6ed Include subentries in Google Generative AI diagnostics (#147558) 2025-06-26 18:02:28 +00:00
tronikos 914bb3aa76 Use default title for migrated Google Generative AI entries (#147551) 2025-06-26 18:02:26 +00:00
Simone Chemelli cfa6746115 Fix unload for Alexa Devices (#147548) 2025-06-26 18:02:25 +00:00
Simone Chemelli 03f9caf3eb Add action exceptions to Alexa Devices (#147546) 2025-06-26 18:02:24 +00:00
Joost Lekkerkerker 6b2aaf3fdb Show current Lametric version if there is no newer version (#147538) 2025-06-26 18:02:23 +00:00
Luca Angemi 2c4ea0d584 Fix wind direction state class sensor for AEMET (#147535) 2025-06-26 18:02:21 +00:00
Anders Peter Fugmann e627811f7a Bump dependency on pyW215 for DLink integration to 0.8.0 (#147534) 2025-06-26 18:02:20 +00:00
Simone Chemelli 150f41641b Improve config flow strings for Alexa Devices (#147523) 2025-06-26 18:02:19 +00:00
Erik Montnemery b9a7371996 Set end date for when allowing unique id collisions in config entries (#147516)
* Set end date for when allowing unique id collisions in config entries

* Update test
2025-06-26 18:02:17 +00:00
tronikos 7d0e99da43 Fixes in Google AI TTS (#147501)
* Fix Google AI not using correct config options after subentries migration

* Fixes in Google AI TTS

* Fix tests by @IvanLH

* Change type name.

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-06-26 18:02:16 +00:00
hanwg 71f281cc14 Fix Telegram bot default target when sending messages (#147470)
* handle targets

* updated error message

* validate chat id for single target

* add validation for chat id

* handle empty target

* handle empty target
2025-06-26 18:02:15 +00:00
Renat Sibgatulin aec812a475 Create a new client session for air-Q to fix cookie polution (#147027) 2025-06-26 18:00:50 +00:00
Robin Lintermann d4b548b169 Fixed issue when tests (should) fail in Smarla (#146102)
* Fixed issue when tests (should) fail

* Use usefixture decorator

* Throw ConfigEntryError instead of AuthFailed
2025-06-26 18:00:48 +00:00
Fabio Natanael Kepler a296324c30 Fix playing TTS and local media source over DLNA (#134903)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-06-26 18:00:47 +00:00
Franck Nijhof cff3d3d6ac Bump version to 2025.7.0b0 2025-06-25 18:51:19 +00:00
416 changed files with 7971 additions and 8208 deletions
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -37,10 +37,10 @@ on:
type: boolean
env:
CACHE_VERSION: 3
CACHE_VERSION: 4
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.8"
HA_SHORT_VERSION: "2025.7"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.29.2
uses: github/codeql-action/init@v3.29.0
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.29.2
uses: github/codeql-action/analyze@v3.29.0
with:
category: "/language:python"
+1 -5
View File
@@ -137,8 +137,4 @@ tmp_cache
.ropeproject
# Will be created from script/split_tests.py
pytest_buckets.txt
# AI tooling
.claude
pytest_buckets.txt
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
rev: v0.12.0
hooks:
- id: ruff-check
args:
-1
View File
@@ -1 +0,0 @@
.github/copilot-instructions.md
+7 -2
View File
@@ -75,6 +75,7 @@ from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
backup,
category_registry,
config_validation as cv,
device_registry,
@@ -606,7 +607,7 @@ async def async_enable_logging(
)
threading.excepthook = lambda args: logging.getLogger().exception(
"Uncaught thread exception",
exc_info=( # type: ignore[arg-type]
exc_info=( # type: ignore[arg-type] # noqa: LOG014
args.exc_type,
args.exc_value,
args.exc_traceback,
@@ -879,6 +880,10 @@ async def _async_set_up_integrations(
if "recorder" in all_domains:
recorder.async_initialize_recorder(hass)
# Initialize backup
if "backup" in all_domains:
backup.async_initialize_backup(hass)
stages: list[tuple[str, set[str], int | None]] = [
*(
(name, domain_group, timeout)
@@ -1056,5 +1061,5 @@ async def _async_setup_multi_components(
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(result), result, result.__traceback__),
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
)
+18 -5
View File
@@ -505,8 +505,13 @@ class ClimateCapabilities(AlexaEntity):
):
yield AlexaThermostatController(self.hass, self.entity)
yield AlexaTemperatureSensor(self.hass, self.entity)
if self.entity.domain == water_heater.DOMAIN and (
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
if (
self.entity.domain == water_heater.DOMAIN
and (
supported_features
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
)
and self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST)
):
yield AlexaModeController(
self.entity,
@@ -634,7 +639,9 @@ class FanCapabilities(AlexaEntity):
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
)
force_range_controller = False
if supported & fan.FanEntityFeature.PRESET_MODE:
if supported & fan.FanEntityFeature.PRESET_MODE and self.entity.attributes.get(
fan.ATTR_PRESET_MODES
):
yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
)
@@ -672,7 +679,11 @@ class RemoteCapabilities(AlexaEntity):
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
if (
activities
and (supported & remote.RemoteEntityFeature.ACTIVITY)
and self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
):
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
@@ -692,7 +703,9 @@ class HumidifierCapabilities(AlexaEntity):
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & humidifier.HumidifierEntityFeature.MODES:
if (
supported & humidifier.HumidifierEntityFeature.MODES
) and self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES):
yield AlexaModeController(
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
)
@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonEchoApi
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -36,6 +36,8 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except WrongCountry:
errors["base"] = "wrong_country"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.22"]
"requirements": ["aioamazondevices==3.5.0"]
}
@@ -33,6 +33,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
@@ -61,8 +61,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
@@ -71,13 +69,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
@@ -1,17 +1,69 @@
"""Conversation support for Anthropic."""
from typing import Literal
from collections.abc import AsyncGenerator, Callable, Iterable
import json
from typing import Any, Literal, cast
import anthropic
from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, intent, llm
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AnthropicConfigEntry
from .const import CONF_PROMPT, DOMAIN
from .entity import AnthropicBaseLLMEntity
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
async def async_setup_entry(
@@ -30,10 +82,253 @@ async def async_setup_entry(
)
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=[tool_result_block],
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
tool_result_block,
]
else:
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
elif isinstance(content, conversation.UserContent):
# Combine consequent user messages
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=content.content,
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
TextBlockParam(type="text", text=content.content),
]
else:
messages[-1]["content"].append( # type: ignore[attr-defined]
TextBlockParam(type="text", text=content.content)
)
elif isinstance(content, conversation.AssistantContent):
# Combine consequent assistant messages
if not messages or messages[-1]["role"] != "assistant":
messages.append(
MessageParam(
role="assistant",
content=[],
)
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=tool_call.tool_args,
)
for tool_call in content.tool_calls
]
)
else:
# Note: We don't pass SystemContent here as its passed to the API as the prompt
raise TypeError(f"Unexpected content type: {type(content)}")
return messages
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
- RawMessageStartEvent with no content
- RawContentBlockStartEvent with an empty ThinkingBlock (if extended thinking is enabled)
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- ...
- RawContentBlockDeltaEvent with a SignatureDelta
- RawContentBlockStopEvent
- RawContentBlockStartEvent with a RedactedThinkingBlock (occasionally)
- RawContentBlockStopEvent (RedactedThinkingBlock does not have a delta)
- RawContentBlockStartEvent with an empty TextBlock
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- ...
- RawContentBlockStopEvent
- RawContentBlockStartEvent with ToolUseBlock specifying the function name
- RawContentBlockDeltaEvent with a InputJSONDelta
- RawContentBlockDeltaEvent with a InputJSONDelta
- ...
- RawContentBlockStopEvent
- RawMessageDeltaEvent with a stop_reason='tool_use'
- RawMessageStopEvent(type='message_stop')
Each message could contain multiple blocks of the same type.
"""
if result is None:
raise TypeError("Expected a stream of messages")
current_message: MessageParam | None = None
current_block: (
TextBlockParam
| ToolUseBlockParam
| ThinkingBlockParam
| RedactedThinkingBlockParam
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
current_block = TextBlockParam(
type="text", text=response.content_block.text
)
yield {"role": "assistant"}
if response.content_block.text:
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
current_block = ThinkingBlockParam(
type="thinking",
thinking=response.content_block.thinking,
signature=response.content_block.signature,
)
elif isinstance(response.content_block, RedactedThinkingBlock):
current_block = RedactedThinkingBlockParam(
type="redacted_thinking", data=response.content_block.data
)
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
"encrypted for safety reasons. This doesnt affect the quality of "
"responses"
)
elif isinstance(response, RawContentBlockDeltaEvent):
if current_block is None:
raise ValueError("Unexpected delta without a block")
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
text_block = cast(TextBlockParam, current_block)
text_block["text"] += response.delta.text
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["thinking"] += response.delta.thinking
elif isinstance(response.delta, SignatureDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["signature"] += response.delta.signature
elif isinstance(response, RawContentBlockStopEvent):
if current_block is None:
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
# tool block
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_block["input"] = tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_block["id"],
tool_name=current_block["name"],
tool_args=tool_args,
)
]
}
elif current_block["type"] == "thinking":
# thinking block
LOGGER.debug("Thinking: %s", current_block["thinking"])
if current_message is None:
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicConversationEntity(
conversation.ConversationEntity,
conversation.AbstractConversationAgent,
AnthropicBaseLLMEntity,
conversation.ConversationEntity, conversation.AbstractConversationAgent
):
"""Anthropic conversation agent."""
@@ -41,7 +336,17 @@ class AnthropicConversationEntity(
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the agent."""
super().__init__(entry, subentry)
self.entry = entry
self.subentry = subentry
self._attr_name = subentry.title
self._attr_unique_id = subentry.subentry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
if self.subentry.data.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
@@ -52,6 +357,13 @@ class AnthropicConversationEntity(
"""Return a list of supported languages."""
return MATCH_ALL
async def async_added_to_hass(self) -> None:
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
self.entry.async_on_unload(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@@ -82,3 +394,77 @@ class AnthropicConversationEntity(
conversation_id=chat_log.conversation_id,
continue_conversation=chat_log.continue_conversation,
)
async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
) -> None:
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[ToolParam] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
model_args = {
"model": model,
"messages": messages,
"tools": tools or NOT_GIVEN,
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
try:
stream = await client.messages.create(**model_args)
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
)
)
if not chat_log.unresponded_tool_results:
break
async def _async_entry_update_listener(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)
@@ -1,393 +0,0 @@
"""Base entity for Anthropic."""
from collections.abc import AsyncGenerator, Callable, Iterable
import json
from typing import Any, cast
import anthropic
from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigSubentry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=[tool_result_block],
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
tool_result_block,
]
else:
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
elif isinstance(content, conversation.UserContent):
# Combine consequent user messages
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=content.content,
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
TextBlockParam(type="text", text=content.content),
]
else:
messages[-1]["content"].append( # type: ignore[attr-defined]
TextBlockParam(type="text", text=content.content)
)
elif isinstance(content, conversation.AssistantContent):
# Combine consequent assistant messages
if not messages or messages[-1]["role"] != "assistant":
messages.append(
MessageParam(
role="assistant",
content=[],
)
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=tool_call.tool_args,
)
for tool_call in content.tool_calls
]
)
else:
# Note: We don't pass SystemContent here as its passed to the API as the prompt
raise TypeError(f"Unexpected content type: {type(content)}")
return messages
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
- RawMessageStartEvent with no content
- RawContentBlockStartEvent with an empty ThinkingBlock (if extended thinking is enabled)
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- ...
- RawContentBlockDeltaEvent with a SignatureDelta
- RawContentBlockStopEvent
- RawContentBlockStartEvent with a RedactedThinkingBlock (occasionally)
- RawContentBlockStopEvent (RedactedThinkingBlock does not have a delta)
- RawContentBlockStartEvent with an empty TextBlock
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- ...
- RawContentBlockStopEvent
- RawContentBlockStartEvent with ToolUseBlock specifying the function name
- RawContentBlockDeltaEvent with a InputJSONDelta
- RawContentBlockDeltaEvent with a InputJSONDelta
- ...
- RawContentBlockStopEvent
- RawMessageDeltaEvent with a stop_reason='tool_use'
- RawMessageStopEvent(type='message_stop')
Each message could contain multiple blocks of the same type.
"""
if result is None:
raise TypeError("Expected a stream of messages")
current_message: MessageParam | None = None
current_block: (
TextBlockParam
| ToolUseBlockParam
| ThinkingBlockParam
| RedactedThinkingBlockParam
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
current_block = TextBlockParam(
type="text", text=response.content_block.text
)
yield {"role": "assistant"}
if response.content_block.text:
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
current_block = ThinkingBlockParam(
type="thinking",
thinking=response.content_block.thinking,
signature=response.content_block.signature,
)
elif isinstance(response.content_block, RedactedThinkingBlock):
current_block = RedactedThinkingBlockParam(
type="redacted_thinking", data=response.content_block.data
)
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
"encrypted for safety reasons. This doesnt affect the quality of "
"responses"
)
elif isinstance(response, RawContentBlockDeltaEvent):
if current_block is None:
raise ValueError("Unexpected delta without a block")
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
text_block = cast(TextBlockParam, current_block)
text_block["text"] += response.delta.text
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["thinking"] += response.delta.thinking
elif isinstance(response.delta, SignatureDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["signature"] += response.delta.signature
elif isinstance(response, RawContentBlockStopEvent):
if current_block is None:
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
# tool block
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_block["input"] = tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_block["id"],
tool_name=current_block["name"],
tool_args=tool_args,
)
]
}
elif current_block["type"] == "thinking":
# thinking block
LOGGER.debug("Thinking: %s", current_block["thinking"])
if current_message is None:
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicBaseLLMEntity(Entity):
"""Anthropic base LLM entity."""
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
"""Initialize the entity."""
self.entry = entry
self.subentry = subentry
self._attr_name = subentry.title
self._attr_unique_id = subentry.subentry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
async def _async_handle_chat_log(
self,
chat_log: conversation.ChatLog,
) -> None:
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[ToolParam] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
model_args = {
"model": model,
"messages": messages,
"tools": tools or NOT_GIVEN,
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
try:
stream = await client.messages.create(**model_args)
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
)
)
if not chat_log.unresponded_tool_results:
break
+34 -18
View File
@@ -2,9 +2,9 @@
from homeassistant.config_entries import SOURCE_SYSTEM
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
@@ -37,6 +37,7 @@ from .manager import (
IdleEvent,
IncorrectPasswordError,
ManagerBackup,
ManagerStateEvent,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
@@ -44,7 +45,6 @@ from .manager import (
WrittenBackup,
)
from .models import AddonInfo, AgentBackup, BackupNotFound, Folder
from .services import async_setup_services
from .util import suggested_filename, suggested_filename_from_name_date
from .websocket import async_register_websocket_handlers
@@ -71,12 +71,12 @@ __all__ = [
"IncorrectPasswordError",
"LocalBackupAgent",
"ManagerBackup",
"ManagerStateEvent",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
"suggested_filename_from_name_date",
]
@@ -103,11 +103,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
backup_manager = BackupManager(hass, reader_writer)
hass.data[DATA_MANAGER] = backup_manager
await backup_manager.async_setup()
try:
await backup_manager.async_setup()
except Exception as err:
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
raise
else:
hass.data[DATA_BACKUP].manager_ready.set_result(None)
async_register_websocket_handlers(hass, with_hassio)
async_setup_services(hass)
async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
agent_id = list(backup_manager.local_backup_agents)[0]
await backup_manager.async_create_backup(
agent_ids=[agent_id],
include_addons=None,
include_all_addons=False,
include_database=True,
include_folders=None,
include_homeassistant=True,
name=None,
password=None,
)
async def async_handle_create_automatic_service(call: ServiceCall) -> None:
"""Service handler for creating automatic backups."""
await backup_manager.async_create_automatic_backup()
if not with_hassio:
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
hass.services.async_register(
DOMAIN, "create_automatic", async_handle_create_automatic_service
)
async_register_http_views(hass)
@@ -136,15 +164,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@callback
def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_MANAGER not in hass.data:
raise HomeAssistantError("Backup integration is not available")
return hass.data[DATA_MANAGER]
@@ -0,0 +1,38 @@
"""Websocket commands for the Backup integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import async_subscribe_events
from .const import DATA_MANAGER
from .manager import ManagerStateEvent
@callback
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
"""Register websocket commands."""
websocket_api.async_register_command(hass, handle_subscribe_events)
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
if DATA_MANAGER in hass.data:
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
connection.send_result(msg["id"])
@@ -8,6 +8,10 @@ from datetime import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import (
async_subscribe_events,
async_subscribe_platform_events,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER
@@ -52,8 +56,8 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
update_interval=None,
)
self.unsubscribe: list[Callable[[], None]] = [
backup_manager.async_subscribe_events(self._on_event),
backup_manager.async_subscribe_platform_events(self._on_event),
async_subscribe_events(hass, self._on_event),
async_subscribe_platform_events(hass, self._on_event),
]
self.backup_manager = backup_manager
+7 -30
View File
@@ -36,6 +36,7 @@ from homeassistant.helpers import (
issue_registry as ir,
start,
)
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
@@ -371,10 +372,12 @@ class BackupManager:
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_action_event: ManagerStateEvent | None = None
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
self._backup_platform_event_subscriptions: list[
Callable[[BackupPlatformEvent], None]
] = []
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
].backup_event_subscriptions
self._backup_platform_event_subscriptions = hass.data[
DATA_BACKUP
].backup_platform_event_subscriptions
async def async_setup(self) -> None:
"""Set up the backup manager."""
@@ -1382,32 +1385,6 @@ class BackupManager:
for subscription in self._backup_event_subscriptions:
subscription(event)
@callback
def async_subscribe_events(
self,
on_event: Callable[[ManagerStateEvent], None],
) -> Callable[[], None]:
"""Subscribe events."""
def remove_subscription() -> None:
self._backup_event_subscriptions.remove(on_event)
self._backup_event_subscriptions.append(on_event)
return remove_subscription
@callback
def async_subscribe_platform_events(
self,
on_event: Callable[[BackupPlatformEvent], None],
) -> Callable[[], None]:
"""Subscribe to backup platform events."""
def remove_subscription() -> None:
self._backup_platform_event_subscriptions.remove(on_event)
self._backup_platform_event_subscriptions.append(on_event)
return remove_subscription
def _create_automatic_backup_failed_issue(
self, translation_key: str, translation_placeholders: dict[str, str] | None
) -> None:
@@ -19,14 +19,9 @@ from homeassistant.components.onboarding import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from . import (
BackupManager,
Folder,
IncorrectPasswordError,
async_get_manager,
http as backup_http,
)
from . import BackupManager, Folder, IncorrectPasswordError, http as backup_http
if TYPE_CHECKING:
from homeassistant.components.onboarding import OnboardingStoreData
@@ -59,7 +54,7 @@ def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
if self._data["done"]:
raise HTTPUnauthorized
manager = async_get_manager(request.app[KEY_HASS])
manager = await async_get_backup_manager(request.app[KEY_HASS])
return await func(self, manager, request, *args, **kwargs)
return with_backup
@@ -1,36 +0,0 @@
"""The Backup integration."""
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.hassio import is_hassio
from .const import DATA_MANAGER, DOMAIN
async def _async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups."""
backup_manager = call.hass.data[DATA_MANAGER]
agent_id = list(backup_manager.local_backup_agents)[0]
await backup_manager.async_create_backup(
agent_ids=[agent_id],
include_addons=None,
include_all_addons=False,
include_database=True,
include_folders=None,
include_homeassistant=True,
name=None,
password=None,
)
async def _async_handle_create_automatic_service(call: ServiceCall) -> None:
"""Service handler for creating automatic backups."""
await call.hass.data[DATA_MANAGER].async_create_automatic_backup()
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services."""
if not is_hassio(hass):
hass.services.async_register(DOMAIN, "create", _async_handle_create_service)
hass.services.async_register(
DOMAIN, "create_automatic", _async_handle_create_automatic_service
)
+1 -25
View File
@@ -10,11 +10,7 @@ from homeassistant.helpers import config_validation as cv
from .config import Day, ScheduleRecurrence
from .const import DATA_MANAGER, LOGGER
from .manager import (
DecryptOnDowloadNotSupported,
IncorrectPasswordError,
ManagerStateEvent,
)
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
from .models import BackupNotFound, Folder
@@ -34,7 +30,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
websocket_api.async_register_command(hass, handle_delete)
websocket_api.async_register_command(hass, handle_restore)
websocket_api.async_register_command(hass, handle_subscribe_events)
websocket_api.async_register_command(hass, handle_config_info)
websocket_api.async_register_command(hass, handle_config_update)
@@ -422,22 +417,3 @@ def handle_config_update(
changes.pop("type")
manager.config.update(**changes)
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
connection.send_result(msg["id"])
@@ -19,7 +19,7 @@
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.1",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.43.0",
"habluetooth==3.49.0"
]
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.typing import ConfigType
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
from .services import async_setup_services
from .services import setup_services
from .types import BoschAlarmConfigEntry
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -29,7 +29,7 @@ PLATFORMS: list[Platform] = [
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up bosch alarm services."""
async_setup_services(hass)
setup_services(hass)
return True
@@ -9,7 +9,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
@@ -66,8 +66,7 @@ async def async_set_panel_date(call: ServiceCall) -> None:
) from err
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the bosch alarm integration."""
hass.services.async_register(
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.104.0"],
"requirements": ["hass-nabucasa==0.106.0"],
"single_config_entry": true
}
@@ -5,9 +5,8 @@ from pycoolmasternet_async import CoolMasterNet
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_SWING_SUPPORT, DOMAIN
from .const import CONF_SWING_SUPPORT
from .coordinator import CoolmasterConfigEntry, CoolmasterDataUpdateCoordinator
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
@@ -49,14 +48,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: CoolmasterConfigEntry) -> bool:
"""Unload a Coolmaster config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_config_entry_device(
hass: HomeAssistant,
config_entry: CoolmasterConfigEntry,
device_entry: dr.DeviceEntry,
) -> bool:
"""Remove a config entry from a device."""
return not device_entry.identifiers.intersection(
(DOMAIN, unit_id) for unit_id in config_entry.runtime_data.data
)
@@ -9,7 +9,7 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
@@ -35,7 +35,7 @@ class DevoloDeviceEntity(Entity):
) # This is not doing I/O. It fetches an internal state of the API
self._attr_should_poll = False
self._attr_unique_id = element_uid
self._attr_device_info = dr.DeviceInfo(
self._attr_device_info = DeviceInfo(
configuration_url=f"https://{urlparse(device_instance.href).netloc}",
identifiers={(DOMAIN, self._device_instance.uid)},
manufacturer=device_instance.brand,
@@ -88,16 +88,6 @@ class DevoloDeviceEntity(Entity):
elif len(message) == 3 and message[2] == "status":
# Maybe the API wants to tell us, that the device went on- or offline.
self._attr_available = self._device_instance.is_online()
elif message[1] == "del" and self.platform.config_entry:
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(
identifiers={(DOMAIN, self._device_instance.uid)}
)
if device:
device_registry.async_update_device(
device.id,
remove_config_entry_id=self.platform.config_entry.entry_id,
)
else:
_LOGGER.debug("No valid message received: %s", message)
@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push",
"loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"],
"requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
@@ -7,7 +7,7 @@
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.44.0"],
"requirements": ["async-upnp-client==0.45.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
@@ -12,7 +12,7 @@ from .bridge import DynaliteBridge
from .const import DOMAIN, LOGGER, PLATFORMS
from .convert_config import convert_config
from .panel import async_register_dynalite_frontend
from .services import async_setup_services
from .services import setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -21,7 +21,7 @@ type DynaliteConfigEntry = ConfigEntry[DynaliteBridge]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Dynalite platform."""
async_setup_services(hass)
setup_services(hass)
await async_register_dynalite_frontend(hass)
@@ -50,7 +50,7 @@ async def _request_channel_level(service_call: ServiceCall) -> None:
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def setup_services(hass: HomeAssistant) -> None:
"""Set up the Dynalite platform."""
hass.services.async_register(
DOMAIN,
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
}
@@ -16,12 +16,7 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
selector,
)
from homeassistant.helpers.selector import selector
from .const import (
CONF_MESSAGE,
@@ -31,9 +26,6 @@ from .const import (
FEED_ID,
FEED_NAME,
FEED_TAG,
SYNC_MODE,
SYNC_MODE_AUTO,
SYNC_MODE_MANUAL,
)
@@ -110,17 +102,6 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
"mode": "dropdown",
"multiple": True,
}
if user_input.get(SYNC_MODE) == SYNC_MODE_AUTO:
return self.async_create_entry(
title=sensor_name(self.url),
data={
CONF_URL: self.url,
CONF_API_KEY: self.api_key,
CONF_ONLY_INCLUDE_FEEDID: [
feed[FEED_ID] for feed in result[CONF_MESSAGE]
],
},
)
return await self.async_step_choose_feeds()
return self.async_show_form(
step_id="user",
@@ -129,15 +110,6 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_URL): str,
vol.Required(CONF_API_KEY): str,
vol.Required(
SYNC_MODE, default=SYNC_MODE_MANUAL
): SelectSelector(
SelectSelectorConfig(
options=[SYNC_MODE_MANUAL, SYNC_MODE_AUTO],
mode=SelectSelectorMode.DROPDOWN,
translation_key=SYNC_MODE,
)
),
}
),
user_input,
@@ -14,9 +14,6 @@ EMONCMS_UUID_DOC_URL = (
FEED_ID = "id"
FEED_NAME = "name"
FEED_TAG = "tag"
SYNC_MODE = "sync_mode"
SYNC_MODE_AUTO = "auto"
SYNC_MODE_MANUAL = "manual"
LOGGER = logging.getLogger(__package__)
+1 -10
View File
@@ -7,8 +7,7 @@
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"sync_mode": "Synchronization mode"
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"url": "Server URL starting with the protocol (http or https)",
@@ -25,14 +24,6 @@
"already_configured": "This server is already configured"
}
},
"selector": {
"sync_mode": {
"options": {
"auto": "Synchronize all available Feeds",
"manual": "Select which Feeds to synchronize"
}
}
},
"entity": {
"sensor": {
"energy": {
@@ -63,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) ->
coordinator = entry.runtime_data
coordinator.async_cancel_token_refresh()
coordinator.async_cancel_firmware_refresh()
coordinator.async_cancel_mac_verification()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.1.0"],
"requirements": ["pyenphase==2.2.2"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -363,7 +363,7 @@
"discharging": "[%key:common::state::discharging%]",
"idle": "[%key:common::state::idle%]",
"charging": "[%key:common::state::charging%]",
"full": "[%key:common::state::full%]"
"full": "Full"
}
},
"acb_available_energy": {
+1 -1
View File
@@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
_static_info: _InfoT
_state: _StateT
_has_state: bool
_has_state: bool = False
unique_id: str
def __init__(
+1 -1
View File
@@ -19,7 +19,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="chlorine",
translation_key="chlorine",
native_unit_of_measurement="mg/L",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
@@ -171,14 +171,19 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
for device in new_data.devices.values():
# create device registry entry for new main devices
if (
device.ain not in self.data.devices
and device.device_and_unit_id[1] is None
if device.ain not in self.data.devices and (
device.device_and_unit_id[1] is None
or (
# workaround for sub units without a main device, e.g. Energy 250
# https://github.com/home-assistant/core/issues/145204
device.device_and_unit_id[1] == "1"
and device.device_and_unit_id[0] not in new_data.devices
)
):
dr.async_get(self.hass).async_get_or_create(
config_entry_id=self.config_entry.entry_id,
name=device.name,
identifiers={(DOMAIN, device.ain)},
identifiers={(DOMAIN, device.device_and_unit_id[0])},
manufacturer=device.manufacturer,
model=device.productname,
sw_version=device.fw_version,
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250627.0"]
"requirements": ["home-assistant-frontend==20250702.3"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["dacite", "gios"],
"requirements": ["gios==6.0.0"]
"requirements": ["gios==6.1.2"]
}
@@ -306,6 +306,11 @@ class WebRTCProvider(CameraWebRTCProvider):
await self.teardown()
raise HomeAssistantError("Camera has no stream source")
if camera.platform.platform_name == "generic":
# This is a workaround to use ffmpeg for generic cameras
# A proper fix will be added in the future together with supporting multiple streams per camera
stream_source = "ffmpeg:" + stream_source
if not self.async_is_supported(stream_source):
await self.teardown()
raise HomeAssistantError("Stream source is not supported by go2rtc")
+1 -1
View File
@@ -127,7 +127,7 @@ class GoogleCloudSpeechToTextEntity(SpeechToTextEntity):
try:
responses = await self._client.streaming_recognize(
requests=request_generator(),
timeout=10,
timeout=30,
retry=AsyncRetry(initial=0.1, maximum=2.0, multiplier=2.0),
)
+1 -1
View File
@@ -218,7 +218,7 @@ class BaseGoogleCloudProvider:
response = await self._client.synthesize_speech(
request,
timeout=10,
timeout=30,
retry=AsyncRetry(initial=0.1, maximum=2.0, multiplier=2.0),
)
@@ -330,13 +330,14 @@ async def google_generative_ai_config_option_schema(
api_models = [api_model async for api_model in api_models_pager]
models = [
SelectOptionDict(
label=api_model.display_name,
label=api_model.name.lstrip("models/"),
value=api_model.name,
)
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
for api_model in sorted(
api_models, key=lambda x: x.name.lstrip("models/") or ""
)
if (
api_model.display_name
and api_model.name
api_model.name
and ("tts" in api_model.name) == (subentry_type == "tts")
and "vision" not in api_model.name
and api_model.supported_actions
@@ -27,7 +27,7 @@ from .const import (
SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED,
)
from .coordinator import GuardianDataUpdateCoordinator
from .services import async_setup_services
from .services import setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -55,7 +55,7 @@ class GuardianData:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Elexa Guardian component."""
async_setup_services(hass)
setup_services(hass)
return True
@@ -122,9 +122,8 @@ async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None:
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the guardian services."""
def setup_services(hass: HomeAssistant) -> None:
"""Register the Renault services."""
for service_name, schema, method in (
(
SERVICE_NAME_PAIR_SENSOR,
+2 -2
View File
@@ -48,13 +48,13 @@ from homeassistant.components.backup import (
RestoreBackupStage,
RestoreBackupState,
WrittenBackup,
async_get_manager as async_get_backup_manager,
suggested_filename as suggested_backup_filename,
suggested_filename_from_name_date,
)
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum
@@ -839,7 +839,7 @@ async def backup_addon_before_update(
async def backup_core_before_update(hass: HomeAssistant) -> None:
"""Prepare for updating core."""
backup_manager = async_get_backup_manager(hass)
backup_manager = await async_get_backup_manager(hass)
client = get_supervisor_client(hass)
try:
+6 -2
View File
@@ -11,6 +11,7 @@ from urllib.parse import quote
import aiohttp
from aiohttp import ClientTimeout, ClientWebSocketResponse, hdrs, web
from aiohttp.helpers import must_be_empty_body
from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest
from multidict import CIMultiDict
from yarl import URL
@@ -184,13 +185,16 @@ class HassIOIngress(HomeAssistantView):
content_type = "application/octet-stream"
# Simple request
if result.status in (204, 304) or (
if (empty_body := must_be_empty_body(result.method, result.status)) or (
content_length is not UNDEFINED
and (content_length_int := int(content_length))
<= MAX_SIMPLE_RESPONSE_SIZE
):
# Return Response
body = await result.read()
if empty_body:
body = None
else:
body = await result.read()
simple_response = web.Response(
headers=headers,
status=result.status,
@@ -1,3 +1 @@
"""The hddtemp component."""
DOMAIN = "hddtemp"
+1 -19
View File
@@ -22,14 +22,11 @@ from homeassistant.const import (
CONF_PORT,
UnitOfTemperature,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
@@ -59,21 +56,6 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the HDDTemp sensor."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "hddtemp",
},
)
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
+2 -2
View File
@@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from . import services
from .const import DOMAIN
from .coordinator import HeosConfigEntry, HeosCoordinator
from .services import async_setup_services
PLATFORMS = [Platform.MEDIA_PLAYER]
@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HEOS component."""
async_setup_services(hass)
services.register(hass)
return True
+2 -3
View File
@@ -9,7 +9,7 @@ import voluptuous as vol
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
@@ -44,8 +44,7 @@ HEOS_SIGN_IN_SCHEMA = vol.Schema(
HEOS_SIGN_OUT_SCHEMA = vol.Schema({})
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def register(hass: HomeAssistant) -> None:
"""Register HEOS services."""
hass.services.async_register(
DOMAIN,
@@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType
from .api import AsyncConfigEntryAuth
from .const import DOMAIN, OLD_NEW_UNIQUE_ID_SUFFIX_MAP
from .coordinator import HomeConnectConfigEntry, HomeConnectCoordinator
from .services import async_setup_services
from .services import register_actions
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ PLATFORMS = [
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Home Connect component."""
async_setup_services(hass)
register_actions(hass)
return True
@@ -41,7 +41,12 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN
from .const import (
API_DEFAULT_RETRY_AFTER,
APPLIANCES_WITH_PROGRAMS,
BSH_OPERATION_STATE_PAUSE,
DOMAIN,
)
from .utils import get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
@@ -66,6 +71,7 @@ class HomeConnectApplianceData:
def update(self, other: HomeConnectApplianceData) -> None:
"""Update data with data from other instance."""
self.commands.clear()
self.commands.update(other.commands)
self.events.update(other.events)
self.info.connected = other.info.connected
@@ -201,6 +207,28 @@ class HomeConnectCoordinator(
raw_key=status_key.value,
value=event.value,
)
if (
status_key == StatusKey.BSH_COMMON_OPERATION_STATE
and event.value == BSH_OPERATION_STATE_PAUSE
and CommandKey.BSH_COMMON_RESUME_PROGRAM
not in (
commands := self.data[
event_message_ha_id
].commands
)
):
# All the appliances that can be paused
# should have the resume command available.
commands.add(CommandKey.BSH_COMMON_RESUME_PROGRAM)
for (
listener,
context,
) in self._special_listeners.values():
if (
EventKey.BSH_COMMON_APPLIANCE_DEPAIRED
not in context
):
listener()
self._call_event_listener(event_message)
case EventType.NOTIFY:
@@ -627,10 +655,7 @@ class HomeConnectCoordinator(
"times": str(MAX_EXECUTIONS),
"time_window": str(MAX_EXECUTIONS_TIME_WINDOW // 60),
"home_connect_resource_url": "https://www.home-connect.com/global/help-support/error-codes#/Togglebox=15362315-13320636-1/",
"home_assistant_core_new_issue_url": (
"https://github.com/home-assistant/core/issues/new?template=bug_report.yml"
f"&integration_name={DOMAIN}&integration_link=https://www.home-assistant.io/integrations/{DOMAIN}/"
),
"home_assistant_core_issue_url": "https://github.com/home-assistant/core/issues/147299",
},
)
return True
@@ -18,7 +18,7 @@ from aiohomeconnect.model.error import HomeConnectError
import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -522,8 +522,7 @@ async def async_service_start_program(call: ServiceCall) -> None:
await _async_service_program(call, True)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def register_actions(hass: HomeAssistant) -> None:
"""Register custom actions."""
hass.services.async_register(
@@ -130,7 +130,7 @@
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::home_connect_too_many_connected_paired_events::title%]",
"description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please create an issue in the [Home Assistant core repository]({home_assistant_core_new_issue_url})."
"description": "The appliance \"{appliance_name}\" has been reported as connected or paired {times} times in less than {time_window} minutes, so refreshes on connected or paired events has been disabled to avoid exceeding the API rate limit.\n\nPlease refer to the [Home Connect Wi-Fi requirements and recommendations]({home_connect_resource_url}). If everything seems right with your network configuration, restart the appliance.\n\nClick \"submit\" to re-enable the updates.\nIf the issue persists, please see the following issue in the [Home Assistant core repository]({home_assistant_core_issue_url})."
}
}
}
@@ -113,9 +113,7 @@ class HomematicipHAP:
self._ws_close_requested = False
self._ws_connection_closed = asyncio.Event()
self._retry_task: asyncio.Task | None = None
self._tries = 0
self._accesspoint_connected = True
self._get_state_task: asyncio.Task | None = None
self.hmip_device_by_entity_id: dict[str, Any] = {}
self.reset_connection_listener: Callable | None = None
@@ -161,17 +159,8 @@ class HomematicipHAP:
"""
if not self.home.connected:
_LOGGER.error("HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self._ws_connection_closed.set()
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Now the HOME_CHANGED event has fired indicating the access
# point has reconnected to the cloud again.
# Explicitly getting an update as entity states might have
# changed during access point disconnect."""
job = self.hass.async_create_task(self.get_state())
job.add_done_callback(self.get_state_finished)
self._accesspoint_connected = True
@callback
def async_create_entity(self, *args, **kwargs) -> None:
@@ -185,20 +174,43 @@ class HomematicipHAP:
await asyncio.sleep(30)
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
async def _try_get_state(self) -> None:
"""Call get_state in a loop until no error occurs, using exponential backoff on error."""
# Wait until WebSocket connection is established.
while not self.home.websocket_is_connected():
await asyncio.sleep(2)
delay = 8
max_delay = 1500
while True:
try:
await self.get_state()
break
except HmipConnectionError as err:
_LOGGER.warning(
"Get_state failed, retrying in %s seconds: %s", delay, err
)
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay)
async def get_state(self) -> None:
"""Update HMIP state and tell Home Assistant."""
await self.home.get_current_state_async()
self.update_all()
def get_state_finished(self, future) -> None:
"""Execute when get_state coroutine has finished."""
"""Execute when try_get_state coroutine has finished."""
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error("Updating state after HMIP access point reconnect failed")
self.hass.async_create_task(self.home.disable_events())
except Exception as err: # noqa: BLE001
_LOGGER.error(
"Error updating state after HMIP access point reconnect: %s", err
)
else:
_LOGGER.info(
"Updating state after HMIP access point reconnect finished successfully",
)
def set_all_to_unavailable(self) -> None:
"""Set all devices to unavailable and tell Home Assistant."""
@@ -222,8 +234,8 @@ class HomematicipHAP:
async def async_reset(self) -> bool:
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_task is not None:
self._retry_task.cancel()
if self._get_state_task is not None:
self._get_state_task.cancel()
await self.home.disable_events_async()
_LOGGER.debug("Closed connection to HomematicIP cloud server")
await self.hass.config_entries.async_unload_platforms(
@@ -247,7 +259,9 @@ class HomematicipHAP:
"""Handle websocket connected."""
_LOGGER.info("Websocket connection to HomematicIP Cloud established")
if self._ws_connection_closed.is_set():
await self.get_state()
self._get_state_task = self.hass.async_create_task(self._try_get_state())
self._get_state_task.add_done_callback(self.get_state_finished)
self._ws_connection_closed.clear()
async def ws_disconnected_handler(self) -> None:
@@ -256,11 +270,12 @@ class HomematicipHAP:
self._ws_connection_closed.set()
async def ws_reconnected_handler(self, reason: str) -> None:
"""Handle websocket reconnection."""
"""Handle websocket reconnection. Is called when Websocket tries to reconnect."""
_LOGGER.info(
"Websocket connection to HomematicIP Cloud re-established due to reason: %s",
"Websocket connection to HomematicIP Cloud trying to reconnect due to reason: %s",
reason,
)
self._ws_connection_closed.set()
async def get_hap(
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.6"]
"requirements": ["homematicip==2.0.7"]
}
@@ -2,18 +2,15 @@
from datetime import datetime
import logging
from typing import TYPE_CHECKING
from aioautomower.model import make_name_string
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from . import AutomowerConfigEntry
from .const import DOMAIN
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
@@ -54,19 +51,6 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
self._attr_unique_id = mower_id
self._event: CalendarEvent | None = None
@property
def device_name(self) -> str:
"""Return the prefix for the event summary."""
device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, self.mower_id)}
)
if TYPE_CHECKING:
assert device_entry is not None
assert device_entry.name is not None
return device_entry.name_by_user or device_entry.name
@property
def event(self) -> CalendarEvent | None:
"""Return the current or next upcoming event."""
@@ -82,7 +66,7 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
program_event.work_area_id
]
return CalendarEvent(
summary=f"{self.device_name} {make_name_string(work_area_name, program_event.schedule_no)}",
summary=make_name_string(work_area_name, program_event.schedule_no),
start=program_event.start,
end=program_event.end,
rrule=program_event.rrule_str,
@@ -109,7 +93,7 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
]
calendar_events.append(
CalendarEvent(
summary=f"{self.device_name} {make_name_string(work_area_name, program_event.schedule_no)}",
summary=make_name_string(work_area_name, program_event.schedule_no),
start=program_event.start.replace(tzinfo=start_date.tzinfo),
end=program_event.end.replace(tzinfo=start_date.tzinfo),
rrule=program_event.rrule_str,
@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==1.0.1"]
"requirements": ["aioautomower==2025.6.0"]
}
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "silver",
"requirements": ["aioimmich==0.10.1"]
"requirements": ["aioimmich==0.10.2"]
}
+1 -1
View File
@@ -44,7 +44,7 @@ class ImmichUpdateEntity(ImmichEntity, UpdateEntity):
return self.coordinator.data.server_about.version
@property
def latest_version(self) -> str:
def latest_version(self) -> str | None:
"""Available new immich server version."""
assert self.coordinator.data.server_version_check
return self.coordinator.data.server_version_check.release_version
@@ -10,8 +10,4 @@ OHM = "Ω"
DISCOVERY_SVC_UUID = "9eae1000-9d0d-48c5-aa55-33e27f9bc533"
MAX_TEMP: int = 450
MAX_TEMP_F: int = 850
MIN_TEMP: int = 10
MIN_TEMP_F: int = 50
MIN_BOOST_TEMP: int = 250
MIN_BOOST_TEMP_F: int = 480
@@ -168,9 +168,7 @@ class IronOSSettingsCoordinator(IronOSBaseCoordinator[SettingsDataResponse]):
if self.device.is_connected and characteristics:
try:
return await self.device.get_settings(
list(characteristics | {CharSetting.TEMP_UNIT})
)
return await self.device.get_settings(list(characteristics))
except CommunicationError as e:
_LOGGER.debug("Failed to fetch settings", exc_info=e)
+78 -155
View File
@@ -6,9 +6,10 @@ from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from pynecil import CharSetting, LiveDataResponse, SettingsDataResponse, TempUnit
from pynecil import CharSetting, LiveDataResponse, SettingsDataResponse
from homeassistant.components.number import (
DEFAULT_MAX_VALUE,
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
@@ -23,17 +24,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.unit_conversion import TemperatureConverter
from . import IronOSConfigEntry
from .const import (
MAX_TEMP,
MAX_TEMP_F,
MIN_BOOST_TEMP,
MIN_BOOST_TEMP_F,
MIN_TEMP,
MIN_TEMP_F,
)
from .const import MAX_TEMP, MIN_TEMP
from .coordinator import IronOSCoordinators
from .entity import IronOSBaseEntity
@@ -45,10 +38,9 @@ class IronOSNumberEntityDescription(NumberEntityDescription):
"""Describes IronOS number entity."""
value_fn: Callable[[LiveDataResponse, SettingsDataResponse], float | int | None]
max_value_fn: Callable[[LiveDataResponse], float | int] | None = None
characteristic: CharSetting
raw_value_fn: Callable[[float], float | int] | None = None
native_max_value_f: float | None = None
native_min_value_f: float | None = None
class PinecilNumber(StrEnum):
@@ -82,6 +74,44 @@ def multiply(value: float | None, multiplier: float) -> float | None:
PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
IronOSNumberEntityDescription(
key=PinecilNumber.SETPOINT_TEMP,
translation_key=PinecilNumber.SETPOINT_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE,
value_fn=lambda data, _: data.setpoint_temp,
characteristic=CharSetting.SETPOINT_TEMP,
mode=NumberMode.BOX,
native_min_value=MIN_TEMP,
native_step=5,
max_value_fn=lambda data: min(data.max_tip_temp_ability or MAX_TEMP, MAX_TEMP),
),
IronOSNumberEntityDescription(
key=PinecilNumber.SLEEP_TEMP,
translation_key=PinecilNumber.SLEEP_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE,
value_fn=lambda _, settings: settings.get("sleep_temp"),
characteristic=CharSetting.SLEEP_TEMP,
mode=NumberMode.BOX,
native_min_value=MIN_TEMP,
native_max_value=MAX_TEMP,
native_step=10,
entity_category=EntityCategory.CONFIG,
),
IronOSNumberEntityDescription(
key=PinecilNumber.BOOST_TEMP,
translation_key=PinecilNumber.BOOST_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=NumberDeviceClass.TEMPERATURE,
value_fn=lambda _, settings: settings.get("boost_temp"),
characteristic=CharSetting.BOOST_TEMP,
mode=NumberMode.BOX,
native_min_value=0,
native_max_value=MAX_TEMP,
native_step=10,
entity_category=EntityCategory.CONFIG,
),
IronOSNumberEntityDescription(
key=PinecilNumber.QC_MAX_VOLTAGE,
translation_key=PinecilNumber.QC_MAX_VOLTAGE,
@@ -266,6 +296,32 @@ PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
IronOSNumberEntityDescription(
key=PinecilNumber.TEMP_INCREMENT_SHORT,
translation_key=PinecilNumber.TEMP_INCREMENT_SHORT,
value_fn=(lambda _, settings: settings.get("temp_increment_short")),
characteristic=CharSetting.TEMP_INCREMENT_SHORT,
raw_value_fn=lambda value: value,
mode=NumberMode.BOX,
native_min_value=1,
native_max_value=50,
native_step=1,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
IronOSNumberEntityDescription(
key=PinecilNumber.TEMP_INCREMENT_LONG,
translation_key=PinecilNumber.TEMP_INCREMENT_LONG,
value_fn=(lambda _, settings: settings.get("temp_increment_long")),
characteristic=CharSetting.TEMP_INCREMENT_LONG,
raw_value_fn=lambda value: value,
mode=NumberMode.BOX,
native_min_value=5,
native_max_value=90,
native_step=5,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
)
PINECIL_NUMBER_DESCRIPTIONS_V223: tuple[IronOSNumberEntityDescription, ...] = (
@@ -285,82 +341,6 @@ PINECIL_NUMBER_DESCRIPTIONS_V223: tuple[IronOSNumberEntityDescription, ...] = (
),
)
"""
The `device_class` attribute was removed from the `setpoint_temperature`, `sleep_temperature`, and `boost_temp` entities.
These entities represent user-defined input values, not measured temperatures, and their
interpretation depends on the device's current unit configuration. Applying a device_class
results in automatic unit conversions, which introduce rounding errors due to the use of integers.
This can prevent the correct value from being set, as the input is modified during synchronization with the device.
"""
PINECIL_TEMP_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
IronOSNumberEntityDescription(
key=PinecilNumber.SLEEP_TEMP,
translation_key=PinecilNumber.SLEEP_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda _, settings: settings.get("sleep_temp"),
characteristic=CharSetting.SLEEP_TEMP,
mode=NumberMode.BOX,
native_min_value=MIN_TEMP,
native_max_value=MAX_TEMP,
native_min_value_f=MIN_TEMP_F,
native_max_value_f=MAX_TEMP_F,
native_step=10,
entity_category=EntityCategory.CONFIG,
),
IronOSNumberEntityDescription(
key=PinecilNumber.BOOST_TEMP,
translation_key=PinecilNumber.BOOST_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda _, settings: settings.get("boost_temp"),
characteristic=CharSetting.BOOST_TEMP,
mode=NumberMode.BOX,
native_min_value=MIN_BOOST_TEMP,
native_min_value_f=MIN_BOOST_TEMP_F,
native_max_value=MAX_TEMP,
native_max_value_f=MAX_TEMP_F,
native_step=10,
entity_category=EntityCategory.CONFIG,
),
IronOSNumberEntityDescription(
key=PinecilNumber.TEMP_INCREMENT_SHORT,
translation_key=PinecilNumber.TEMP_INCREMENT_SHORT,
value_fn=(lambda _, settings: settings.get("temp_increment_short")),
characteristic=CharSetting.TEMP_INCREMENT_SHORT,
raw_value_fn=lambda value: value,
mode=NumberMode.BOX,
native_min_value=1,
native_max_value=50,
native_step=1,
entity_category=EntityCategory.CONFIG,
),
IronOSNumberEntityDescription(
key=PinecilNumber.TEMP_INCREMENT_LONG,
translation_key=PinecilNumber.TEMP_INCREMENT_LONG,
value_fn=(lambda _, settings: settings.get("temp_increment_long")),
characteristic=CharSetting.TEMP_INCREMENT_LONG,
raw_value_fn=lambda value: value,
mode=NumberMode.BOX,
native_min_value=5,
native_max_value=90,
native_step=5,
entity_category=EntityCategory.CONFIG,
),
)
PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
key=PinecilNumber.SETPOINT_TEMP,
translation_key=PinecilNumber.SETPOINT_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data, _: data.setpoint_temp,
characteristic=CharSetting.SETPOINT_TEMP,
mode=NumberMode.BOX,
native_min_value=MIN_TEMP,
native_max_value=MAX_TEMP,
native_min_value_f=MIN_TEMP_F,
native_max_value_f=MAX_TEMP_F,
native_step=5,
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -374,18 +354,9 @@ async def async_setup_entry(
if coordinators.live_data.v223_features:
descriptions += PINECIL_NUMBER_DESCRIPTIONS_V223
entities = [
async_add_entities(
IronOSNumberEntity(coordinators, description) for description in descriptions
]
entities.extend(
IronOSTemperatureNumberEntity(coordinators, description)
for description in PINECIL_TEMP_NUMBER_DESCRIPTIONS
)
entities.append(
IronOSSetpointNumberEntity(coordinators, PINECIL_SETPOINT_NUMBER_DESCRIPTION)
)
async_add_entities(entities)
class IronOSNumberEntity(IronOSBaseEntity, NumberEntity):
@@ -417,6 +388,15 @@ class IronOSNumberEntity(IronOSBaseEntity, NumberEntity):
self.coordinator.data, self.settings.data
)
@property
def native_max_value(self) -> float:
"""Return sensor state."""
if self.entity_description.max_value_fn is not None:
return self.entity_description.max_value_fn(self.coordinator.data)
return self.entity_description.native_max_value or DEFAULT_MAX_VALUE
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
@@ -427,60 +407,3 @@ class IronOSNumberEntity(IronOSBaseEntity, NumberEntity):
)
)
await self.settings.async_request_refresh()
class IronOSTemperatureNumberEntity(IronOSNumberEntity):
"""Implementation of a IronOS temperature number entity."""
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of the sensor, if any."""
return (
UnitOfTemperature.FAHRENHEIT
if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT
else UnitOfTemperature.CELSIUS
)
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
return (
self.entity_description.native_min_value_f
if self.entity_description.native_min_value_f
and self.native_unit_of_measurement is UnitOfTemperature.FAHRENHEIT
else super().native_min_value
)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
return (
self.entity_description.native_max_value_f
if self.entity_description.native_max_value_f
and self.native_unit_of_measurement is UnitOfTemperature.FAHRENHEIT
else super().native_max_value
)
class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity):
"""IronOS setpoint temperature entity."""
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
return (
min(
TemperatureConverter.convert(
float(max_tip_c),
UnitOfTemperature.CELSIUS,
self.native_unit_of_measurement,
),
super().native_max_value,
)
if (max_tip_c := self.coordinator.data.max_tip_temp_ability) is not None
else super().native_max_value
)
+2 -2
View File
@@ -91,7 +91,7 @@ from .schema import (
TimeSchema,
WeatherSchema,
)
from .services import async_setup_services
from .services import register_knx_services
from .storage.config_store import STORAGE_KEY as CONFIG_STORAGE_KEY, KNXConfigStore
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams
from .websocket import register_panel
@@ -138,7 +138,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if (conf := config.get(DOMAIN)) is not None:
hass.data[_KNX_YAML_CONFIG] = dict(conf)
async_setup_services(hass)
register_knx_services(hass)
return True
+1 -1
View File
@@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def register_knx_services(hass: HomeAssistant) -> None:
"""Register KNX integration services."""
hass.services.async_register(
DOMAIN,
@@ -14,7 +14,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_SERVICE_CODE
from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -30,7 +29,6 @@ class PlenticoreSwitchEntityDescription(SwitchEntityDescription):
on_label: str
off_value: str
off_label: str
installer_required: bool = False
SWITCH_SETTINGS_DATA = [
@@ -44,17 +42,6 @@ SWITCH_SETTINGS_DATA = [
off_value="2",
off_label="Automatic economical",
),
PlenticoreSwitchEntityDescription(
module_id="devices:local",
key="Battery:ManualCharge",
name="Battery Manual Charge",
is_on="1",
on_value="1",
on_label="On",
off_value="0",
off_label="Off",
installer_required=True,
),
]
@@ -86,13 +73,7 @@ async def async_setup_entry(
description.key,
)
continue
if entry.data.get(CONF_SERVICE_CODE) is None and description.installer_required:
_LOGGER.debug(
"Skipping installer required setting data %s/%s",
description.module_id,
description.key,
)
continue
entities.append(
PlenticoreDataSwitch(
settings_data_update_coordinator,
@@ -23,7 +23,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import (
@@ -57,11 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
assert entry.unique_id
serial = entry.unique_id
client = async_get_clientsession(hass)
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
client=client,
client=async_create_clientsession(hass),
)
try:
@@ -66,7 +66,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
WidgetType.CM_BACK_FLUSH, BackFlush(status=BackFlushStatus.OFF)
),
).status
is BackFlushStatus.REQUESTED
in (BackFlushStatus.REQUESTED, BackFlushStatus.CLEANING)
),
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda coordinator: (
@@ -33,7 +33,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
}
self._client = async_get_clientsession(self.hass)
self._client = async_create_clientsession(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.9"]
"requirements": ["pylamarzocco==2.0.11"]
}
+1 -1
View File
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["pypck"],
"quality_scale": "bronze",
"requirements": ["pypck==0.8.10", "lcn-frontend==0.2.5"]
"requirements": ["pypck==0.8.9", "lcn-frontend==0.2.5"]
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.28.1", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
}
@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.28.1", "led-ble==1.1.7"]
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
}
@@ -780,10 +780,10 @@
"battery_level": {
"name": "Battery",
"state": {
"high": "[%key:common::state::full%]",
"high": "Full",
"mid": "[%key:common::state::medium%]",
"low": "[%key:common::state::low%]",
"warning": "[%key:common::state::empty%]"
"warning": "Empty"
}
},
"relative_to_start": {
@@ -70,7 +70,7 @@
"motor_fault_short": "Motor shorted",
"motor_ot_amps": "Motor overtorqued",
"motor_disconnected": "Motor disconnected",
"empty": "[%key:common::state::empty%]"
"empty": "Empty"
}
},
"last_seen": {
+2 -2
View File
@@ -45,7 +45,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import JsonObjectType, load_json_object
from .const import ATTR_FORMAT, ATTR_IMAGES, CONF_ROOMS_REGEX, DOMAIN, FORMAT_HTML
from .services import async_setup_services
from .services import register_services
_LOGGER = logging.getLogger(__name__)
@@ -128,7 +128,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
config[CONF_COMMANDS],
)
async_setup_services(hass)
register_services(hass)
return True
+2 -3
View File
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .const import (
@@ -50,8 +50,7 @@ async def _handle_send_message(call: ServiceCall) -> None:
await matrix_bot.handle_send_message(call)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
def register_services(hass: HomeAssistant) -> None:
"""Set up the Matrix bot component."""
hass.services.async_register(
-79
View File
@@ -2,12 +2,9 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, cast
from chip.clusters import Objects as clusters
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
from matter_server.common import custom_clusters
from homeassistant.components.number import (
@@ -47,23 +44,6 @@ class MatterNumberEntityDescription(NumberEntityDescription, MatterEntityDescrip
"""Describe Matter Number Input entities."""
@dataclass(frozen=True, kw_only=True)
class MatterRangeNumberEntityDescription(
NumberEntityDescription, MatterEntityDescription
):
"""Describe Matter Number Input entities with min and max values."""
ha_to_native_value: Callable[[Any], Any]
# attribute descriptors to get the min and max value
min_attribute: type[ClusterAttributeDescriptor]
max_attribute: type[ClusterAttributeDescriptor]
# command: a custom callback to create the command to send to the device
# the callback's argument will be the index of the selected list value
command: Callable[[int], ClusterCommand]
class MatterNumber(MatterEntity, NumberEntity):
"""Representation of a Matter Attribute as a Number entity."""
@@ -87,42 +67,6 @@ class MatterNumber(MatterEntity, NumberEntity):
self._attr_native_value = value
class MatterRangeNumber(MatterEntity, NumberEntity):
"""Representation of a Matter Attribute as a Number entity with min and max values."""
entity_description: MatterRangeNumberEntityDescription
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
send_value = self.entity_description.ha_to_native_value(value)
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.command(send_value),
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
self._attr_native_value = value
self._attr_native_min_value = (
cast(
int,
self.get_matter_attribute_value(self.entity_description.min_attribute),
)
/ 100
)
self._attr_native_max_value = (
cast(
int,
self.get_matter_attribute_value(self.entity_description.max_attribute),
)
/ 100
)
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
@@ -269,27 +213,4 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterNumber,
required_attributes=(clusters.DoorLock.Attributes.AutoRelockTime,),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
entity_description=MatterRangeNumberEntityDescription(
key="TemperatureControlTemperatureSetpoint",
name=None,
translation_key="temperature_setpoint",
command=lambda value: clusters.TemperatureControl.Commands.SetTemperature(
targetTemperature=value
),
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
measurement_to_ha=lambda x: None if x is None else x / 100,
ha_to_native_value=lambda x: round(x * 100),
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
mode=NumberMode.SLIDER,
),
entity_class=MatterRangeNumber,
required_attributes=(
clusters.TemperatureControl.Attributes.TemperatureSetpoint,
clusters.TemperatureControl.Attributes.MinTemperature,
clusters.TemperatureControl.Attributes.MaxTemperature,
),
),
]
@@ -183,9 +183,6 @@
"temperature_offset": {
"name": "Temperature offset"
},
"temperature_setpoint": {
"name": "Temperature setpoint"
},
"pir_occupied_to_unoccupied_delay": {
"name": "Occupied to unoccupied delay"
},
+17 -30
View File
@@ -17,7 +17,6 @@ from homeassistant.components.vacuum import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import MatterEntity
@@ -68,31 +67,20 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
entity_description: StateVacuumEntityDescription
_platform_translation_key = "vacuum"
def _get_run_mode_by_tag(
self, tag: ModeTag
) -> clusters.RvcRunMode.Structs.ModeOptionStruct | None:
"""Get the run mode by tag."""
supported_run_modes = self._supported_run_modes or {}
for mode in supported_run_modes.values():
for t in mode.modeTags:
if t.value == tag.value:
return mode
return None
async def async_stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner."""
# We simply set the RvcRunMode to the first runmode
# that has the idle tag to stop the vacuum cleaner.
# this is compatible with both Matter 1.2 and 1.3+ devices.
mode = self._get_run_mode_by_tag(ModeTag.IDLE)
if mode is None:
raise HomeAssistantError(
"No supported run mode found to stop the vacuum cleaner."
)
await self.send_device_command(
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
)
supported_run_modes = self._supported_run_modes or {}
for mode in supported_run_modes.values():
for tag in mode.modeTags:
if tag.value == ModeTag.IDLE:
# stop the vacuum by changing the run mode to idle
await self.send_device_command(
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
)
return
async def async_return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock."""
@@ -122,15 +110,14 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
# We simply set the RvcRunMode to the first runmode
# that has the cleaning tag to start the vacuum cleaner.
# this is compatible with both Matter 1.2 and 1.3+ devices.
mode = self._get_run_mode_by_tag(ModeTag.CLEANING)
if mode is None:
raise HomeAssistantError(
"No supported run mode found to start the vacuum cleaner."
)
await self.send_device_command(
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
)
supported_run_modes = self._supported_run_modes or {}
for mode in supported_run_modes.values():
for tag in mode.modeTags:
if tag.value == ModeTag.CLEANING:
await self.send_device_command(
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
)
return
async def async_pause(self) -> None:
"""Pause the cleaning task."""
@@ -6,6 +6,8 @@ import time
from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_HUMIDITY,
ATTR_FORECAST_NATIVE_PRECIPITATION,
@@ -49,9 +51,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def format_condition(condition: str):
def format_condition(condition: str, force_day: bool = False) -> str:
"""Return condition from dict CONDITION_MAP."""
return CONDITION_MAP.get(condition, condition)
mapped_condition = CONDITION_MAP.get(condition, condition)
if force_day and mapped_condition == ATTR_CONDITION_CLEAR_NIGHT:
# Meteo-France can return clear night condition instead of sunny for daily weather, so we map it to sunny
return ATTR_CONDITION_SUNNY
return mapped_condition
async def async_setup_entry(
@@ -212,7 +218,7 @@ class MeteoFranceWeather(
forecast["dt"]
).isoformat(),
ATTR_FORECAST_CONDITION: format_condition(
forecast["weather12H"]["desc"]
forecast["weather12H"]["desc"], force_day=True
),
ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"],
ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"],
+8 -7
View File
@@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
EntityCategory,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -59,6 +60,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
native_attr_name="name",
name="Station name",
icon="mdi:label-outline",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
MetOfficeSensorEntityDescription(
@@ -235,14 +237,13 @@ class MetOfficeCurrentSensor(
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
value = get_attribute(
self.coordinator.data.now(), self.entity_description.native_attr_name
)
native_attr = self.entity_description.native_attr_name
if (
self.entity_description.native_attr_name == "significantWeatherCode"
and value is not None
):
if native_attr == "name":
return str(self.coordinator.data.name)
value = get_attribute(self.coordinator.data.now(), native_attr)
if native_attr == "significantWeatherCode" and value is not None:
value = CONDITION_MAP.get(value)
return value
@@ -26,14 +26,6 @@ class OAuth2FlowHandler(
"""Return logger."""
return logging.getLogger(__name__)
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
# "vg" is mandatory but the value doesn't seem to matter
return {
"vg": "sv-SE",
}
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
+2 -2
View File
@@ -172,7 +172,7 @@ async def async_modbus_setup(
async def async_write_register(service: ServiceCall) -> None:
"""Write Modbus registers."""
slave = 1
slave = 0
if ATTR_UNIT in service.data:
slave = int(float(service.data[ATTR_UNIT]))
@@ -195,7 +195,7 @@ async def async_modbus_setup(
async def async_write_coil(service: ServiceCall) -> None:
"""Write Modbus coil."""
slave = 1
slave = 0
if ATTR_UNIT in service.data:
slave = int(float(service.data[ATTR_UNIT]))
if ATTR_SLAVE in service.data:
@@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push",
"loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.28"]
"requirements": ["motionblinds==0.6.29"]
}
@@ -2114,6 +2114,9 @@ def data_schema_from_fields(
if schema_section is None:
data_schema.update(data_schema_element)
continue
if not data_schema_element:
# Do not show empty sections
continue
collapsed = (
not any(
(default := data_schema_fields[str(option)].default) is vol.UNDEFINED
+23 -12
View File
@@ -389,16 +389,6 @@ def async_setup_entity_entry_helper(
_async_setup_entities()
def init_entity_id_from_config(
hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str
) -> None:
"""Set entity_id from object_id if defined in config."""
if CONF_OBJECT_ID in config:
entity.entity_id = async_generate_entity_id(
entity_id_format, config[CONF_OBJECT_ID], None, hass
)
class MqttAttributesMixin(Entity):
"""Mixin used for platforms that support JSON attributes."""
@@ -1312,6 +1302,7 @@ class MqttEntity(
_attr_should_poll = False
_default_name: str | None
_entity_id_format: str
_update_registry_entity_id: str | None = None
def __init__(
self,
@@ -1346,13 +1337,33 @@ class MqttEntity(
def _init_entity_id(self) -> None:
"""Set entity_id from object_id if defined in config."""
init_entity_id_from_config(
self.hass, self, self._config, self._entity_id_format
if CONF_OBJECT_ID not in self._config:
return
self.entity_id = async_generate_entity_id(
self._entity_id_format, self._config[CONF_OBJECT_ID], None, self.hass
)
if self.unique_id is None:
return
# Check for previous deleted entities
entity_registry = er.async_get(self.hass)
entity_platform = self._entity_id_format.split(".")[0]
if (
deleted_entry := entity_registry.deleted_entities.get(
(entity_platform, DOMAIN, self.unique_id)
)
) and deleted_entry.entity_id != self.entity_id:
# Plan to update the entity_id basis on `object_id` if a deleted entity was found
self._update_registry_entity_id = self.entity_id
@final
async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""
if self._update_registry_entity_id is not None:
entity_registry = er.async_get(self.hass)
entity_registry.async_update_entity(
self.entity_id, new_entity_id=self._update_registry_entity_id
)
await super().async_added_to_hass()
self._subscriptions = {}
self._prepare_subscribe_topics()
+6
View File
@@ -98,6 +98,12 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"together with state class `{state_class}`"
)
unit_of_measurement: str | None
if (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is not None and not unit_of_measurement.strip():
config.pop(CONF_UNIT_OF_MEASUREMENT)
# Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values
if (options := config.get(CONF_OPTIONS)) is not None:
@@ -34,7 +34,7 @@ class MusicAssistantEntity(Entity):
identifiers={(DOMAIN, player_id)},
manufacturer=self.player.device_info.manufacturer or provider.name,
model=self.player.device_info.model or self.player.name,
name=self.player.display_name,
name=self.player.name,
configuration_url=f"{mass.server_url}/#/settings/editplayer/{player_id}",
)
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
"iot_class": "local_push",
"loggers": ["music_assistant"],
"requirements": ["music-assistant-client==1.2.0"],
"requirements": ["music-assistant-client==1.2.4"],
"zeroconf": ["_mass._tcp.local."]
}
@@ -6,11 +6,7 @@ import logging
from typing import TYPE_CHECKING, Any, cast
from music_assistant_models.enums import MediaType as MASSMediaType
from music_assistant_models.media_items import (
BrowseFolder,
MediaItemType,
SearchResults,
)
from music_assistant_models.media_items import MediaItemType, SearchResults
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -549,8 +545,6 @@ def _process_search_results(
# Add available items to results
for item in items:
if TYPE_CHECKING:
assert not isinstance(item, BrowseFolder)
if not item.available:
continue
@@ -248,10 +248,8 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
player = self.player
active_queue = self.active_queue
# update generic attributes
if player.powered and active_queue is not None:
self._attr_state = MediaPlayerState(active_queue.state.value)
if player.powered and player.state is not None:
self._attr_state = MediaPlayerState(player.state.value)
if player.powered and player.playback_state is not None:
self._attr_state = MediaPlayerState(player.playback_state.value)
else:
self._attr_state = MediaPlayerState(STATE_OFF)
# active source and source list (translate to HA source names)
@@ -270,12 +268,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self._attr_source = active_source_name
group_members: list[str] = []
if player.group_childs:
group_members = player.group_childs
if player.group_members:
group_members = player.group_members
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
group_members = parent.group_childs
group_members = parent.group_members
# translate MA group_childs to HA group_members as entity id's
# translate MA group_members to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids: list[str] = [
entity_id
-9
View File
@@ -44,15 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
try:
await nam.async_check_credentials()
except (ApiError, ClientError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
except AuthFailedError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
+22 -43
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any
@@ -26,15 +25,6 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@dataclass
class NamConfig:
"""NAM device configuration class."""
mac_address: str
auth_enabled: bool
_LOGGER = logging.getLogger(__name__)
AUTH_SCHEMA = vol.Schema(
@@ -42,29 +32,14 @@ AUTH_SCHEMA = vol.Schema(
)
async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig:
"""Get device MAC address and auth_enabled property."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host)
nam = await NettigoAirMonitor.create(websession, options)
mac = await nam.async_get_mac_address()
return NamConfig(mac, nam.auth_enabled)
async def async_check_credentials(
async def async_get_nam(
hass: HomeAssistant, host: str, data: dict[str, Any]
) -> None:
"""Check if credentials are valid."""
) -> NettigoAirMonitor:
"""Get NAM client."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD))
nam = await NettigoAirMonitor.create(websession, options)
await nam.async_check_credentials()
return await NettigoAirMonitor.create(websession, options)
class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -72,8 +47,8 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
_config: NamConfig
host: str
auth_enabled: bool = False
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -85,21 +60,20 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
self.host = user_input[CONF_HOST]
try:
config = await async_get_config(self.hass, self.host)
nam = await async_get_nam(self.hass, self.host, {})
except (ApiError, ClientConnectorError, TimeoutError):
errors["base"] = "cannot_connect"
except CannotGetMacError:
return self.async_abort(reason="device_unsupported")
except AuthFailedError:
return await self.async_step_credentials()
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(config.mac_address))
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
if config.auth_enabled is True:
return await self.async_step_credentials()
return self.async_create_entry(
title=self.host,
data=user_input,
@@ -119,7 +93,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await async_check_credentials(self.hass, self.host, user_input)
nam = await async_get_nam(self.hass, self.host, user_input)
except AuthFailedError:
errors["base"] = "invalid_auth"
except (ApiError, ClientConnectorError, TimeoutError):
@@ -128,6 +102,9 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
return self.async_create_entry(
title=self.host,
data={**user_input, CONF_HOST: self.host},
@@ -148,14 +125,16 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: self.host})
try:
self._config = await async_get_config(self.hass, self.host)
nam = await async_get_nam(self.hass, self.host, {})
except (ApiError, ClientConnectorError, TimeoutError):
return self.async_abort(reason="cannot_connect")
except CannotGetMacError:
return self.async_abort(reason="device_unsupported")
except AuthFailedError:
self.auth_enabled = True
return await self.async_step_confirm_discovery()
await self.async_set_unique_id(format_mac(self._config.mac_address))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
await self.async_set_unique_id(format_mac(nam.mac))
return await self.async_step_confirm_discovery()
@@ -171,7 +150,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
data={CONF_HOST: self.host},
)
if self._config.auth_enabled is True:
if self.auth_enabled is True:
return await self.async_step_credentials()
self._set_confirm_only()
@@ -198,7 +177,7 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await async_check_credentials(self.hass, self.host, user_input)
await async_get_nam(self.hass, self.host, user_input)
except (
ApiError,
AuthFailedError,
@@ -228,11 +207,11 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
config = await async_get_config(self.hass, user_input[CONF_HOST])
nam = await async_get_nam(self.hass, user_input[CONF_HOST], {})
except (ApiError, ClientConnectorError, TimeoutError):
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(format_mac(config.mac_address))
await self.async_set_unique_id(format_mac(nam.mac))
self._abort_if_unique_id_mismatch(reason="another_device")
return self.async_update_reload_and_abort(
+1 -1
View File
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["nettigo_air_monitor"],
"requirements": ["nettigo-air-monitor==4.1.0"],
"requirements": ["nettigo-air-monitor==5.0.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

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