Compare commits

...

637 Commits

Author SHA1 Message Date
epenet 5200d66299 Adjust 2025-12-11 12:02:41 +00:00
epenet f70bc20582 Adjust 2025-12-11 11:59:58 +00:00
epenet 14e5e416ed Simplify 2025-12-11 11:02:08 +00:00
epenet 3004b49a4f Simplify 2025-12-11 10:59:40 +00:00
epenet b1e410a211 Drop usused scale_value_back 2025-12-11 10:28:33 +00:00
epenet 8d79b9aa8b Tweak 2025-12-11 10:26:40 +00:00
epenet aa7e113aa5 Improve Tuya action validation 2025-12-11 10:09:56 +00:00
epenet 7fe0d96c88 Remove unnecessary wrapper base method in Tuya (#158708) 2025-12-11 10:13:24 +01:00
Joost Lekkerkerker cdc2192bba Clean up Homelink tests (#158685)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 09:10:02 +01:00
Matt Zimmerman 74b1c1f6fd Bump python-smarttub to 0.0.46 (#158702) 2025-12-11 09:07:46 +01:00
J. Nick Koston 69c7a7b0ab Pin pycares to 4.11.0 (#158695) 2025-12-10 22:00:59 -05:00
Norbert Rittel ef302215cc Add two common config flow strings in energyid (#158680) 2025-12-10 21:23:18 +01:00
Norbert Rittel 6378f5f02a Use common reauth_successful string in rituals_perfume_genie (#158684) 2025-12-10 21:14:25 +01:00
Bouwe Westerdijk 79245195cd Bump Plugwise to v1.11.0 (#158067) 2025-12-10 21:03:27 +01:00
epenet d0e33a6e04 Use process_raw_value in Tuya JsonTypeInformation (#158517) 2025-12-10 20:13:16 +01:00
Joost Lekkerkerker f55fc788db Cleanup homelink config flow (#158479) 2025-12-10 18:39:44 +01:00
epenet 6152e0fa27 Split action and state wrapper in Tuya alarm control panel (#158532) 2025-12-10 18:37:04 +01:00
Matthias Alphart f1a89741c0 Add counter for KNX DataSecure undecodable telegrams (#157844) 2025-12-10 17:59:00 +01:00
Paul Tarjan 7629c9f280 Fix flaky test_stream_source timeout in generic camera tests (#158506) 2025-12-10 17:49:27 +01:00
Abílio Costa 6b8650c6d9 Remove uneeded check in whirlpool oven temperature sensors (#157997) 2025-12-10 17:45:34 +01:00
Paul Tarjan 48f186368a Fix flaky playstation_network test_image_platform test (#158296) 2025-12-10 17:07:58 +01:00
Allen Porter d65baac8d4 Bump python-roborock to 3.12.2 (#158572) 2025-12-10 17:05:57 +01:00
Paul Tarjan d57801407b Fix flaky test_calls_not_allowed by using thread-safe event signaling (#158504) 2025-12-10 16:38:34 +01:00
Markus Jacobsen 4495a76557 Change Bang & Olufsen "stopped" state translation (#158534) 2025-12-10 16:37:48 +01:00
Paul Tarjan 99dfb93ac0 Fix flaky test_rename_entity_collision test (#158297) 2025-12-10 16:36:52 +01:00
Abílio Costa 7c7c0aad25 Rename trigger helper state checkers (#158537)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 14:56:56 +00:00
Andrew Jackson 5992898340 Add measurement state class to ohme sensors (#158541) 2025-12-10 15:35:24 +01:00
Sander Blomvågnes 4f2ff9a4f4 Add codeowner and integration_type to Entur manifest (#158543) 2025-12-10 15:26:24 +01:00
bestycame a8a135c2ca Bump hanna-cloud to version 0.0.7 (#158536)
Co-authored-by: Olivier d'Otreppe <odotreppe@abbove.com>
2025-12-10 14:50:05 +01:00
epenet 43e241ee39 Use process_raw_value in Tuya RawTypeInformation (#158521) 2025-12-10 10:44:49 +01:00
Kinachi249 6af7052b9d Bump PyCync to 0.5.0 (#158509) 2025-12-10 08:15:47 +01:00
dependabot[bot] c0aa35ff6d Bump codecov/codecov-action from 5.5.1 to 5.5.2 (#158515) 2025-12-10 07:27:42 +01:00
Ludovic BOUÉ 2c7763e350 Fix Matter epoch timestamp sensors (#157600) 2025-12-10 07:13:21 +01:00
Joost Lekkerkerker 95e344ea44 Cleanup homelink (#158477) 2025-12-10 01:09:23 +01:00
Yevhenii Vaskivskyi 7ed8613411 Bump asusrouter to 1.21.3 (#158492) 2025-12-09 22:56:11 +01:00
Anton Dalgren 4ac0567ccc Add AirPatrol integration (#149247)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-09 21:26:21 +01:00
epenet bc031e7a81 Improve Tuya HVACMode handling (#158042)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 21:17:57 +01:00
Raphael Hehl ad1ba629c5 Fix typos in UniFi Protect integration (#158478) 2025-12-09 21:13:38 +01:00
Paul Tarjan 0c2cb460cb Fix flaky laundrify coordinator test (#158460) 2025-12-09 21:12:55 +01:00
Kira 5388740c83 Bump blinkpy to 0.25.1 (#158135)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-09 20:53:59 +01:00
Raphael Hehl 2a54d4c3a9 Add model_id to NVR device info in UniFi Protect (#158481)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-09 20:18:04 +01:00
Steve Easley 2008972215 Bump kaleidescape requirement version to v1.0.2 (#158068) 2025-12-09 20:12:55 +01:00
Jordan Harvey 39004bd0a2 Update pynintendoparental to 2.1.0 (#158487) 2025-12-09 20:11:28 +01:00
Michael Hansen bb847ce3ff Bump pysilero-vad to 3.0.1 (#158486) 2025-12-09 13:08:34 -06:00
Denis Shulyaka 05920a9c73 Bump openai to 2.9.0 (#158476) 2025-12-09 12:30:57 -05:00
epenet 61499a5ad4 Fix Tuya BitmapTypeInformation parsing (#158474) 2025-12-09 17:39:54 +01:00
epenet 0076aafa6e Move color_extractor services to separate module (#158341) 2025-12-09 17:32:03 +01:00
epenet c50f4d6d2d Add Tuya local_strategy to Tuya diagnostic (#158450) 2025-12-09 17:29:11 +01:00
hanwg 68036099a2 Remove timeout parameter for Telegram bot actions (#155198)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-09 16:53:02 +01:00
Willem-Jan van Rootselaar 180053fe98 Bump python-bsblan to v3.1.3 (#157626) 2025-12-09 15:33:04 +01:00
Thomas D 280c25cb85 Enable volvo engine status for all engine types (#158437) 2025-12-09 14:35:02 +01:00
Petro31 4064b6d28c Sort weather platform keys (#158106)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-09 14:29:01 +01:00
epenet ff25809a3e Do not unregister google_mail services (#158431) 2025-12-09 13:52:37 +01:00
mettolen 245f47c7fb Add diagnostics to Airobot integration (#158247) 2025-12-09 12:55:37 +01:00
Andre Lengwenus 86135a19d1 Bump pypck to 0.9.7 (#158089) 2025-12-09 12:43:21 +01:00
Simon Lamon 2e038250a9 Bump doorbirdpy to v3.0.11 (#151178)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-09 10:37:18 +01:00
Jordan Harvey 88c7c6fc8a Update pynintendoparental to 2.0.0 (#158285) 2025-12-09 08:47:52 +01:00
epenet d691862d0d Bump heatmiserV3 to 2.0.4 (#158304) 2025-12-09 08:47:16 +01:00
Allen Porter cceaff7bc6 Fix roborock off peak electricity timer (#158292) 2025-12-09 08:24:04 +01:00
Michael Hansen 079c6daa63 Replace microVAD with Silero VAD (ggml) (#158282) 2025-12-08 20:02:14 -06:00
Michel D'Astous b120ae827f Fix webhook exception when empty json data is sent (#158254) 2025-12-08 23:44:59 +01:00
Tsvi Mostovicz c1227aaf1f Jewish Calendar coordinator (#152434) 2025-12-08 22:41:58 +01:00
Artur Pragacz c0365dfe99 Query state after turn on in Onkyo (#158093) 2025-12-08 21:49:00 +01:00
Petro31 02aa3fc906 Fix legacy template entity_id field in migration (#158105)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-08 21:07:40 +01:00
Marcel van der Veldt 42e55491cc Skip check for onboarding done in Music Assistant integration (#158270) 2025-12-08 20:28:02 +01:00
Harvey 33e09c4967 Bump HueBLE to 2.1.0 (#158197) 2025-12-08 19:55:55 +01:00
Petro31 6f5507670f Fix multiple top-level support for template integration (#158244) 2025-12-08 19:19:52 +01:00
Paul Bottein 765be3f047 Update frontend to 20251203.2 (#158259) 2025-12-08 18:08:03 +01:00
omrishiv 12bc9e9f68 fix Lutron Caseta smart away subscription (#158082)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-08 17:45:36 +01:00
Nic Eggert 2617c4a453 Add eGauge integration (#155279)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-08 17:33:11 +01:00
andreimoraru 0e6d9ecbdc Bump yt-dlp to 2025.12.08 (#158253) 2025-12-08 16:45:23 +01:00
Sander Blomvågnes 5cdbbe999d Move Entur constants to separate module (#158256) 2025-12-08 16:40:54 +01:00
Yevhenii Vaskivskyi 5ca61386f8 Bump asusrouter to 1.21.1 (#158192) 2025-12-08 16:16:25 +01:00
Thomas55555 6d6ee866a6 Remove stale fixture in Google Air Quality (#158235) 2025-12-08 15:44:05 +01:00
Paul Bottein eeb2b2febc Be more specific about winter mode in the description (#158230)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-08 15:28:17 +01:00
Thomas55555 a6c7bd76eb Bump google air quality api to 2.0.0 (#158234) 2025-12-08 13:41:28 +01:00
epenet 470f5a2396 Validate action translation placeholders (#158225)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-12-08 13:40:53 +01:00
Marc Mueller d934fd974d Update Python to 3.13.11 and 3.14.2 in CI (#158238) 2025-12-08 13:30:45 +01:00
epenet edc81b706d Fix teslemetry service description placeholders (#158240) 2025-12-08 12:50:23 +01:00
epenet 03aaebe718 Fix zwave_js service description placeholders (#158236) 2025-12-08 12:47:35 +01:00
epenet 98d61aa5b2 Fix yeelight service description placeholders (#158239) 2025-12-08 12:46:33 +01:00
Jan Bouwhuis fe5d411856 Fix description placeholders for system_bridge (#158232) 2025-12-08 12:26:46 +01:00
Petar Petrov efa5a773eb Add query params handling for requests in Supervisor API (#157832)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-08 12:09:59 +01:00
Jordan Harvey 32399de5f1 Bump pyanglianwater to 3.0.0 (#158121) 2025-12-08 11:01:41 +01:00
Klaas Schoute a1ad28c066 Update powerfox to v2.0.0 (#158223) 2025-12-08 10:59:59 +01:00
Álvaro Fernández Rojas 6faccf4327 Update aioairzone to v1.0.4 (#158208)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2025-12-08 10:28:26 +01:00
Allen Porter 2ac15ab67d Ensure Roborock disconnects mqtt on unload/stop (#158144)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 09:36:34 +01:00
dependabot[bot] d599bb9553 Bump github/codeql-action from 4.31.6 to 4.31.7 (#158218)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 08:51:12 +01:00
Klaas Schoute 92ee37017d Update energyzero to v4.0.1 (#158211) 2025-12-08 08:50:14 +01:00
Allen Porter adf698d570 Bump python-roborock to 3.10.10 (#158212) 2025-12-07 20:06:50 -08:00
taltenbach 6ce9a13816 Add Roborock attach/detach mop status translations (#158184) 2025-12-07 16:33:24 -08:00
epenet 9cb9efeb88 Make Tuya find_dpcode a class method (#158028) 2025-12-07 22:32:14 +01:00
J. Nick Koston ca31134caa Keep persistent BLE connection during Shelly WiFi provisioning (#158145)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-07 14:28:53 -06:00
Raphael Hehl 769578dc51 UnifiProtect: Create NVR device before loading platforms to fix via_device references (#158191) 2025-12-07 21:22:39 +01:00
Thomas55555 9dcabfe804 Use SectionConfig in Google Air Quality (#158188) 2025-12-07 19:49:44 +01:00
Åke Strandberg dc6c23a58c Add program id codes for Miele WQ1000 (#158175) 2025-12-07 19:34:35 +01:00
Álvaro Fernández Rojas 6ec7efc2b8 Update aioairzone to v1.0.3 (#158181) 2025-12-07 11:54:04 -06:00
Åke Strandberg 97e5b7954e Bump pymiele dependency to 0.6.1 (#158177) 2025-12-07 18:35:28 +01:00
J. Nick Koston 25505752b7 Use "Output" for Shelly RPC switch sub-device names (#158139) 2025-12-07 11:33:00 -06:00
J. Nick Koston 95a347dcf8 Bump aioshelly to 13.23.0 (#158183) 2025-12-07 11:18:53 -06:00
Manu 8c0f3014f7 Fix secure URLs for promotional game media in Xbox integration (#158162) 2025-12-07 11:34:18 +01:00
Josef Zweck bb3cd3ebd3 Bump pylamarzocco to 2.2.3 (#158104) 2025-12-07 08:57:02 +01:00
Philip Cheong 319d6711c4 Add support for LockStatus.JAMMED to yalexs_ble (#157551)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-06 13:17:09 -06:00
J. Nick Koston ea3f76c315 Bump yalexs-ble to 3.2.2 (#158124) 2025-12-06 12:16:52 -06:00
Shay Levy b892cc1cad Revert "Remove Shelly redundant device entry check for sleepy devices" (#158108) 2025-12-06 19:40:05 +02:00
Marc Mueller 3046c7afd8 Fix shelly RuntimeWarnings in tests (#158101) 2025-12-06 11:34:24 -06:00
Raphael Hehl 73dc81034e Implement reconfiguration flow for UniFi Protect integration (#157532)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-06 11:32:43 -06:00
Jesse Hills f306cde3b6 Add response support to esphome custom actions (#157393)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-06 09:48:20 -06:00
Kevin Stillhammer 38c5e483a8 add entity_picture to fressnapf_tracker (#158099) 2025-12-06 13:28:53 +01:00
Michael ce14544ec1 Add packet loss sensor to Ping integration (#158081) 2025-12-06 10:57:31 +01:00
mettolen 87b9c3193e Add sensor entities to Airobot integration (#157938) 2025-12-06 07:57:03 +01:00
Adam Goode 061c38d2a7 Make unifi LEDs EntityCategory.CONFIG (#158088) 2025-12-06 07:51:09 +01:00
Allen Porter e1720be5a4 Update roborock quality scale (#158024) 2025-12-05 22:52:38 +01:00
Paul Bottein 2d13a92496 Update frontend to 20251203.1 (#158069) 2025-12-05 21:25:01 +01:00
Artur Pragacz b06bffa815 Add ai_task to core files (#158058) 2025-12-05 21:14:49 +01:00
Joost Lekkerkerker b8f4b9515b Prevent entsoe from loading (#158036)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-05 21:08:57 +01:00
Petro31 3c10e9f1c0 Fix inverted kelvin issue (#158054) 2025-12-05 19:12:12 +00:00
Artur Pragacz 2dec3befcd Assign hass in Condition init (#158062) 2025-12-05 19:04:11 +00:00
J. Nick Koston 7d065bf314 Bump aiodns to 3.6.0 (#158063) 2025-12-05 20:00:09 +01:00
Raphael Hehl 3315680d0b Bump uiprotect to 7.33.2 (#158057) 2025-12-05 19:43:44 +01:00
Markus Jacobsen ce48c89a26 Fix button event entity creation in Bang & Olufsen (#157982)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-05 18:39:58 +00:00
Jan Bouwhuis f67a926f56 Move lametric URLs out of strings.json (#158051) 2025-12-05 19:35:07 +01:00
Paulus Schoutsen e0a9d305b2 Use multiple selector for validation in AI task (#158056) 2025-12-05 18:51:18 +01:00
Jan Bouwhuis 4ff141d35e Move example image path out of translatable strings (#158053) 2025-12-05 18:05:09 +01:00
Artur Pragacz f12a43b2b7 Mark reauthentication in music assistant quality scale (#158055) 2025-12-05 18:02:16 +01:00
Paul Tarjan 35e6f504a3 Fix doorbird duplicate unique ID generation (#158013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 10:17:43 -06:00
Denis Shulyaka 1f68809cf9 Replace deprecated preview image model (#158048) 2025-12-05 07:55:05 -08:00
Paul Bottein 66bddebca1 Add subscribe preview feature endpoint to labs (#157976) 2025-12-05 16:36:56 +01:00
TheJulianJES 2280d779a8 Change ZHA strings for incorrect adapter state (#158021)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-05 16:35:34 +01:00
Maciej Bieniek ebc608845c Do not create restart button for sleeping gen2+ Shelly devices (#158047) 2025-12-05 16:33:11 +01:00
Max Michels 5d13a41926 Move telegram-bot URLs out of strings.json (#155130)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-12-05 16:33:01 +01:00
Quentin Ulmer 630b40fbba Fix Rituals Perfume Genie (#151537)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 16:16:51 +01:00
Manu 7fd440c4a0 Add coordinator to Duck DNS integration (#158041) 2025-12-05 15:49:48 +01:00
Petro31 2a116a2a11 Fix missing template key in deprecation repair (#158033) 2025-12-05 15:30:39 +01:00
David Bonnes f189e3b5ca Bump evohome-async to 1.0.6 (#158005) 2025-12-05 13:27:38 +01:00
wollew 4cd460351d Add Squeezebox binary sensors for player alarm status (#154491)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-05 11:43:19 +01:00
Johnny Willemsen afea571c2c Enhance migration logging for home_connect (#158027) 2025-12-05 11:30:10 +01:00
Manu e4aadd675e Add reconfigure flow to Duck DNS (#157948) 2025-12-05 10:19:05 +01:00
Abílio Costa a47255c233 Bump whirlpool-sixth-sense to 1.0.3 (#157996) 2025-12-05 08:27:31 +01:00
hanwg c1e7492743 Improve action descriptions for Telegram bot (#158022) 2025-12-05 08:26:42 +01:00
Kevin Stillhammer 63e8cf582f Set PARALLEL_UPDATES in fressnapf_tracker (#158008) 2025-12-05 08:21:48 +01:00
Allen Porter 73f23168a2 Bump python-roborock to 3.10.2 (#158020) 2025-12-05 08:20:41 +01:00
Mark Adkins 20d8176515 SharkIQ dep upgrade v1.5.0 (#158015) 2025-12-04 22:00:47 -05:00
Ezra Freedman c9351a022e Add HassStopMoving intent for covers and valves (#155267)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-12-04 22:23:49 +01:00
epenet 4e8a31a4e2 Improve Tuya data validation (#157968)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 22:20:05 +01:00
johanzander 2beb551db3 Replace bare Exception with specific exceptions in Growatt (#157790) 2025-12-04 20:44:41 +01:00
Markus Jacobsen 90cea0325f Bump mozart_api to 5.3.1.108.0 (#157983) 2025-12-04 19:29:35 +00:00
dontinelli f5dd9d83ac Bump solarlog_cli to 0.6.1 (#157845) 2025-12-04 19:24:33 +00:00
Alsatian67 e0484ba1ff Improve dev error message for YAML platform setup missing method (#155505)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-04 20:06:37 +01:00
Kevin Stillhammer 62f758f695 mark quality_scale rules done for fressnapf_tracker (#157990) 2025-12-04 20:03:53 +01:00
Abílio Costa 20d2115122 Bump oralb-ble to 1.0.2 (#157992) 2025-12-04 18:51:16 +01:00
Jan Bouwhuis 2bed7afe0e Move out example URL and IP of strings.json for reolink (#157970) 2025-12-04 18:37:30 +01:00
Petro31 2eeac5f9c9 Update template deprecation to be more explicit (#157965) 2025-12-04 18:34:01 +01:00
Abílio Costa a35af9097b Remove uneeded async_setup_component from trigger/condition tests (#157873) 2025-12-04 17:21:30 +00:00
Luke Lashley 710b7c2b41 Bump python-Roborock to 3.10.0 (#157980) 2025-12-04 17:26:41 +01:00
Abílio Costa c058810461 Cache flattened service descriptions in websocket api (#157510)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-04 16:05:49 +00:00
Kevin Stillhammer 0ccfd77fef add switch platform to fressnapf_tracker (#157971) 2025-12-04 16:44:53 +01:00
epenet 4805b33a27 Fix unit parsing in Tuya climate entities (#157964) 2025-12-04 16:17:39 +01:00
Hem Bhagat c333036959 Move translatable URL out of strings.json for ntfy integration (#155859) 2025-12-04 16:17:16 +01:00
Petro31 002eed24f1 Fix template migration errors (#157949) 2025-12-04 16:16:58 +01:00
Jan Bouwhuis 9a9f8271b3 Move pilight URL out of strings.json (#157967) 2025-12-04 16:02:28 +01:00
Paulus Schoutsen 855d7c6e16 Extract WebRTC integration (#157648) 2025-12-04 09:44:24 -05:00
Jordan Harvey 837de55ce6 Set account number as required for Anglian Water config entry (#157939) 2025-12-04 15:39:52 +01:00
epenet 81ed259c59 Move Tuya type information classes to separate module (#157958)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 15:34:04 +01:00
Manu 5f00452c96 Convert image URLs to secure URLs in Xbox integration (#157945) 2025-12-04 15:12:33 +01:00
Jan Bouwhuis 06a44de3fb Move Yeelight URLs out of translatable strings for action descriptions (#157957) 2025-12-04 15:10:50 +01:00
Jan Bouwhuis 11b4d75cfb Move out zwave_js api docs url from strings.json (#157959) 2025-12-04 15:10:26 +01:00
David Rapan 845c9ee05f Fix Starlink's ever updating uptime (#155574)
Signed-off-by: David Rapan <david@rapan.cz>
2025-12-04 14:44:23 +01:00
Jordan Harvey dedf6b1223 Add pyanglianwater to Anglian Water loggers (#157947) 2025-12-04 13:55:24 +01:00
Felipe Santos c1b631d049 Remove Intellicode extension from devcontainer (#157894) 2025-12-04 13:43:01 +01:00
milanhin 6cc645bc6c Remove deprecation warning of step_id in ConfigFlow class (#157925) 2025-12-04 13:41:52 +01:00
Jan Bouwhuis f10866395d Move out URL of Xiaomy_aquara from strings.json (#157937)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 12:59:55 +01:00
Jan Bouwhuis df68448b27 Move translatable URL from rainmachine push_weather_data action description (#157941)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-04 12:59:15 +01:00
Abílio Costa bf7b96622c Correct websocket commands test name (#157870) 2025-12-04 11:14:34 +00:00
Kevin Stillhammer 53c644ac5b add light platform to fressnapf_tracker (#157865) 2025-12-04 11:09:53 +01:00
starkillerOG 5e9107e52b Bump reolink_aio to 0.17.1 (#157929) 2025-12-04 10:58:25 +01:00
Jan Bouwhuis ca9ea267c7 Move teslemetry time-of-use URL out of strings.json (#157874) 2025-12-04 10:34:51 +01:00
ryanjones-gentex f1bfe2f11e Add HomeLink integration (#136460)
Co-authored-by: Nicholas Aelick <niaexa@syntronic.com>
2025-12-04 10:32:02 +01:00
Franck Nijhof 34cc6036b9 Merge branch 'master' into dev 2025-12-04 09:12:55 +00:00
cdnninja 2facfbadaa Add VeSync type hints and returns (#157900)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-04 09:19:48 +01:00
cdnninja 1b1dface35 Fix VeSync binary sensor discovery (#157898) 2025-12-04 09:14:35 +01:00
TheJulianJES 3c0cfd5e0c Display error when forming new ZHA network fails (#157863) 2025-12-03 22:37:52 -05:00
Luke Lashley 69f66ffef4 Correctly pass MopParserConfig for Roborock (#157891) 2025-12-03 17:35:51 -08:00
Norbert Rittel d2c3543b6c Consistently use "Labs" as name in kitchen_sink (#157875) 2025-12-04 00:19:24 +01:00
Franck Nijhof ca4a2d441e 2025.12.0 (#157330) 2025-12-03 19:06:27 +01:00
Joost Lekkerkerker f42fe9cee3 Add button to reset hood filter to SmartThings (#157847) 2025-12-03 18:38:23 +01:00
Kevin Stillhammer b67873f40c bump fressnapftracker to 0.2.0 (#157868) 2025-12-03 18:29:20 +01:00
Markus Jacobsen ecc08fce0f Reduce naming verbosity in Bang & Olufsen (#157825) 2025-12-03 17:46:18 +01:00
Ludovic BOUÉ 375f536b15 Add Matter DoorPositionSensor open/closed count sensors (#155809) 2025-12-03 17:35:47 +01:00
Kevin Stillhammer 5cff813eac remove deep_sleep binary_sensor from fressnapf_tracker (#157857) 2025-12-03 17:28:59 +01:00
Franck Nijhof c2ce322af1 Bump version to 2025.12.0 2025-12-03 16:28:33 +00:00
Thomas55555 079f306a65 Fix strings in Google Air Quality (#157862) 2025-12-03 16:28:06 +00:00
Thomas55555 9129665c64 Fix strings in Google Air Quality (#157862) 2025-12-03 17:26:21 +01:00
Franck Nijhof 7bf60f9d15 Bump version to 2025.12.0b9 2025-12-03 15:57:15 +00:00
Robert Resch 7dddd89ac2 Add retry logic to docker.io image push step (#157859) 2025-12-03 15:57:03 +00:00
Luke Lashley a2322ef3c7 Bump Roborock to 3.9.3 (#157852) 2025-12-03 15:57:01 +00:00
Bram Kragten 5f6ef2109a Update frontend to 20251203.0 (#157851) 2025-12-03 15:57:00 +00:00
starkillerOG 44f0a8899a Bump reolink_aio to 0.17.0 (#157850) 2025-12-03 15:56:58 +00:00
Robert Resch 78fa29b41f Bump deebot-client to 17.0.0 (#157836) 2025-12-03 15:56:57 +00:00
Manu 06d4f085c0 Prevent startup blocking when a friend’s trophy summary is private on PlayStation Network (#157597)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-12-03 15:56:55 +00:00
Robert Resch f4e11da1a6 Add retry logic to docker.io image push step (#157859) 2025-12-03 16:53:45 +01:00
Bram Kragten e0238b5ab2 Update frontend to 20251203.0 (#157851) 2025-12-03 10:40:05 -05:00
Luke Lashley 352f3813e2 Bump Roborock to 3.9.3 (#157852) 2025-12-03 16:37:59 +01:00
Manu b1399a5541 Prevent startup blocking when a friend’s trophy summary is private on PlayStation Network (#157597)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-12-03 16:29:51 +01:00
J. Nick Koston 316cddec86 Bump bleak to 2.0.0 (#157766) 2025-12-03 15:25:42 +00:00
starkillerOG 2f71aec26f Bump reolink_aio to 0.17.0 (#157850) 2025-12-03 16:22:02 +01:00
Joost Lekkerkerker aa72b76ee7 Add cooktop fixture to SmartThings (#157842) 2025-12-03 15:36:29 +01:00
Erik Montnemery e009898107 Remove template config entry from source device (#157814)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 15:24:48 +01:00
Artur Pragacz ceb13e70b9 Add integration type to wake_on_lan (#157726)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 15:14:57 +01:00
Robert Resch 498a80ac7f Bump deebot-client to 17.0.0 (#157836) 2025-12-03 15:12:31 +01:00
victorigualada a9deb2a08a Bump hass-nabucasa from 1.6.2 to 1.7.0 (#157834) 2025-12-03 13:56:01 +00:00
victorigualada 0d26d22986 Allow non strict response_format structures for Cloud LLM generation (#157822) 2025-12-03 13:55:09 +00:00
Erik Montnemery 062366966b Pin Python point release used in CI (#157819) 2025-12-03 13:53:30 +00:00
Artur Pragacz 1a60c46d67 Bump aioonkyo to 0.4.0 (#157838) 2025-12-03 14:46:52 +01:00
Matthias Alphart 62fba5ca20 Update xknx to 3.12.0 (#157835) 2025-12-03 14:40:40 +01:00
victorigualada b54cde795c Bump hass-nabucasa from 1.6.2 to 1.7.0 (#157834) 2025-12-03 14:37:45 +01:00
victorigualada 0f456373bf Allow non strict response_format structures for Cloud LLM generation (#157822) 2025-12-03 14:31:09 +01:00
IAmStiven a5042027b8 Add support for new ElevenLabs model Scribe v2 (#156961) 2025-12-03 14:29:25 +01:00
Franck Nijhof 1b8a50e80a Bump version to 2025.12.0b8 2025-12-03 12:16:30 +00:00
Franck Nijhof 59761385f0 Add final learn more and feedback links for purpose-specific triggers and conditions preview feature (#157830) 2025-12-03 12:16:21 +00:00
Robert Resch 6536d348e5 Prioritize default stun port over alternative (#157829) 2025-12-03 12:16:20 +00:00
Michael c157c83d54 Add integration_type to Oralb (#157828) 2025-12-03 12:16:18 +00:00
torben-iometer 77425cc40f bump iometer to v0.3.0 (#157826) 2025-12-03 12:16:16 +00:00
Franck Nijhof b15b5ba95c Add final learn more and feedback links for purpose-specific triggers and conditions preview feature (#157830) 2025-12-03 13:14:37 +01:00
Robert Resch cd6e72798e Prioritize default stun port over alternative (#157829) 2025-12-03 13:14:28 +01:00
Kamil Breguła 739157e59f Simplify availability property in WLED (#157800)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-03 13:00:21 +01:00
torben-iometer 267aa1af42 bump iometer to v0.3.0 (#157826) 2025-12-03 12:47:05 +01:00
Michael 7328b61a69 Add integration_type to Oralb (#157828) 2025-12-03 12:46:50 +01:00
Franck Nijhof c4b67329c3 Bump version to 2025.12.0b7 2025-12-03 10:42:56 +00:00
Allen Porter c1f8c89bd0 Bump python-roborock to 3.9.2 (#157815)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:42:32 +00:00
Allen Porter b1bf6f5678 Bump google-nest-sdm to 9.1.2 (#157812)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:42:30 +00:00
Josef Zweck d347136188 Mark nordpool as service integration_type (#157810) 2025-12-03 10:42:29 +00:00
Kamil Breguła a4319f3bf8 Update release URL in WLED (#157801) 2025-12-03 10:42:27 +00:00
Marc Mueller db27aee62a Fix ping TypeError when killing the process (#157794) 2025-12-03 10:42:26 +00:00
Joost Lekkerkerker a7446b3da9 Make occupancy trigger check occupancy instead of presence (#157791) 2025-12-03 10:42:24 +00:00
Stefan Agner 7fc5464621 Add storage link to low disk space repair issue (#157786)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-12-03 10:42:23 +00:00
hanwg a00b50c195 Fix bug in group notify entities when title is missing (#157171)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 10:42:21 +00:00
Allen Porter 203f2fb364 Bump google-nest-sdm to 9.1.2 (#157812)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 11:23:00 +01:00
Josef Zweck b956c17ce4 Mark nordpool as service integration_type (#157810) 2025-12-03 11:22:42 +01:00
Marc Mueller 5163dc0567 Fix ping TypeError when killing the process (#157794) 2025-12-03 11:22:14 +01:00
Allen Porter 31a0478717 Bump python-roborock to 3.9.2 (#157815)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-03 10:56:56 +01:00
dependabot[bot] 24da3f0db8 Bump actions/checkout from 6.0.0 to 6.0.1 (#157806)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 10:38:45 +01:00
dependabot[bot] 786922fc5d Bump actions/stale from 10.1.0 to 10.1.1 (#157807)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 10:36:44 +01:00
Erik Montnemery c2f8b6986b Pin Python point release used in CI (#157819) 2025-12-03 10:26:15 +01:00
hanwg 0a0832671f Fix bug in group notify entities when title is missing (#157171)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-03 09:44:01 +01:00
Aidan Timson 7b353d7ad4 Add levoit virtual integration (#157618) 2025-12-03 09:38:01 +01:00
epenet 99de73a729 Update SFR Box unit of measurement (#157813) 2025-12-03 08:46:59 +01:00
Joost Lekkerkerker 1995fbd252 Make occupancy trigger check occupancy instead of presence (#157791) 2025-12-03 08:15:31 +01:00
Kamil Breguła 315ea9dc76 Update release URL in WLED (#157801) 2025-12-03 05:55:03 +01:00
Josef Zweck 639a96f8cb La Marzocco add Bluetooth offline mode (#157011) 2025-12-03 05:53:27 +01:00
Stefan Agner b6786c5a42 Add storage link to low disk space repair issue (#157786)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-12-02 22:14:24 -05:00
Kamil Breguła 6f6e9b8057 Add quality scale for WLED (#155482)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-03 01:19:02 +01:00
johanzander e0c687e415 Remove extra logging in Growatt (#157788) 2025-12-03 00:38:03 +01:00
Abestanis 982362110c Allow to configure KNX time, date & datetime entities via UI (#157603) 2025-12-02 23:45:43 +01:00
Lukas 90dc3a8fdf Pooldose: add number platform (#157787) 2025-12-02 23:31:44 +01:00
J. Nick Koston 5112742b71 Bump habluetooth to 5.8.0 (#157771) 2025-12-02 15:55:37 -06:00
johanzander 8899bc01bd Add bronze quality scale to Growatt Server integration (#154649)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 22:36:51 +01:00
Franck Nijhof 738fb59efa Bump version to 2025.12.0b6 2025-12-02 21:23:23 +00:00
Joris Pelgröm 04e512a48e Bump letpot to 0.6.4 (#157781) 2025-12-02 21:23:12 +00:00
Michael Hansen c63aca2d9b Bump hassil to 3.5.0 (#157780) 2025-12-02 21:23:11 +00:00
Kamil Breguła c95203e095 Handle unsupported version in WLED (#157778)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-02 21:23:09 +00:00
Josef Zweck 259235ceeb Add integration_type for tedee (#157776) 2025-12-02 21:23:08 +00:00
Paulus Schoutsen c7f1729300 Allow fetching the Cloud ICE servers (#157774) 2025-12-02 21:23:06 +00:00
puddly 065329e668 Fix ZHA network formation (#157769) 2025-12-02 21:23:05 +00:00
Marcel van der Veldt a93ed69fe4 Let AuthenticationRequired also trigger the reauth flow in MusicAssistant (#157580) 2025-12-02 21:23:04 +00:00
Sab44 189497622d Fix orphaned devices not being removed during integration startup (#155900) 2025-12-02 21:23:02 +00:00
Joris Pelgröm ed8f9105ff Bump letpot to 0.6.4 (#157781) 2025-12-02 22:12:41 +01:00
Michael Hansen 185de98f5e Bump hassil to 3.5.0 (#157780) 2025-12-02 22:11:00 +01:00
Paulus Schoutsen e857abb43f Allow fetching the Cloud ICE servers (#157774) 2025-12-02 16:02:30 -05:00
Joost Lekkerkerker 5b1829f3a1 Add hood filter usage entity to SmartThings (#157775) 2025-12-02 21:45:17 +01:00
Kamil Breguła 520156a33a Handle unsupported version in WLED (#157778)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-02 21:20:36 +01:00
Kevin Stillhammer e3b5342b76 use sentence casing in binary_sensor for fressnapf_tracker (#157772) 2025-12-02 21:06:18 +01:00
Josef Zweck 951b19e80c Add integration_type for tedee (#157776) 2025-12-02 21:04:51 +01:00
Sab44 e2351ecec2 Fix orphaned devices not being removed during integration startup (#155900) 2025-12-02 21:03:37 +01:00
Joost Lekkerkerker d75e5498c6 Add health concern entities to SmartThings (#157773) 2025-12-02 21:00:50 +01:00
puddly 2dd58dbe39 Fix ZHA network formation (#157769) 2025-12-02 14:59:55 -05:00
Joost Lekkerkerker 4ef17799db Add snapshot test to Vivotek (#157767) 2025-12-02 20:47:02 +01:00
Joost Lekkerkerker 9373378350 Add fixture for hood to SmartThings (#157770) 2025-12-02 20:46:06 +01:00
Marcel van der Veldt 18833a194b Let AuthenticationRequired also trigger the reauth flow in MusicAssistant (#157580) 2025-12-02 14:22:40 -05:00
Kevin Stillhammer 2631c77bee add platform binary_sensor to fressnapf_tracker (#157753) 2025-12-02 20:05:34 +01:00
Kevin McCormack c67247bf32 Add config flow for Vivotek integration (#154801)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 19:47:22 +01:00
Joost Lekkerkerker 18b5ffd365 Add SmartThings walloven fixtures (#157748) 2025-12-02 19:32:28 +01:00
Franck Nijhof a466fc4a01 Bump version to 2025.12.0b5 2025-12-02 18:19:42 +00:00
Matthias Alphart 8a968b5d0e Add integration_type for Fronius (#157760) 2025-12-02 18:19:13 +00:00
Michael Hansen 3baee5c4ac Bump intents to 2025.12.2 (#157758) 2025-12-02 18:17:57 +00:00
Bram Kragten f624a43770 Update frontend to 20251202.0 (#157755)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-02 18:16:17 +00:00
Artur Pragacz 242935774b Add integration type to ping (#157730)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:17 +00:00
Artur Pragacz 051ad5878f Add integration type to rest (#157728)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:16 +00:00
Artur Pragacz b2156c1d4c Add integration type to speedtestdotnet (#157727)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:15 +00:00
Artur Pragacz 7d4394f7ed Add integration type to google_translate (#157718)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 18:14:14 +00:00
Brett Adams 4df172374c Add integration_type to Tesla Fleet manifest (#157679) 2025-12-02 18:14:13 +00:00
Brett Adams c97755472e Add integration_type to Teslemetry manifest (#157677) 2025-12-02 18:14:11 +00:00
Erik Montnemery ebc9060b01 Improve trigger descriptions (#157643)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-02 18:14:10 +00:00
Erik Montnemery bbcc2a94b3 Add occupancy binary sensor triggers (#157631) 2025-12-02 18:14:09 +00:00
victorigualada 692188fa85 Don't register Home Assistant Cloud LLM platforms if not logged in (#157630) 2025-12-02 18:14:08 +00:00
Jordan Harvey 2c993ea5a2 Fix Anglian Water sensor setup (#157457) 2025-12-02 18:14:06 +00:00
Bram Kragten c4e3a4d65e Update frontend to 20251202.0 (#157755)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-02 17:50:13 +01:00
victorigualada 84d2686517 Don't register Home Assistant Cloud LLM platforms if not logged in (#157630) 2025-12-02 17:47:08 +01:00
Michael Hansen ae8980ce5b Bump intents to 2025.12.2 (#157758) 2025-12-02 17:43:21 +01:00
epenet b2d4c9ecb4 Add exception translation to SFR box (#157756) 2025-12-02 17:42:16 +01:00
Matthias Alphart f5b046ee7d Add integration_type for Fronius (#157760) 2025-12-02 17:31:05 +01:00
epenet 55c5fb7374 Migrate Tuya climate (swing) to use wrapper class (#157646) 2025-12-02 17:24:36 +01:00
Erik Montnemery 5d78cd328a Remove explicit templating of velbus service data (#157749) 2025-12-02 17:00:10 +01:00
epenet bc36578ada Add mac address to SFR Box device registry entries (#157752) 2025-12-02 16:52:09 +01:00
Erik Montnemery e63242e465 Add occupancy binary sensor triggers (#157631) 2025-12-02 16:37:02 +01:00
epenet e84c09745d Bump SFR box IQS to silver (#157754) 2025-12-02 16:32:38 +01:00
Julian Meier f07991d0ba Add boot and energy sensor to MyStrom Switch (#155132) 2025-12-02 15:42:04 +01:00
epenet 872fef1f6f Add reconfigure flow to SFR Box (#157711) 2025-12-02 15:35:25 +01:00
Kevin Stillhammer c866dc973c Add sensor platform to fressnapf_tracker (#157658)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-02 15:33:57 +01:00
Zoltán Farkasdi e2acf30637 Add Netatmo outdoor camera test (#156740) 2025-12-02 15:01:47 +01:00
epenet 29631a2c5a Cleanup SFR Box sensors (#157708) 2025-12-02 14:52:52 +01:00
Heindrich Paul 1d31e6d0ea Create more sensors for Nederlandse Spoorwegen (#154466)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-02 14:39:05 +01:00
Franck Nijhof c765776726 Bump version to 2025.12.0b4 2025-12-02 12:42:32 +00:00
Robert Resch 723365d8e6 Create the go2rtc unix socket inside a temporary folder (#157742) 2025-12-02 12:42:13 +00:00
Artur Pragacz 3d8e136049 Add integration type to xiaomi_ble (#157740)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:11 +00:00
Artur Pragacz 2fe9fc7ee3 Add integration type to broadlink (#157739)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:10 +00:00
Artur Pragacz e11e31a1a0 Add integration type to ring (#157738)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:09 +00:00
Artur Pragacz 989407047d Add integration type to roborock (#157737)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:08 +00:00
Artur Pragacz 6d3087c5a4 Add integration type to webostv (#157736)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:06 +00:00
Artur Pragacz 9bd3c35231 Add integration type to tplink (#157735)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:05 +00:00
Artur Pragacz b7e97971cf Add integration type to ibeacon (#157734)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:04 +00:00
Artur Pragacz 4d232c63f8 Add integration type to dlna_dmr (#157733)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:03 +00:00
Artur Pragacz 6fc000ee2a Add integration type to google (#157729)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:02 +00:00
Artur Pragacz 623d3ecde5 Add integration type to music_assistant (#157725)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:00 +00:00
Artur Pragacz 0fbb3215b4 Add integration type to dlna_dms (#157723)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:59 +00:00
Artur Pragacz c82ce1ff89 Add integration type to met (#157720)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:58 +00:00
Franck Nijhof 8c891a20e5 Rename preview feature to purpose-specific triggers and conditions (#157717) 2025-12-02 12:41:57 +00:00
Erik Montnemery 97c50b2d86 Improve helpers.condition.async_subscribe_platform_events (#157710) 2025-12-02 12:41:55 +00:00
Erik Montnemery ef4062a565 Improve helpers.trigger.async_subscribe_platform_events (#157709) 2025-12-02 12:41:54 +00:00
cdnninja e31cce5d9b Bump pyvesync to 3.3.3 (#157697) 2025-12-02 12:41:53 +00:00
Paulus Schoutsen 21f6b9a53a Add integration_type to Nuki Bridge manifest (#157683)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:52 +00:00
Paulus Schoutsen 047e549112 Add integration_type to Motionblinds manifest (#157682)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:51 +00:00
Paulus Schoutsen 4c4aecd9a7 Add integration_type to Konnected.io manifest (#157681) 2025-12-02 12:41:50 +00:00
Paulus Schoutsen 733496ff3f Add integration_type to HomeWizard Energy manifest (#157680)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:49 +00:00
Paulus Schoutsen f682e93243 Add integration_type to Tessie manifest (#157676)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:48 +00:00
Paulus Schoutsen c8fa5b0290 Add integration_type to SwitchBot Bluetooth manifest (#157675)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:46 +00:00
Paulus Schoutsen 8ff2a22664 Add integration_type to Sonos manifest (#157674)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:45 +00:00
Paulus Schoutsen c174ab2d96 Add integration_type to SmartThings manifest (#157673) 2025-12-02 12:41:44 +00:00
Paulus Schoutsen 10f0ff7bd7 Add integration_type to Reolink manifest (#157672)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:43 +00:00
Paulus Schoutsen 4a4eb33bf7 Add integration_type to HomeKit Device manifest (#157671)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:42 +00:00
Paulus Schoutsen 8199c4e5de Add integration_type to Home Connect manifest (#157668)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:40 +00:00
Paulus Schoutsen 0bfa8318a7 Add integration_type to Ecowitt manifest (#157666) 2025-12-02 12:41:39 +00:00
Paulus Schoutsen ed66a4920c Add integration_type to Apple TV manifest (#157664)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:41:38 +00:00
Åke Strandberg f51007c448 Add program id:s and phases to new Miele WQ1000 (#157660) 2025-12-02 12:41:37 +00:00
TheJulianJES bd44402b04 Set Matter integration type to "hub" (#157657) 2025-12-02 12:41:36 +00:00
TheJulianJES 99fa92d966 Set ZHA integration type to "hub" (#157656) 2025-12-02 12:41:35 +00:00
Arjan 1cb8f19020 Meteo France: add new mapping "Brouillard dense givrant" (#157627) 2025-12-02 12:41:33 +00:00
Copilot 81cdbdd4df Add labs_updated event to subscription allowlist (#157552)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2025-12-02 12:41:32 +00:00
Artur Pragacz 8109d9a39c Add integration type to music_assistant (#157725)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:38:08 +01:00
Artur Pragacz e1abd451b8 Add integration type to google (#157729)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:37:37 +01:00
Robert Resch 2c72cd94f2 Create the go2rtc unix socket inside a temporary folder (#157742) 2025-12-02 13:35:39 +01:00
Franck Nijhof 3bccb4b89c Rename preview feature to purpose-specific triggers and conditions (#157717) 2025-12-02 13:34:52 +01:00
Artur Pragacz 6d4fb30630 Add integration type to tplink (#157735)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:24:21 +01:00
Artur Pragacz c04411f1bc Add integration type to dlna_dmr (#157733)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:48:59 +01:00
Artur Pragacz 753ea023de Add integration type to ibeacon (#157734)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:48:33 +01:00
Artur Pragacz 1ca1cf59eb Add integration type to ring (#157738)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:44:09 +01:00
Artur Pragacz 5b01bb1a29 Add integration type to broadlink (#157739)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:43:19 +01:00
Artur Pragacz 15c89d24eb Add integration type to xiaomi_ble (#157740)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:21 +01:00
Artur Pragacz b26b2347e6 Add integration type to roborock (#157737)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:42:10 +01:00
Artur Pragacz 7d54103c09 Add integration type to speedtestdotnet (#157727)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:27:25 +01:00
Artur Pragacz c705a1dc4b Add integration type to rest (#157728)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:26:02 +01:00
Artur Pragacz 998bd23446 Add integration type to webostv (#157736)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:25:37 +01:00
Artur Pragacz 3a1a58d6ad Add integration type to ping (#157730)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:23:19 +01:00
Artur Pragacz f9219dd841 Add integration type to dlna_dms (#157723)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:04:17 +01:00
Artur Pragacz 402ed7e0f3 Add integration type to met (#157720)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:51:51 +01:00
epenet 7a1a5df89e Use _async_send_commands in Tuya base entity (#157716) 2025-12-02 11:50:07 +01:00
Artur Pragacz df558fc1e7 Add integration type to google_translate (#157718)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:47:30 +01:00
Erik Montnemery ec66407ef1 Improve helpers.condition.async_subscribe_platform_events (#157710) 2025-12-02 11:32:14 +01:00
Paulus Schoutsen 6b99234a43 Add integration_type to SwitchBot Bluetooth manifest (#157675)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 11:31:04 +01:00
Erik Montnemery 393be71009 Improve trigger descriptions (#157643)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-02 11:08:39 +01:00
epenet 12bc1687ec Use _async_send_commands in Tuya vacuum (#157704) 2025-12-02 11:01:51 +01:00
epenet c59b322c0a Use _async_send_commands in Tuya light (#157703) 2025-12-02 11:01:38 +01:00
Arjan e00266463d Meteo France: add new mapping "Brouillard dense givrant" (#157627) 2025-12-02 10:55:51 +01:00
dependabot[bot] cbc8a33553 Bump github/codeql-action from 4.31.5 to 4.31.6 (#157700)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 10:52:13 +01:00
Paulus Schoutsen 28582f75d4 Add integration_type to Ecowitt manifest (#157666) 2025-12-02 10:49:58 +01:00
J. Diego Rodríguez Royo 39cccd212d Bump aiohomeconnect to version 0.24.0 (#157670) 2025-12-02 10:46:37 +01:00
Brett Adams 329ea33337 Add integration_type to Teslemetry manifest (#157677) 2025-12-02 10:45:41 +01:00
Brett Adams 521733c420 Revert integration type in Tessie (#157713) 2025-12-02 10:45:21 +01:00
Brett Adams 33e9f9a0ff Add integration_type to Tesla Fleet manifest (#157679) 2025-12-02 10:44:49 +01:00
Erik Montnemery 5fda2bccbe Improve helpers.trigger.async_subscribe_platform_events (#157709) 2025-12-02 10:37:19 +01:00
Åke Strandberg ae75332656 Add program id:s and phases to new Miele WQ1000 (#157660) 2025-12-02 09:25:47 +01:00
Paulus Schoutsen b171785f96 Add integration_type to SmartThings manifest (#157673) 2025-12-02 09:17:49 +01:00
Paulus Schoutsen ff3d6783c6 Add integration_type to Konnected.io manifest (#157681) 2025-12-02 09:15:18 +01:00
cdnninja b1e579bea0 Bump pyvesync to 3.3.3 (#157697) 2025-12-02 09:14:41 +01:00
Jan Bouwhuis 87241ea051 Add read support for MQTT config entry version to 2.1 (#157623) 2025-12-02 08:02:06 +01:00
dependabot[bot] a871ec0bdf Bump home-assistant/wheels from 2025.11.0 to 2025.12.0 (#157699) 2025-12-02 07:41:44 +01:00
Copilot b8829b645a Add labs_updated event to subscription allowlist (#157552)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2025-12-02 07:35:29 +01:00
Paulus Schoutsen 5b056a83d4 Add integration_type to Motionblinds manifest (#157682)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 07:15:11 +01:00
Paulus Schoutsen 02a70123c1 Add integration_type to HomeWizard Energy manifest (#157680)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 00:04:08 -05:00
Paulus Schoutsen 5f6d2f537a Add integration_type to Tessie manifest (#157676)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 23:50:35 -05:00
Paulus Schoutsen 5e04e9f04d Add integration_type to Home Connect manifest (#157668)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 23:49:39 -05:00
Paulus Schoutsen 56515ad7b5 Add integration_type to Sonos manifest (#157674)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:03:53 +01:00
Paulus Schoutsen a1fe2bf4fa Add integration_type to HomeKit Device manifest (#157671)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:02:59 +01:00
Paulus Schoutsen b8fa8efd91 Add integration_type to Apple TV manifest (#157664)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 05:01:56 +01:00
Jesse Hills 03557b5ef2 Bump aioesphomeapi to 42.10.0 (#157678) 2025-12-01 20:59:35 -05:00
Paulus Schoutsen dafec8ce58 Add integration_type to Reolink manifest (#157672)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:58:51 -05:00
Paulus Schoutsen 6ff3f74347 Add integration_type to Nuki Bridge manifest (#157683)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:58:03 -05:00
karwosts ddd8cf7fde Fix a bad script error message (#157654) 2025-12-01 15:19:30 -05:00
TheJulianJES 1356eea52f Set Matter integration type to "hub" (#157657) 2025-12-01 15:18:55 -05:00
TheJulianJES 6188e0e39b Set ZHA integration type to "hub" (#157656) 2025-12-01 15:18:49 -05:00
Franck Nijhof c82706eaf5 Bump version to 2025.12.0b3 2025-12-01 19:26:30 +00:00
andreimoraru 07f9bec8b6 bump yt-dlp to 2025.11.12 (#157645) 2025-12-01 19:26:14 +00:00
Åke Strandberg 33d576234b Add code mappings for Miele WQ1000 (#157642) 2025-12-01 19:26:13 +00:00
Bram Kragten 9e2b4615f1 Update frontend to 20251201.0 (#157638) 2025-12-01 19:26:11 +00:00
Petro31 a46dc7e05f Reload config entry templates when labs flag automation.new_triggers_conditions is set (#157637)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-01 19:26:09 +00:00
Erik Montnemery 7dd9953345 Bump area registry to version 1.9 and sort areas (#157634) 2025-12-01 19:26:08 +00:00
Maciej Bieniek 1145026190 Bump aioshelly to version 13.22.0 (#157629) 2025-12-01 19:26:06 +00:00
Erik Montnemery d8f9574bc3 Remove cover triggers (#157621) 2025-12-01 19:26:05 +00:00
Erik Montnemery e91f8d3a81 Remove description_configured from condition and trigger translations (#157620) 2025-12-01 19:26:04 +00:00
Aidan Timson 8c0fd0565e Default area icons for new instances (#157619) 2025-12-01 19:26:02 +00:00
Paul Bottein cc620fc0f8 Fix user store not loaded on restart (#157616) 2025-12-01 19:26:00 +00:00
Erik Montnemery 5a89332680 Bump floor registry to version 1.3 and sort floors (#157614)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-01 19:25:59 +00:00
LG-ThinQ-Integration 1831c5e249 Bump thinqconnect to 1.0.9 (#157607) 2025-12-01 19:25:57 +00:00
cdnninja dddd2503ea Bump pyvesync to 3.3.2 (#157605) 2025-12-01 19:25:56 +00:00
Norbert Rittel 91ba510a1e Fix spelling of "to log in" in anglian_water (#157594) 2025-12-01 19:25:54 +00:00
Norbert Rittel 6e5e739496 Fix spelling of "to set up" in hue_ble (#157593) 2025-12-01 19:25:53 +00:00
Sanjay Govind 6b39eb069c Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-12-01 19:25:51 +00:00
Allen Porter 847c332c70 Bump google-nest-sdm to 9.1.1 (#157562) 2025-12-01 19:25:50 +00:00
J. Nick Koston 1a19f3b527 Bump aioesphomeapi to 42.9.0 (#157558) 2025-12-01 19:25:49 +00:00
Thomas55555 8110935d2d Bump google air quality api to 1.1.3 (#157555) 2025-12-01 19:25:47 +00:00
Raphael Hehl af69da94f5 Bump uiprotect to 7.31.0 (#157543) 2025-12-01 19:25:46 +00:00
Jan Bouwhuis c1cf17d4db Fix MQTT entity cannot be renamed (#157540) 2025-12-01 19:25:44 +00:00
Allen Porter 6079637909 Bump python-roborock to 3.8.4 (#157538) 2025-12-01 19:25:43 +00:00
Jordan Harvey 9268e12b20 Disable cookie quotes for Anglian Water (#157518) 2025-12-01 19:25:41 +00:00
Raphael Hehl d07993f4a4 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-12-01 19:25:40 +00:00
Allen Porter 441cb4197c Bump python-roborock to 3.8.3 (#157512) 2025-12-01 19:25:38 +00:00
J. Nick Koston d2a095588d Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-12-01 19:25:37 +00:00
Arie Catsman f2578da7db Bump pyenphase to 2.4.2 (#157500) 2025-12-01 19:25:35 +00:00
Jan Bouwhuis 22200d6804 Fix subentry ID is not updated when renaming the entity ID (#157498) 2025-12-01 19:25:34 +00:00
Maciej Bieniek 8a4e5c3a28 Remove name from Shelly RGBCCT sensors (#157492) 2025-12-01 19:25:32 +00:00
Maciej Bieniek 30f31c7d8c Remove name for Shelly gas valve (gen1) entity (#157490) 2025-12-01 19:25:30 +00:00
Maciej Bieniek 232c4255a1 Add missing string for Shelly away mode switch (#157488) 2025-12-01 19:25:28 +00:00
Petro31 236f7cd22c Ensure platform template does not appear in repair (#157486) 2025-12-01 19:25:27 +00:00
Åke Strandberg 5948ff2e31 Add loggers to senz manifest (#157479) 2025-12-01 19:25:25 +00:00
epenet 380127bc70 Fix blocking call in Tuya initialisation (#157477) 2025-12-01 19:25:23 +00:00
epenet b6a1e8251a Remove unnecessary instanciating in Tuya find_dpcode (#157473) 2025-12-01 19:25:22 +00:00
David Woodhouse c20236717c Clarify percentage_command_topic and percentage_state_topic for MQTT fan (#157460)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-12-01 19:25:21 +00:00
Artur Pragacz 1fd9feaace Provide log info for discovered flows in logger (#157454) 2025-12-01 19:25:19 +00:00
ElectricSteve 7ce072b4dc bump: youtubeaio to 2.1.1 (#157452) 2025-12-01 19:25:17 +00:00
Artur Pragacz 45aa0399c7 Add tools in default agent also in fallback pipeline (#157441) 2025-12-01 19:25:16 +00:00
Hem Bhagat d82b3871c1 Move translatable URLs out of strings.json for opentherm_gw integration (#157437) 2025-12-01 19:25:14 +00:00
Thomas55555 8f6d1162e5 Fix strings in Google Air Quality (#157297) 2025-12-01 19:21:28 +00:00
puddly dafce97341 Disable owning integrations for the entire firmware interaction process (#157082) 2025-12-01 19:21:26 +00:00
Sebastian Schneider ffd5d33bbc Support UniFi LED control for devices without RGB (#156812) 2025-12-01 19:21:24 +00:00
Aidan Timson 699fa1617d Default area icons for new instances (#157619) 2025-12-01 20:02:38 +01:00
Erik Montnemery 449f0fa5a5 Bump floor registry to version 1.3 and sort floors (#157614)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-01 19:54:43 +01:00
andreimoraru 2e008d2bb7 bump yt-dlp to 2025.11.12 (#157645) 2025-12-01 19:44:54 +01:00
Petro31 05dec2619d Ensure platform template does not appear in repair (#157486) 2025-12-01 19:38:49 +01:00
Paul Bottein 25a6778ba8 Fix user store not loaded on restart (#157616) 2025-12-01 19:37:27 +01:00
epenet f564b8cb44 Remove unnecessary instanciating in Tuya find_dpcode (#157473) 2025-12-01 19:37:06 +01:00
Erik Montnemery ce6bfdebfc Improve typing of floor registry events (#157624) 2025-12-01 19:36:50 +01:00
Bram Kragten f00a944ac1 Update frontend to 20251201.0 (#157638) 2025-12-01 19:28:18 +01:00
Petro31 3073a99ce6 Reload config entry templates when labs flag automation.new_triggers_conditions is set (#157637)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-01 19:27:30 +01:00
Erik Montnemery 8b04ce1328 Bump area registry to version 1.9 and sort areas (#157634) 2025-12-01 19:26:17 +01:00
Andrew Jackson 39f76787ab Allow multiline post in Mastodon (#157647) 2025-12-01 18:35:59 +01:00
puddly e8acced335 Disable owning integrations for the entire firmware interaction process (#157082) 2025-12-01 18:18:32 +01:00
Åke Strandberg 758a30eebc Add code mappings for Miele WQ1000 (#157642) 2025-12-01 17:28:02 +01:00
epenet faf94bea24 Use read_wrapper entity helper in Tuya (#157632) 2025-12-01 17:00:08 +01:00
epenet ff91c57228 Adjust Tuya wrapper to return a command list (#157622) 2025-12-01 16:59:26 +01:00
epenet 3d2b506997 Rename Tuya method (#157640) 2025-12-01 16:56:46 +01:00
Kevin Stillhammer d3c1c28605 Add integration fressnapf_tracker (#157480)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-01 16:48:30 +01:00
mettolen d4e1f7741d Add sensor entities to Saunum integration (#157342)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-01 16:47:00 +01:00
mettolen e713632eed Add reauth flow to Airobot integration (#157501) 2025-12-01 16:28:10 +01:00
Maciej Bieniek 060ad35ddc Bump aioshelly to version 13.22.0 (#157629) 2025-12-01 15:47:53 +01:00
Erik Montnemery 6c5dba40cd Remove cover triggers (#157621) 2025-12-01 14:09:29 +01:00
Erik Montnemery a04d595424 Remove description_configured from condition and trigger translations (#157620) 2025-12-01 12:57:07 +01:00
Lukas fe85eaf2a2 Pooldose: Add sensors for water meter (#157382) 2025-12-01 11:43:50 +01:00
Raphael Hehl 3551c4b01f Fix UniFi Protect G6 Instant speaker volume control (#157549) 2025-12-01 11:38:34 +01:00
Shay Levy e7edd51a65 Refactor Shelly number platform to use upstream set_thermostat_state (#157527)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 12:37:12 +02:00
Raphael Hehl 0c4f2326ef Use public API for UniFi Protect light brightness control (#157550) 2025-12-01 11:32:03 +01:00
dependabot[bot] 81f4456d7c Bump actions/ai-inference from 2.0.3 to 2.0.4 (#157608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:04:04 +01:00
Maciej Bieniek 2b608bf15c Remove name from Shelly RGBCCT sensors (#157492) 2025-12-01 10:54:19 +01:00
karwosts 972ed4b27f Finish removal of sensor.sun_solar_rising (#157606) 2025-12-01 09:26:54 +01:00
LG-ThinQ-Integration 23c167da1b Bump thinqconnect to 1.0.9 (#157607) 2025-12-01 09:26:01 +01:00
Jan Bouwhuis 34d6938171 Fix subentry ID is not updated when renaming the entity ID (#157498) 2025-12-01 07:55:13 +01:00
Kevin Stillhammer 4bb8590076 Revert "Force httpx client to use IPv4 for waze_travel_time" (#157596) 2025-12-01 06:35:26 +01:00
Norbert Rittel 5e0923b60d Fix spelling of "to set up" in hue_ble (#157593) 2025-12-01 06:33:18 +01:00
Norbert Rittel ad48f3c634 Fix spelling of "to log in" in anglian_water (#157594) 2025-12-01 06:32:54 +01:00
cdnninja 2bdd6854eb Bump pyvesync to 3.3.2 (#157605) 2025-11-30 23:41:46 -05:00
Lukas 0bf906911c pooldose bump to api 0.8.1 (#157591) 2025-11-30 23:49:40 +01:00
Ludovic BOUÉ 874d6f5613 Add Matter fixture for Eufy vacuum Omni E28 (#157590) 2025-11-30 21:47:31 +01:00
Raphael Hehl 43ba10eebd Add missing translations for UniFi Protect integration (#157570) 2025-11-30 17:05:05 +01:00
Sanjay Govind 64bed19805 Bump bosch-alarm-mode2 to v0.4.10 (#157564) 2025-11-30 16:02:43 +01:00
Shay Levy 6357067f0f Rename Shelly SENSORS to BLOCK_SENSORS to match naming in other platforms (#157553) 2025-11-30 12:48:35 +02:00
Thomas55555 e328ba4045 Bump google air quality api to 1.1.3 (#157555) 2025-11-30 07:17:36 +01:00
Allen Porter 332dbddce6 Bump google-nest-sdm to 9.1.1 (#157562) 2025-11-29 23:19:44 -05:00
J. Nick Koston 82d935a819 Bump aioesphomeapi to 42.9.0 (#157558) 2025-11-29 18:04:55 -06:00
Raphael Hehl 4b84998c0c Fix UFPConfigEntry type consistency in unifiprotect (#157548) 2025-11-29 17:07:44 -06:00
Raphael Hehl e10c1ebcf6 Fix UniFi Protect RTSP repair warnings when globally disabled (#157516) 2025-11-29 22:53:34 +02:00
Raphael Hehl 0174bad182 Add PARALLEL_UPDATES to UniFi Protect platforms (#157504) 2025-11-29 19:48:43 +01:00
Allen Porter d5be623684 Bump python-roborock to 3.8.4 (#157538) 2025-11-29 20:34:27 +02:00
Raphael Hehl d006b044c8 Bump uiprotect to 7.31.0 (#157543) 2025-11-29 20:33:09 +02:00
Jan Bouwhuis fdd9571623 Fix MQTT entity cannot be renamed (#157540) 2025-11-29 19:29:54 +01:00
Shay Levy 4f4c5152b9 Refactor Shelly setup to use async_setup_entry_block for block entities (#157517) 2025-11-29 18:08:12 +02:00
Denis Shulyaka b031a082cd Bump anthropic to 0.75.0 (#157491) 2025-11-29 14:35:30 +01:00
Shay Levy a1132195fd Refactor Shelly RPC event platform to use base class (#157499) 2025-11-29 13:09:32 +02:00
Jordan Harvey 708b3dc8b2 Disable cookie quotes for Anglian Water (#157518) 2025-11-29 11:52:55 +01:00
J. Nick Koston 8ae0216135 Bump ESPHome stable BLE version to 2025.11.0 (#157511) 2025-11-29 03:40:22 -06:00
David Woodhouse 1472281cd5 Clarify percentage_command_topic and percentage_state_topic for MQTT fan (#157460)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-11-29 02:47:34 -06:00
Allen Porter ceaa71d198 Bump python-roborock to 3.8.3 (#157512) 2025-11-29 09:34:22 +01:00
Arie Catsman 7f0d0c555a Bump pyenphase to 2.4.2 (#157500) 2025-11-28 21:58:57 +01:00
steaura 3b94b2491a Update bootstrap.py for grammar in slow startup error log (#157458) 2025-11-28 19:34:30 +01:00
Sebastian Schneider 8c8708d5bc Support UniFi LED control for devices without RGB (#156812) 2025-11-28 17:33:15 +01:00
Maciej Bieniek ca35102138 Remove name for Shelly gas valve (gen1) entity (#157490) 2025-11-28 15:26:17 +01:00
Maciej Bieniek 1a1b50ef1a Add missing string for Shelly away mode switch (#157488) 2025-11-28 16:07:40 +02:00
epenet 5a4d51e57a Mark config-flow-test-coverage as done in SFR Box IQS (#157485) 2025-11-28 12:46:01 +01:00
epenet 9e1bc637e2 Improve diagnostics tests in SFR Box API (#157483) 2025-11-28 11:58:33 +01:00
Joakim Plate ab879c07ca Add logbook support for args same as params for zha (#154997) 2025-11-28 11:15:49 +01:00
Hem Bhagat 488c97531e Move translatable URLs out of strings.json for opentherm_gw integration (#157437) 2025-11-28 10:45:45 +01:00
epenet 3b52c5df79 Use snapshot_platform helper in SFR Box tests (#157481) 2025-11-28 10:44:39 +01:00
Shay Levy 7f4b56104d Update Shelly utils coverage to 100% (#157478) 2025-11-28 11:32:41 +02:00
Åke Strandberg ab8135ba1a Add loggers to senz manifest (#157479) 2025-11-28 10:19:28 +01:00
epenet a88599bc09 Improve tests in SFR Box (#157444)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 10:10:38 +01:00
Manuel Stahl 45034279c8 Update pystiebeleltron to 0.2.5 (#157450) 2025-11-28 09:48:51 +01:00
Artur Pragacz 9f3dae6254 Add tools in default agent also in fallback pipeline (#157441) 2025-11-28 09:47:52 +01:00
epenet ef36d7b1e5 Fix blocking call in Tuya initialisation (#157477) 2025-11-28 09:45:28 +01:00
dependabot[bot] e5346ba017 Bump home-assistant/builder from 2025.09.0 to 2025.11.0 (#157468)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:30:37 +01:00
dependabot[bot] 68d41d2a48 Bump docker/metadata-action from 5.9.0 to 5.10.0 (#157467)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:30:22 +01:00
dependabot[bot] 00a882c20a Bump actions/ai-inference from 2.0.2 to 2.0.3 (#157466)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-28 09:28:59 +01:00
Jordan Harvey 44a6772947 Fix Anglian Water sensor setup (#157457) 2025-11-28 07:25:04 +01:00
cdnninja f874ba1355 Move device_info to attribute in vesync (#157462) 2025-11-28 07:20:50 +01:00
Allen Porter 4fc125c49a Improve Nest error message wording in test before setup (#157465) 2025-11-28 07:19:54 +01:00
Artur Pragacz 8c59196e19 Provide log info for discovered flows in logger (#157454) 2025-11-28 02:13:10 +01:00
Shay Levy 326f7f0559 Add coverage to Shelly utils (#157455) 2025-11-28 00:29:47 +02:00
ElectricSteve 11afda8c22 bump: youtubeaio to 2.1.1 (#157452) 2025-11-27 22:42:57 +01:00
StaleLoafOfBread f1ee0e4ac9 Add support for gallons per day as a unit of volume flow rate (#157394) 2025-11-27 20:42:16 +01:00
Joakim Plate 5f522e5afa Fix cancel propagation in update coordinator and config entry (#153504) 2025-11-27 19:48:45 +01:00
Thomas55555 4f6624d0aa Fix strings in Google Air Quality (#157297) 2025-11-27 19:26:33 +01:00
epenet 70990645a7 Mark config-flow as done in SFR Box IQS (#157439) 2025-11-27 19:14:13 +01:00
Andrew Jackson 2f7d74ff62 Add icons to transmission entities (#157436) 2025-11-27 18:38:32 +01:00
epenet 885667832b Add initial IQS to sfr_box (#155419) 2025-11-27 18:36:51 +01:00
Franck Nijhof bac32bc379 Bump version to 2025.12.0b2 2025-11-27 17:13:26 +00:00
Allen Porter 6344837009 Fix regression in roborock image entity naming (#157432) 2025-11-27 17:12:08 +00:00
Allen Porter 9079ff5ea8 Update roborock test typing (#157370) 2025-11-27 17:12:06 +00:00
Bram Kragten cd646aea11 Update frontend to 20251127.0 (#157431) 2025-11-27 17:09:23 +00:00
Shay Levy b3a93d9fab Fix Shelly support for button5 trigger (#157422) 2025-11-27 17:09:22 +00:00
Denis Shulyaka db98fb138b Fix Anthropic init with incorrect model (#157421) 2025-11-27 17:09:21 +00:00
Petro31 348c8bca7c Avoid custom template platform deprecations (#157415) 2025-11-27 17:09:20 +00:00
Allen Porter e30707ad5e Bump python-roborock to 3.8.1 (#157376) 2025-11-27 17:09:18 +00:00
Petro31 3fa4dcb980 Reload templates when labs flag automation.new_triggers_conditions is set (#157368) 2025-11-27 17:09:17 +00:00
Kamil Breguła 57835efc9d Fix MAC address mix-ups between WLED devices (#155491)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-27 17:09:16 +00:00
Petro31 4646929987 Avoid custom template platform deprecations (#157415) 2025-11-27 18:06:29 +01:00
Petro31 010aea952c Reload templates when labs flag automation.new_triggers_conditions is set (#157368) 2025-11-27 18:05:33 +01:00
Bram Kragten 563678dc47 Update frontend to 20251127.0 (#157431) 2025-11-27 18:05:18 +01:00
epenet a48f01f213 Raise UpdateFailed if API returns None in sfr_box (#157434) 2025-11-27 18:01:56 +01:00
Andrew Jackson 08b758b0d2 Add device info and parallel_updates to Transmission (#157423) 2025-11-27 17:37:27 +01:00
Allen Porter 4306fbea52 Fix regression in roborock image entity naming (#157432) 2025-11-27 17:36:18 +01:00
Robert Resch 6f4c479f8f Use same cosign version in build workflow (#157365) 2025-11-27 17:13:04 +01:00
Shay Levy 1d9c06264e Fix Shelly support for button5 trigger (#157422) 2025-11-27 16:38:45 +01:00
epenet d045ecaf13 Add parallel_updates to SFR Box (#157426) 2025-11-27 16:04:25 +01:00
Markus Jacobsen f7c41e694c Add media content id attribute to Bang & Olufsen (#156597) 2025-11-27 15:53:43 +01:00
Kamil Breguła 9ee7ed5cdb Fix MAC address mix-ups between WLED devices (#155491)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-11-27 15:10:32 +01:00
Denis Shulyaka 83c4e2abc9 Fix Anthropic init with incorrect model (#157421) 2025-11-27 14:16:46 +01:00
Franck Nijhof f8d5a8bc58 Bump version to 2025.12.0b1 2025-11-27 11:49:46 +00:00
epenet 3f1f8da6f5 Bump renault-api to 0.5.1 (#157411) 2025-11-27 11:48:09 +00:00
Jan Čermák 55613f56b6 Fix state classes of Ecowitt rain sensors (#157409) 2025-11-27 11:48:08 +00:00
victorigualada 3ee2a78663 Bump hass-nabucasa from 1.6.1 to 1.6.2 (#157405) 2025-11-27 11:48:06 +00:00
victorigualada 814a0c4cc9 Return early when setting cloud ai_task and conversation and not logged in to cloud (#157402) 2025-11-27 11:48:04 +00:00
starkillerOG 71b674d8f1 Bump reolink-aio to 0.16.6 (#157399) 2025-11-27 11:48:03 +00:00
Erik Montnemery c952fc5e31 Minor polish of cover trigger tests (#157397) 2025-11-27 11:48:02 +00:00
Allen Porter 8c3d40a348 Remove old roborock map storage (#157379) 2025-11-27 11:48:01 +00:00
Paulus Schoutsen 2451dfb63d Default conversation agent to store tool calls in chat log (#157377) 2025-11-27 11:48:00 +00:00
Sarah Seidman 8e5921eab6 Normalize input for Droplet pairing code (#157361) 2025-11-27 11:47:59 +00:00
Jaap Pieroen bc730da9b1 Bugfix: Essent remove average gas price today (#157317) 2025-11-27 11:47:57 +00:00
abelyliu 28b7ebea6e Fix parsing of Tuya electricity RAW values (#157039) 2025-11-27 11:47:56 +00:00
Erik Montnemery cfa447c7a9 Add climate started_cooling and started_drying triggers (#156945) 2025-11-27 11:47:55 +00:00
Erik Montnemery a7dbf551a3 Add climate started_cooling and started_drying triggers (#156945) 2025-11-27 12:41:08 +01:00
Petro31 0b2bb9f6bf Modernize template binary sensor (#157279) 2025-11-27 12:28:16 +01:00
tan-lawrence 0769163b67 Use "medium" instead of "med" for the medium fan mode in Coolmaster (#157253) 2025-11-27 12:27:49 +01:00
Robert Resch 2bb51e1146 Reduce Devcontainer docker layers (#157412) 2025-11-27 12:27:18 +01:00
Paulus Schoutsen d2248d282c Default conversation agent to store tool calls in chat log (#157377) 2025-11-27 12:27:03 +01:00
Jan Čermák 8fe79a88ca Fix state classes of Ecowitt rain sensors (#157409) 2025-11-27 12:24:28 +01:00
Jaap Pieroen 7a328539b2 Bugfix: Essent remove average gas price today (#157317) 2025-11-27 12:24:07 +01:00
abelyliu ec69efee4d Fix parsing of Tuya electricity RAW values (#157039) 2025-11-27 12:23:33 +01:00
Shay Levy dbcde549d4 Update Shelly coordinator coverage to 100% (#157380) 2025-11-27 12:22:19 +01:00
Michael 988355e138 Add tests for the switch platform to the AdGuard Home integration (#157105) 2025-11-27 12:21:23 +01:00
victorigualada 7711eac607 Return early when setting cloud ai_task and conversation and not logged in to cloud (#157402) 2025-11-27 12:20:42 +01:00
Denis Shulyaka 32fe53cceb Add anthropic model to the device info (#157413) 2025-11-27 12:16:05 +01:00
Andrew Jackson 3a65d3c0dc Add tests to Transmission (#157355) 2025-11-27 12:15:10 +01:00
epenet 7fe26223ac Bump renault-api to 0.5.1 (#157411) 2025-11-27 12:06:57 +01:00
victorigualada 7e8496afb2 Bump hass-nabucasa from 1.6.1 to 1.6.2 (#157405) 2025-11-27 11:40:50 +01:00
Paulus Schoutsen 2ec5190243 Install requirements_test_all in dev (#157392) 2025-11-27 10:30:50 +01:00
Erik Montnemery a706db8fdb Minor polish of cover trigger tests (#157397) 2025-11-27 09:57:03 +01:00
starkillerOG a00923c48b Bump reolink-aio to 0.16.6 (#157399) 2025-11-27 09:53:25 +01:00
Sarah Seidman 7480d59f0f Normalize input for Droplet pairing code (#157361) 2025-11-27 08:36:30 +01:00
Erik Montnemery 4c8d9ed401 Adjust type hints in sensor group (#157373) 2025-11-27 08:34:16 +01:00
Lukas eef10c59db Pooldose bump api 0.8.0 (new) (#157381) 2025-11-27 08:33:32 +01:00
dependabot[bot] a1a1f8dd77 Bump docker/metadata-action from 5.5.1 to 5.9.0 (#157395) 2025-11-27 07:26:58 +01:00
dependabot[bot] c75a5c5151 Bump docker/setup-buildx-action from 3.5.0 to 3.11.1 (#157396) 2025-11-27 07:25:16 +01:00
Allen Porter cdaaa2bd8f Update fitbit to use new asyncio client library for device list (#157308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-27 00:23:49 -05:00
Allen Porter bd84dac8fb Update roborock test typing (#157370) 2025-11-27 00:21:48 -05:00
Allen Porter 42cbeca5b0 Remove old roborock map storage (#157379) 2025-11-27 00:21:04 -05:00
Allen Porter ad0a498d10 Bump python-roborock to 3.8.1 (#157376) 2025-11-26 16:12:19 -08:00
Jan Bouwhuis 973405822b Move translatable URL out of strings.json for knx integration (#155244) 2025-11-26 23:09:59 +01:00
Franck Nijhof b883d2f519 Bump version to 2026.1.0dev0 2025-11-26 17:15:29 +00:00
Franck Nijhof f64c870e42 Bump version to 2025.12.0b0 2025-11-26 17:13:42 +00:00
Christopher Fenner 4654d6de87 Filter devices based on online status in ViCare integration (#157287) 2025-11-26 18:00:52 +01:00
Ludovic BOUÉ 990c8cd4e6 Add Matter Window covering operational status (#156066)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-11-26 18:00:13 +01:00
Raphael Hehl f8c76f42e3 Add session clearing on config entry removal for UniFi Protect integration (#157360)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:59:49 +01:00
Erik Montnemery 21d914c8ca Disable experimental conditions according to labs flag setting (#157345) 2025-11-26 17:59:12 +01:00
Erik Montnemery ec77add1a6 Reload scripts when labs flag automation.new_triggers_conditions is set (#157348) 2025-11-26 17:53:38 +01:00
Erik Montnemery ef3b7dfd1d Reload automations when labs flag automation.new_triggers_conditions is set (#157347) 2025-11-26 17:45:25 +01:00
Robert Resch 51241d963d Bump deebot-client to 16.4.0 (#157358) 2025-11-26 17:28:41 +01:00
Joost Lekkerkerker 7c48e6e046 Delete leftover SmartThings smartapps (#157188) 2025-11-26 17:14:36 +01:00
Bram Kragten 38d8da4279 Update frontend to 20251126.0 (#157352) 2025-11-26 17:13:25 +01:00
Raphael Hehl 3396a72fa8 Bump uiprotect to version 7.29.0 (#157354) 2025-11-26 17:04:38 +01:00
Erik Montnemery 2d26ab390e Save device registry store in worker thread (#157351) 2025-11-26 17:02:10 +01:00
Thomas55555 1bf5bc9323 Bump google air quality api to 1.1.2 (#157337) 2025-11-26 16:04:01 +01:00
Erik Montnemery 87ea96a3e0 Save entity registry store in worker thread (#157274) 2025-11-26 16:03:14 +01:00
Jan Čermák e3cf65510b Update Home Assistant base image to 2025.11.3 (#157346) 2025-11-26 15:15:08 +01:00
Robert Resch f69fce68d6 Use buildx imagetools to copy base image to docker.io and enable provenance (#157341)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-11-26 15:12:32 +01:00
Abílio Costa f758cfa82f Add get_conditions_for_target websocket command (#157344)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 14:08:56 +00:00
Artur Pragacz 9c7a928b29 Add get encryption key websocket to esphome (#154058) 2025-11-26 14:41:19 +01:00
Petro31 405a9948a2 Deprecate legacy and undocumented template entity configurations (#155355) 2025-11-26 14:30:06 +01:00
Oscar 0e3bab3ce4 Energyid bugfix (#157343) 2025-11-26 14:29:28 +01:00
Erik Montnemery 4900d25ac8 Disable experimental triggers according to labs flag setting (#157320) 2025-11-26 14:27:05 +01:00
Shay Levy ea10cdb4b0 Remove Shelly redundant device entry check for sleepy devices (#157333) 2025-11-26 14:54:51 +02:00
Oscar 6baf77d256 Energyid integration (#138206)
Co-authored-by: Jan Pecinovsky <jan.pecinovsky@energieid.be>
Co-authored-by: Jan Pecinovsky <janpecinovsky@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 13:38:57 +01:00
Artur Pragacz 13bc0ebed8 Remove incorrect after dependency in music assistant (#157339) 2025-11-26 13:38:18 +01:00
Marcel van der Veldt 611af9c832 Add support for authentication to the Music Assistant integration (#157257)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2025-11-26 13:34:26 +01:00
Abílio Costa c2b7a63dd9 Add get_services_for_target websocket command (#157334) 2025-11-26 12:30:51 +00:00
Robert Resch 550716a753 Optimize docker container publish job (#157076)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 13:24:45 +01:00
TheJulianJES 56a71e6798 Add missing ZHA button strings (#157335) 2025-11-26 13:21:17 +01:00
Simone Chemelli 80ec51c56b Bump aioamazondevices to 10.0.0 (#157331) 2025-11-26 13:01:40 +01:00
Allen Porter ea651c4a22 Overhaul Roborock integration to use new devices based API (#154837) 2025-11-26 12:52:09 +01:00
Bram Kragten ff40ce419e Add context support for triggers.yaml (#156531) 2025-11-26 12:50:17 +01:00
OzGav d95308719c Qualify Music Assistant to Bronze Quality Level (#155260)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-11-26 12:42:21 +01:00
Petro31 f4fb95ee43 Modernize template light (#156469) 2025-11-26 12:13:27 +01:00
Simone Chemelli 14d95cc86b Temporary raise scan interval for Alexa Devices (#157326) 2025-11-26 11:29:57 +01:00
Joost Lekkerkerker 4257435975 Add Matter info to SmartThings Device (#157321) 2025-11-26 11:28:49 +01:00
Abílio Costa a6aab088fb Add get_triggers_for_target websocket command (#156778) 2025-11-26 11:05:03 +01:00
Aarni Koskela 655a63c104 Add clamp/wrap/remap to template math functions (#154537) 2025-11-26 11:00:12 +01:00
Robert Resch a2ade413c2 Fix aarch64 image download by specifing the platform (#157316)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 10:02:35 +01:00
Jan Bouwhuis 10299b2ef4 Add description placeholders to service translation strings (#154984)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-11-26 09:54:22 +01:00
David Rapan 26444d8d34 Move Shelly sensor translation logic to base class (#157129)
Signed-off-by: David Rapan <david@rapan.cz>
2025-11-26 10:43:16 +02:00
Lukas 554c122a37 Add switch platform to PoolDose integration (#157296) 2025-11-26 09:09:35 +01:00
puddly 1c0dd02a7c Abort USB discovery flows on device unplug (#156303) 2025-11-26 09:00:41 +01:00
1001 changed files with 55838 additions and 15172 deletions
+1
View File
@@ -13,6 +13,7 @@ core: &core
# Our base platforms, that are used by other integrations
base_platforms: &base_platforms
- homeassistant/components/ai_task/**
- homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/**
-1
View File
@@ -27,7 +27,6 @@
"charliermarsh.ruff",
"ms-python.pylint",
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github",
+97 -83
View File
@@ -14,7 +14,9 @@ env:
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
BASE_IMAGE_VERSION: "2025.11.0"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2025.11.3"
ARCHITECTURES: '["amd64", "aarch64"]'
jobs:
init:
@@ -25,9 +27,10 @@ jobs:
version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -85,7 +88,7 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: ["amd64", "aarch64"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
@@ -93,7 +96,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -187,7 +190,8 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
- &install_cosign
name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.5.3"
@@ -269,7 +273,7 @@ jobs:
- green
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set build additional args
run: |
@@ -291,7 +295,7 @@ jobs:
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.09.0
uses: home-assistant/builder@2025.11.0
with:
args: |
$BUILD_ARGS \
@@ -307,7 +311,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -350,13 +354,7 @@ jobs:
matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.2.3"
- *install_cosign
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -366,88 +364,104 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Meta Image
- name: Verify architecture image signatures
shell: bash
run: |
export DOCKER_CLI_EXPERIMENTAL=enabled
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Verifying ${arch} image signature..."
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
echo "✓ All images verified successfully"
function create_manifest() {
local tag_l=${1}
local tag_r=${2}
local registry=${{ matrix.registry }}
# Generate all Docker tags based on version string
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
# Examples:
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
tags: |
type=raw,value=${{ needs.init.outputs.version }},priority=9999
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
docker manifest create "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
shell: bash
run: |
# Use imagetools to copy image blobs directly between registries
# This preserves provenance/attestations and seems to be much faster than pull/push
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
- name: Create and push multi-arch manifests
shell: bash
run: |
# Build list of architecture images dynamically
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
ARCH_IMAGES=()
for arch in $ARCHS; do
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
done
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
cosign sign --yes "${registry}/home-assistant:${tag_l}"
}
# Build list of all tags for single manifest creation
# Note: Using sep-tags=',' in metadata-action for easier parsing
TAG_ARGS=()
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
for tag in "${TAGS[@]}"; do
TAG_ARGS+=("--tag" "${tag}")
done
function validate_image() {
local image=${1}
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
echo "Invalid signature!"
exit 1
fi
}
# Create manifest with ALL tags in a single operation (much faster!)
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
function push_dockerhub() {
local image=${1}
local tag=${2}
# Sign each tag separately (signing requires individual tag names)
echo "Signing all tags..."
for tag in "${TAGS[@]}"; do
echo "Signing ${tag}"
cosign sign --yes "${tag}"
done
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
docker push "docker.io/homeassistant/${image}:${tag}"
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
}
# Pull images from github container registry and verify signature
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
# Upload images to dockerhub
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
fi
# Create version tag
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
create_manifest "dev" "${{ needs.init.outputs.version }}"
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
create_manifest "beta" "${{ needs.init.outputs.version }}"
create_manifest "rc" "${{ needs.init.outputs.version }}"
else
create_manifest "stable" "${{ needs.init.outputs.version }}"
create_manifest "latest" "${{ needs.init.outputs.version }}"
create_manifest "beta" "${{ needs.init.outputs.version }}"
create_manifest "rc" "${{ needs.init.outputs.version }}"
# Create series version tag (e.g. 2021.6)
v="${{ needs.init.outputs.version }}"
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
fi
echo "All manifests created and signed successfully"
build_python:
name: Build PyPi package
@@ -460,7 +474,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -505,7 +519,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+6 -6
View File
@@ -40,9 +40,9 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.12"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
HA_SHORT_VERSION: "2026.1"
DEFAULT_PYTHON: "3.13.11"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -99,7 +99,7 @@ jobs:
steps:
- &checkout
name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -1188,7 +1188,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
flags: full-suite
@@ -1313,7 +1313,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+3 -3
View File
@@ -21,14 +21,14 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
category: "/language:python"
@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt: |
@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o-mini
system-prompt: |
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- &checkout
name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
@@ -136,7 +136,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: &home-assistant-wheels home-assistant/wheels@6066c17a2a4aafcf7bdfeae01717f63adfcdba98 # 2025.11.0
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+1
View File
@@ -187,6 +187,7 @@ homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
homeassistant.components.energenie_power_sockets.*
homeassistant.components.energy.*
homeassistant.components.energyid.*
homeassistant.components.energyzero.*
homeassistant.components.enigma2.*
homeassistant.components.enphase_envoy.*
Generated
+16 -3
View File
@@ -73,6 +73,8 @@ build.json @home-assistant/supervisor
/tests/components/airobot/ @mettolen
/homeassistant/components/airos/ @CoMPaTech
/tests/components/airos/ @CoMPaTech
/homeassistant/components/airpatrol/ @antondalgren
/tests/components/airpatrol/ @antondalgren
/homeassistant/components/airq/ @Sibgatulin @dl2080
/tests/components/airq/ @Sibgatulin @dl2080
/homeassistant/components/airthings/ @danielhiversen @LaStrada
@@ -418,6 +420,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/efergy/ @tkdrob
/tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt
/homeassistant/components/egauge/ @neggert
/tests/components/egauge/ @neggert
/homeassistant/components/eheimdigital/ @autinerd
/tests/components/eheimdigital/ @autinerd
/homeassistant/components/ekeybionyx/ @richardpolzer
@@ -452,13 +456,15 @@ build.json @home-assistant/supervisor
/tests/components/energenie_power_sockets/ @gnumpi
/homeassistant/components/energy/ @home-assistant/core
/tests/components/energy/ @home-assistant/core
/homeassistant/components/energyid/ @JrtPec @Molier
/tests/components/energyid/ @JrtPec @Molier
/homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @autinerd
/tests/components/enigma2/ @autinerd
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 @roberty99
@@ -537,6 +543,8 @@ build.json @home-assistant/supervisor
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415
/homeassistant/components/fressnapf_tracker/ @eifinger
/tests/components/fressnapf_tracker/ @eifinger
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
@@ -567,6 +575,8 @@ build.json @home-assistant/supervisor
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -1352,8 +1362,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
/homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter
@@ -1759,6 +1769,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
/tests/components/vivotek/ @HarlemSquirrel
/homeassistant/components/vizio/ @raman325
/tests/components/vizio/ @raman325
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
@@ -1798,6 +1809,8 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/web_rtc/ @home-assistant/core
/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core
+8 -11
View File
@@ -35,25 +35,22 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
USER vscode
COPY .python-version ./
RUN uv python install
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
RUN uv venv $VIRTUAL_ENV
RUN --mount=type=bind,source=.python-version,target=.python-version \
uv python install \
&& uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /tmp
# Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
&& uv pip install -e ~/hass-release/
# Install Python dependencies from requirements
COPY requirements.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
RUN uv pip install -r requirements.txt
COPY requirements_test.txt requirements_test_pre_commit.txt ./
RUN uv pip install -r requirements_test.txt
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
uv pip install -r requirements.txt -r requirements_test.txt
WORKDIR /workspaces
+2
View File
@@ -7,6 +7,7 @@ from typing import Any, Final
from homeassistant.const import (
EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE,
EVENT_LABS_UPDATED,
EVENT_LOVELACE_UPDATED,
EVENT_PANELS_UPDATED,
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
@@ -45,6 +46,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED,
EVENT_LABEL_REGISTRY_UPDATED,
EVENT_LABS_UPDATED,
EVENT_CATEGORY_REGISTRY_UPDATED,
EVENT_FLOOR_REGISTRY_UPDATED,
}
+1 -1
View File
@@ -1000,7 +1000,7 @@ class _WatchPendingSetups:
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
_LOGGER.warning(
"Waiting on integrations to complete setup: %s",
"Waiting for integrations to complete setup: %s",
self._setup_started,
)
+4 -4
View File
@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),
+1 -1
View File
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any
@@ -174,6 +175,56 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
# Combine existing data with new password
data = {
CONF_HOST: reauth_entry.data[CONF_HOST],
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
try:
await validate_input(self.hass, data)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
description_placeholders={
"username": reauth_entry.data[CONF_USERNAME],
"host": reauth_entry.data[CONF_HOST],
},
errors=errors,
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
@@ -11,6 +11,7 @@ from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -53,7 +54,15 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
try:
status = await self.client.get_statuses()
settings = await self.client.get_settings()
except (AirobotAuthError, AirobotConnectionError) as err:
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
except AirobotAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_failed",
) from err
except AirobotConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="connection_failed",
) from err
return AirobotData(status=status, settings=settings)
@@ -0,0 +1,38 @@
"""Diagnostics support for Airobot."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry
TO_REDACT_CONFIG = [CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirobotConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
# Build device capabilities info
device_capabilities = None
if coordinator.data:
device_capabilities = {
"has_floor_sensor": coordinator.data.status.has_floor_sensor,
"has_co2_sensor": coordinator.data.status.has_co2_sensor,
"hw_version": coordinator.data.status.hw_version,
"fw_version": coordinator.data.status.fw_version,
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_CONFIG),
"device_capabilities": device_capabilities,
"status": asdict(coordinator.data.status) if coordinator.data else None,
"settings": asdict(coordinator.data.settings) if coordinator.data else None,
}
@@ -12,6 +12,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairobotrest"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pyairobotrest==0.1.0"]
}
@@ -34,17 +34,17 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
@@ -54,8 +54,8 @@ rules:
comment: Single device integration, no dynamic device discovery needed.
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: todo
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
+134
View File
@@ -0,0 +1,134 @@
"""Sensor platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from pyairobotrest.models import ThermostatStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import AirobotConfigEntry
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotSensorEntityDescription(SensorEntityDescription):
"""Describes Airobot sensor entity."""
value_fn: Callable[[ThermostatStatus], StateType]
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
AirobotSensorEntityDescription(
key="air_temperature",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_air,
),
AirobotSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.hum_air,
),
AirobotSensorEntityDescription(
key="floor_temperature",
translation_key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_floor,
supported_fn=lambda status: status.has_floor_sensor,
),
AirobotSensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.co2,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.aqi,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="heating_uptime",
translation_key="heating_uptime",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.heating_uptime,
entity_registry_enabled_default=False,
),
AirobotSensorEntityDescription(
key="errors",
translation_key="errors",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.errors,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotSensor(coordinator, description)
for description in SENSOR_TYPES
if description.supported_fn(coordinator.data.status)
)
class AirobotSensor(AirobotEntity, SensorEntity):
"""Representation of an Airobot sensor."""
entity_description: AirobotSensorEntityDescription
def __init__(
self,
coordinator,
description: AirobotSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.status)
+38 -3
View File
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -14,15 +15,24 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "The thermostat password."
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
},
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
},
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"username": "Device ID"
},
"data_description": {
"host": "The hostname or IP address of your Airobot thermostat.",
@@ -33,7 +43,32 @@
}
}
},
"entity": {
"sensor": {
"air_temperature": {
"name": "Air temperature"
},
"device_uptime": {
"name": "Device uptime"
},
"errors": {
"name": "Error count"
},
"floor_temperature": {
"name": "Floor temperature"
},
"heating_uptime": {
"name": "Heating uptime"
}
}
},
"exceptions": {
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."
},
"connection_failed": {
"message": "Failed to communicate with device."
},
"set_preset_mode_failed": {
"message": "Failed to set preset mode to {preset_mode}."
},
@@ -0,0 +1,24 @@
"""The AirPatrol integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
from .coordinator import AirPatrolConfigEntry, AirPatrolDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
"""Set up AirPatrol from a config entry."""
coordinator = AirPatrolDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,208 @@
"""Climate platform for AirPatrol integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
SWING_OFF,
SWING_ON,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirPatrolConfigEntry
from .coordinator import AirPatrolDataUpdateCoordinator
from .entity import AirPatrolEntity
PARALLEL_UPDATES = 0
AP_TO_HA_HVAC_MODES = {
"heat": HVACMode.HEAT,
"cool": HVACMode.COOL,
"off": HVACMode.OFF,
}
HA_TO_AP_HVAC_MODES = {value: key for key, value in AP_TO_HA_HVAC_MODES.items()}
AP_TO_HA_FAN_MODES = {
"min": FAN_LOW,
"max": FAN_HIGH,
"auto": FAN_AUTO,
}
HA_TO_AP_FAN_MODES = {value: key for key, value in AP_TO_HA_FAN_MODES.items()}
AP_TO_HA_SWING_MODES = {
"on": SWING_ON,
"off": SWING_OFF,
}
HA_TO_AP_SWING_MODES = {value: key for key, value in AP_TO_HA_SWING_MODES.items()}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirPatrolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AirPatrol climate entities."""
coordinator = config_entry.runtime_data
units = coordinator.data
async_add_entities(
AirPatrolClimate(coordinator, unit_id)
for unit_id, unit in units.items()
if "climate" in unit
)
class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
"""AirPatrol climate entity."""
_attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
_attr_fan_modes = [FAN_LOW, FAN_HIGH, FAN_AUTO]
_attr_swing_modes = [SWING_ON, SWING_OFF]
_attr_min_temp = 16.0
_attr_max_temp = 30.0
def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator, unit_id)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data."""
return self.device_data.get("climate") or {}
@property
def params(self) -> dict[str, Any]:
"""Return the current parameters for the climate entity."""
return self.climate_data.get("ParametersData") or {}
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and bool(self.climate_data)
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
if humidity := self.climate_data.get("RoomHumidity"):
return float(humidity)
return None
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if temp := self.climate_data.get("RoomTemp"):
return float(temp)
return None
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
if temp := self.params.get("PumpTemp"):
return float(temp)
return None
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
pump_power = self.params.get("PumpPower")
pump_mode = self.params.get("PumpMode")
if pump_power and pump_power == "on" and pump_mode:
return AP_TO_HA_HVAC_MODES.get(pump_mode)
return HVACMode.OFF
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_speed = self.params.get("FanSpeed")
if fan_speed:
return AP_TO_HA_FAN_MODES.get(fan_speed)
return None
@property
def swing_mode(self) -> str | None:
"""Return the current swing mode."""
swing = self.params.get("Swing")
if swing:
return AP_TO_HA_SWING_MODES.get(swing)
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
params = self.params.copy()
if ATTR_TEMPERATURE in kwargs:
temp = kwargs[ATTR_TEMPERATURE]
params["PumpTemp"] = f"{temp:.3f}"
await self._async_set_params(params)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
params = self.params.copy()
if hvac_mode == HVACMode.OFF:
params["PumpPower"] = "off"
else:
params["PumpPower"] = "on"
params["PumpMode"] = HA_TO_AP_HVAC_MODES.get(hvac_mode)
await self._async_set_params(params)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
params = self.params.copy()
params["FanSpeed"] = HA_TO_AP_FAN_MODES.get(fan_mode)
await self._async_set_params(params)
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing mode."""
params = self.params.copy()
params["Swing"] = HA_TO_AP_SWING_MODES.get(swing_mode)
await self._async_set_params(params)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
params = self.params.copy()
if mode := AP_TO_HA_HVAC_MODES.get(params["PumpMode"]):
await self.async_set_hvac_mode(mode)
async def async_turn_off(self) -> None:
"""Turn the entity off."""
await self.async_set_hvac_mode(HVACMode.OFF)
async def _async_set_params(self, params: dict[str, Any]) -> None:
"""Set the unit to dry mode."""
new_climate_data = self.climate_data.copy()
new_climate_data["ParametersData"] = params
await self.coordinator.api.set_unit_climate_data(
self._unit_id, new_climate_data
)
await self.coordinator.async_request_refresh()
@@ -0,0 +1,111 @@
"""Config flow for the AirPatrol integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): TextSelector(
TextSelectorConfig(
type=TextSelectorType.EMAIL,
autocomplete="email",
)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
}
)
async def validate_api(
hass: HomeAssistant, user_input: dict[str, str]
) -> tuple[str | None, str | None, dict[str, str]]:
"""Validate the API connection."""
errors: dict[str, str] = {}
session = async_get_clientsession(hass)
access_token = None
unique_id = None
try:
api = await AirPatrolAPI.authenticate(
session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except AirPatrolAuthenticationError:
errors["base"] = "invalid_auth"
except AirPatrolError:
errors["base"] = "cannot_connect"
else:
access_token = api.get_access_token()
unique_id = api.get_unique_id()
return (access_token, unique_id, errors)
class AirPatrolConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AirPatrol."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
access_token, unique_id, errors = await validate_api(self.hass, user_input)
if access_token and unique_id:
user_input[CONF_ACCESS_TOKEN] = access_token
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication with new credentials."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthentication confirmation."""
errors: dict[str, str] = {}
if user_input:
access_token, unique_id, errors = await validate_api(self.hass, user_input)
if access_token and unique_id:
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_mismatch()
user_input[CONF_ACCESS_TOKEN] = access_token
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm", data_schema=DATA_SCHEMA, errors=errors
)
@@ -0,0 +1,16 @@
"""Constants for the AirPatrol integration."""
from datetime import timedelta
import logging
from airpatrol.api import AirPatrolAuthenticationError, AirPatrolError
from homeassistant.const import Platform
DOMAIN = "airpatrol"
LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.CLIMATE]
SCAN_INTERVAL = timedelta(minutes=1)
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
@@ -0,0 +1,100 @@
"""Data update coordinator for AirPatrol."""
from __future__ import annotations
from typing import Any
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type AirPatrolConfigEntry = ConfigEntry[AirPatrolDataUpdateCoordinator]
class AirPatrolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
"""Class to manage fetching AirPatrol data."""
config_entry: AirPatrolConfigEntry
api: AirPatrolAPI
def __init__(self, hass: HomeAssistant, config_entry: AirPatrolConfigEntry) -> None:
"""Initialize."""
super().__init__(
hass,
LOGGER,
name=f"{DOMAIN.capitalize()} {config_entry.title}",
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
)
async def _async_setup(self) -> None:
try:
await self._setup_client()
except AirPatrolError as api_err:
raise UpdateFailed(
f"Error communicating with AirPatrol API: {api_err}"
) from api_err
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Update unit data from AirPatrol API."""
return {unit_data["unit_id"]: unit_data for unit_data in await self._get_data()}
async def _get_data(self, retry: bool = False) -> list[dict[str, Any]]:
"""Fetch data from API."""
try:
return await self.api.get_data()
except AirPatrolAuthenticationError as auth_err:
if retry:
raise ConfigEntryAuthFailed(
"Authentication with AirPatrol failed"
) from auth_err
await self._update_token()
return await self._get_data(retry=True)
except AirPatrolError as err:
raise UpdateFailed(
f"Error communicating with AirPatrol API: {err}"
) from err
async def _update_token(self) -> None:
"""Refresh the AirPatrol API client and update the access token."""
session = async_get_clientsession(self.hass)
try:
self.api = await AirPatrolAPI.authenticate(
session,
self.config_entry.data[CONF_EMAIL],
self.config_entry.data[CONF_PASSWORD],
)
except AirPatrolAuthenticationError as auth_err:
raise ConfigEntryAuthFailed(
"Authentication with AirPatrol failed"
) from auth_err
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONF_ACCESS_TOKEN: self.api.get_access_token(),
},
)
async def _setup_client(self) -> None:
"""Set up the AirPatrol API client from stored access_token."""
session = async_get_clientsession(self.hass)
api = AirPatrolAPI(
session,
self.config_entry.data[CONF_ACCESS_TOKEN],
self.config_entry.unique_id,
)
try:
await api.get_data()
except AirPatrolAuthenticationError:
await self._update_token()
self.api = api
@@ -0,0 +1,44 @@
"""Base entity for AirPatrol integration."""
from __future__ import annotations
from typing import Any
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AirPatrolDataUpdateCoordinator
class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
"""Base entity for AirPatrol devices."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
) -> None:
"""Initialize the AirPatrol entity."""
super().__init__(coordinator)
self._unit_id = unit_id
device = coordinator.data[unit_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unit_id)},
name=device["name"],
manufacturer=device["manufacturer"],
model=device["model"],
serial_number=device["hwid"],
)
@property
def device_data(self) -> dict[str, Any]:
"""Return the device data."""
return self.coordinator.data[self._unit_id]
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._unit_id in self.coordinator.data
@@ -0,0 +1,11 @@
{
"domain": "airpatrol",
"name": "AirPatrol",
"codeowners": ["@antondalgren"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airpatrol",
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["airpatrol==0.1.0"]
}
@@ -0,0 +1,65 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not provide custom actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities doesn't subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
@@ -0,0 +1,38 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "Login credentials do not match the configured account"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::airpatrol::config::step::user::data_description::email%]",
"password": "[%key:component::airpatrol::config::step::user::data_description::password%]"
},
"description": "Reauthenticate with AirPatrol"
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Your AirPatrol email address",
"password": "Your AirPatrol password"
},
"description": "Connect to AirPatrol"
}
}
}
}
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.2"]
"requirements": ["aioairzone==1.0.4"]
}
@@ -159,81 +159,74 @@
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers when an alarm is armed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed::description%]",
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed"
"name": "Alarm armed"
},
"armed_away": {
"description": "Triggers when an alarm is armed away.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_away::description%]",
"description": "Triggers after one or more alarms become armed in away mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed away"
"name": "Alarm armed away"
},
"armed_home": {
"description": "Triggers when an alarm is armed home.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_home::description%]",
"description": "Triggers after one or more alarms become armed in home mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed home"
"name": "Alarm armed home"
},
"armed_night": {
"description": "Triggers when an alarm is armed night.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_night::description%]",
"description": "Triggers after one or more alarms become armed in night mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed night"
"name": "Alarm armed night"
},
"armed_vacation": {
"description": "Triggers when an alarm is armed vacation.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_vacation::description%]",
"description": "Triggers after one or more alarms become armed in vacation mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed vacation"
"name": "Alarm armed vacation"
},
"disarmed": {
"description": "Triggers when an alarm is disarmed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::disarmed::description%]",
"description": "Triggers after one or more alarms become disarmed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is disarmed"
"name": "Alarm disarmed"
},
"triggered": {
"description": "Triggers when an alarm is triggered.",
"description_configured": "[%key:component::alarm_control_panel::triggers::triggered::description%]",
"description": "Triggers after one or more alarms become triggered.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is triggered"
"name": "Alarm triggered"
}
}
}
@@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
SCAN_INTERVAL = 30
SCAN_INTERVAL = 300
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
@@ -45,7 +45,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
config_entry=entry,
update_interval=timedelta(seconds=SCAN_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=30, immediate=False
hass, _LOGGER, cooldown=SCAN_INTERVAL, immediate=False
),
)
self.api = AmazonEchoApi(
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==9.0.3"]
"requirements": ["aioamazondevices==10.0.0"]
}
@@ -2,6 +2,7 @@
from __future__ import annotations
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
@@ -18,7 +19,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
from .coordinator import AnglianWaterConfigEntry, AnglianWaterUpdateCoordinator
@@ -33,9 +34,11 @@ async def async_setup_entry(
auth = MSOB2CAuth(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
session=async_create_clientsession(
hass,
cookie_jar=CookieJar(quote_cookie=False),
),
refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
)
try:
await auth.send_refresh_request()
@@ -45,7 +48,7 @@ async def async_setup_entry(
_aw = AnglianWater(authenticator=auth)
try:
await _aw.validate_smart_meter()
await _aw.validate_smart_meter(entry.data[CONF_ACCOUNT_NUMBER])
except SmartMeterUnavailableError as err:
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
@@ -7,7 +7,7 @@ from typing import Any
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
from pyanglianwater.auth import BaseAuth, MSOB2CAuth
from pyanglianwater.auth import MSOB2CAuth
from pyanglianwater.exceptions import (
InvalidAccountIdError,
SelfAssertedError,
@@ -30,11 +30,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
async def validate_credentials(
auth: MSOB2CAuth, account_number: str
) -> str | MSOB2CAuth:
"""Validate the provided credentials."""
try:
await auth.send_login_request()
@@ -45,7 +48,7 @@ async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
return "unknown"
_aw = AnglianWater(authenticator=auth)
try:
await _aw.validate_smart_meter()
await _aw.validate_smart_meter(account_number)
except (InvalidAccountIdError, SmartMeterUnavailableError):
return "smart_meter_unavailable"
return auth
@@ -68,35 +71,21 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
)
),
user_input[CONF_ACCOUNT_NUMBER],
)
if isinstance(validation_response, BaseAuth):
account_number = (
user_input.get(CONF_ACCOUNT_NUMBER)
or validation_response.account_number
)
await self.async_set_unique_id(account_number)
if isinstance(validation_response, str):
errors["base"] = validation_response
else:
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account_number,
title=user_input[CONF_ACCOUNT_NUMBER],
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
},
)
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
@@ -44,6 +44,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None:
"""Update data from Anglian Water's API."""
try:
return await self.api.update()
return await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
raise UpdateFailed from err
@@ -18,17 +18,21 @@ _LOGGER = logging.getLogger(__name__)
class AnglianWaterEntity(CoordinatorEntity[AnglianWaterUpdateCoordinator]):
"""Defines a Anglian Water entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AnglianWaterUpdateCoordinator,
smart_meter: SmartMeter,
key: str,
) -> None:
"""Initialize Anglian Water entity."""
super().__init__(coordinator)
self.smart_meter = smart_meter
self._attr_unique_id = f"{smart_meter.serial_number}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, smart_meter.serial_number)},
name="Smart Water Meter",
name=smart_meter.serial_number,
manufacturer="Anglian Water",
serial_number=smart_meter.serial_number,
)
@@ -5,6 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==2.1.0"]
"requirements": ["pyanglianwater==3.0.0"]
}
@@ -108,9 +108,8 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
description: AnglianWaterSensorEntityDescription,
) -> None:
"""Initialize Anglian Water sensor."""
super().__init__(coordinator, smart_meter)
super().__init__(coordinator, smart_meter, description.key)
self.entity_description = description
self._attr_unique_id = f"{smart_meter.serial_number}_{description.key}"
@property
def native_value(self) -> float | None:
@@ -19,7 +19,7 @@
"data_description": {
"account_number": "Your account number found on your latest bill.",
"password": "Your password",
"username": "Username or email used to login to the Anglian Water website."
"username": "Username or email used to log in to the Anglian Water website."
},
"description": "Enter your Anglian Water account credentials to connect to Home Assistant."
}
@@ -17,7 +17,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.typing import ConfigType
from .const import CONF_CHAT_MODEL, DEFAULT, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -37,14 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
try:
# Use model from first conversation subentry for validation
subentries = list(entry.subentries.values())
if subentries:
model_id = subentries[0].data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
else:
model_id = DEFAULT[CONF_CHAT_MODEL]
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
await client.models.list(timeout=10.0)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
@@ -421,6 +421,8 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,
+1 -1
View File
@@ -583,7 +583,7 @@ class AnthropicBaseLLMEntity(Entity):
identifiers={(DOMAIN, subentry.subentry_id)},
name=subentry.title,
manufacturer="Anthropic",
model="Claude",
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
entry_type=dr.DeviceEntryType.SERVICE,
)
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.73.0"]
"requirements": ["anthropic==0.75.0"]
}
@@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["zeroconf"],
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
@@ -3,8 +3,9 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
import logging
import math
from pymicro_vad import MicroVad
from pysilero_vad import SileroVoiceActivityDetector
from pyspeex_noise import AudioProcessor
from .const import BYTES_PER_CHUNK
@@ -42,8 +43,8 @@ class AudioEnhancer(ABC):
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
class MicroVadSpeexEnhancer(AudioEnhancer):
"""Audio enhancer that runs microVAD and speex."""
class SileroVadSpeexEnhancer(AudioEnhancer):
"""Audio enhancer that runs Silero VAD and speex."""
def __init__(
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
@@ -69,21 +70,49 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
self.noise_suppression,
)
self.vad: MicroVad | None = None
self.vad: SileroVoiceActivityDetector | None = None
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
# buffer audio. The previous speech probability is used until enough
# audio has been buffered.
self._vad_buffer: bytearray | None = None
self._vad_buffer_chunks = 0
self._vad_buffer_chunk_idx = 0
self._last_speech_probability: float | None = None
if self.is_vad_enabled:
self.vad = MicroVad()
_LOGGER.debug("Initialized microVAD")
self.vad = SileroVoiceActivityDetector()
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
self._vad_buffer_chunks = int(
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
)
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
self._vad_buffer = bytearray(self.vad.chunk_bytes())
_LOGGER.debug("Initialized Silero VAD")
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
speech_probability: float | None = None
assert len(audio) == BYTES_PER_CHUNK
if self.vad is not None:
# Run VAD
speech_probability = self.vad.Process10ms(audio)
assert self._vad_buffer is not None
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
self._vad_buffer_chunk_idx += 1
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
# We have enough data to run Silero VAD (32 ms)
self._last_speech_probability = self.vad.process_chunk(
self._vad_buffer[: self.vad.chunk_bytes()]
)
# Copy leftover audio that wasn't processed to start
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
-self._vad_leftover_bytes :
]
self._vad_buffer_chunk_idx = 0
if self.audio_processor is not None:
# Run noise suppression and auto gain
@@ -92,5 +121,5 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
return EnhancedAudioChunk(
audio=audio,
timestamp_ms=timestamp_ms,
speech_probability=speech_probability,
speech_probability=self._last_speech_probability,
)
@@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
}
@@ -55,7 +55,7 @@ from homeassistant.util import (
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.limited_size_dict import LimitedSizeDict
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer
from .const import (
ACKNOWLEDGE_PATH,
BYTES_PER_CHUNK,
@@ -633,7 +633,7 @@ class PipelineRun:
# Initialize with audio settings
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
# Default audio enhancer
self.audio_enhancer = MicroVadSpeexEnhancer(
self.audio_enhancer = SileroVadSpeexEnhancer(
self.audio_settings.auto_gain_dbfs,
self.audio_settings.noise_suppression_level,
self.audio_settings.is_vad_enabled,
@@ -1123,63 +1123,6 @@ class PipelineRun:
)
try:
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
if self.tts_stream and self.tts_stream.supports_streaming_input:
tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue()
else:
@@ -1265,6 +1208,17 @@ class PipelineRun:
assert self.tts_stream is not None
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
user_input = conversation.ConversationInput(
text=intent_input,
context=self.context,
conversation_id=conversation_id,
device_id=self._device_id,
satellite_id=self._satellite_id,
language=input_language,
agent_id=self.intent_agent.id,
extra_system_prompt=conversation_extra_system_prompt,
)
with (
chat_session.async_get_chat_session(
self.hass, user_input.conversation_id
@@ -1276,6 +1230,53 @@ class PipelineRun:
chat_log_delta_listener=chat_log_delta_listener,
) as chat_log,
):
agent_id = self.intent_agent.id
processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT
all_targets_in_satellite_area = False
intent_response: intent.IntentResponse | None = None
if not processed_locally and not self._intent_agent_only:
# Sentence triggers override conversation agent
if (
trigger_response_text
:= await conversation.async_handle_sentence_triggers(
self.hass, user_input, chat_log
)
) is not None:
# Sentence trigger matched
agent_id = "sentence_trigger"
processed_locally = True
intent_response = intent.IntentResponse(
self.pipeline.conversation_language
)
intent_response.async_set_speech(trigger_response_text)
intent_filter: Callable[[RecognizeResult], bool] | None = None
# If the LLM has API access, we filter out some sentences that are
# interfering with LLM operation.
if (
intent_agent_state := self.hass.states.get(self.intent_agent.id)
) and intent_agent_state.attributes.get(
ATTR_SUPPORTED_FEATURES, 0
) & conversation.ConversationEntityFeature.CONTROL:
intent_filter = _async_local_fallback_intent_filter
# Try local intents
if (
intent_response is None
and self.pipeline.prefer_local_intents
and (
intent_response := await conversation.async_handle_intents(
self.hass,
user_input,
chat_log,
intent_filter=intent_filter,
)
)
):
# Local intent matched
agent_id = conversation.HOME_ASSISTANT_AGENT
processed_locally = True
# It was already handled, create response and add to chat history
if intent_response is not None:
speech: str = intent_response.speech.get("plain", {}).get(
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.4.0"]
"requirements": ["hassil==3.5.0"]
}
@@ -112,48 +112,44 @@
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers when an Assist satellite becomes idle.",
"description_configured": "[%key:component::assist_satellite::triggers::idle::description%]",
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "When an Assist satellite becomes idle"
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers when an Assist satellite starts listening.",
"description_configured": "[%key:component::assist_satellite::triggers::listening::description%]",
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "When an Assist satellite starts listening"
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers when an Assist satellite is processing.",
"description_configured": "[%key:component::assist_satellite::triggers::processing::description%]",
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "When an Assist satellite is processing"
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers when an Assist satellite is responding.",
"description_configured": "[%key:component::assist_satellite::triggers::responding::description%]",
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
}
},
"name": "When an Assist satellite is responding"
"name": "Satellite started responding"
}
}
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.3"]
}
@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.1"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
}
@@ -12,8 +12,9 @@ from typing import Any, Protocol, cast
from propcache.api import cached_property
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components import labs, websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.components.labs import async_listen as async_labs_listen
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@@ -114,6 +115,52 @@ ATTR_SOURCE = "source"
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"light",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"climate",
"cover",
"fan",
"lawn_mower",
"light",
"media_player",
"text",
"vacuum",
}
@callback
def is_disabled_experimental_condition(hass: HomeAssistant, platform: str) -> bool:
"""Check if the platform is a disabled experimental condition platform."""
return (
platform in _EXPERIMENTAL_CONDITION_PLATFORMS
and not labs.async_is_preview_feature_enabled(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
)
)
@callback
def is_disabled_experimental_trigger(hass: HomeAssistant, platform: str) -> bool:
"""Check if the platform is a disabled experimental trigger platform."""
return (
platform in _EXPERIMENTAL_TRIGGER_PLATFORMS
and not labs.async_is_preview_feature_enabled(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
)
)
class IfAction(Protocol):
"""Define the format of if_action."""
@@ -317,6 +364,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
schema=vol.Schema({vol.Optional(CONF_ID): str}),
)
@callback
def new_triggers_conditions_listener() -> None:
"""Handle new_triggers_conditions flag change."""
hass.async_create_task(
reload_helper.execute_service(ServiceCall(hass, DOMAIN, SERVICE_RELOAD))
)
async_labs_listen(
hass,
DOMAIN,
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
new_triggers_conditions_listener,
)
websocket_api.async_register_command(hass, websocket_config)
return True
@@ -8,6 +8,8 @@
"integration_type": "system",
"preview_features": {
"new_triggers_conditions": {
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
}
},
@@ -69,10 +69,10 @@
},
"preview_features": {
"new_triggers_conditions": {
"description": "Enables new intuitive triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own intuitive triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new intuitive triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new intuitive triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Intuitive triggers and conditions"
"description": "Enables new purpose-specific triggers and conditions that are more user-friendly than technical state-based options.\n\nThese new automation features support targets across your entire home, letting you trigger automations for any entity, device, area, floor, or label (for example, when any light in your living room turned on). Integrations can now also provide their own purpose-specific triggers and conditions, just like actions.\n\nThis preview also includes a new tree view to help you navigate your home when adding triggers, conditions, and actions.",
"disable_confirmation": "Disabling this preview will cause automations and scripts that use the new purpose-specific triggers and conditions to fail.\n\nBefore disabling, ensure that your automations or scripts do not rely on this feature.",
"enable_confirmation": "This feature is still in development and may change. These new purpose-specific triggers and conditions are being refined based on user feedback and are not yet complete.\n\nBy enabling this preview, you'll have early access to these new capabilities, but be aware that they may be modified or updated in future releases.",
"name": "Purpose-specific triggers and conditions"
}
},
"services": {
@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .websocket import BangOlufsenWebsocket
from .websocket import BeoWebsocket
@dataclass
class BangOlufsenData:
class BeoData:
"""Dataclass for API client and WebSocket client."""
websocket: BangOlufsenWebsocket
websocket: BeoWebsocket
client: MozartClient
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
type BeoConfigEntry = ConfigEntry[BeoData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
"""Set up from a config entry."""
# Remove casts to str
assert entry.unique_id
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BangOlufsenWebsocket(hass, entry, client)
websocket = BeoWebsocket(hass, entry, client)
# Add the websocket and API client
entry.runtime_data = BangOlufsenData(websocket, client)
entry.runtime_data = BeoData(websocket, client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -82,9 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
"""Unload a config entry."""
# Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications()
@@ -47,7 +47,7 @@ _exception_map = {
}
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
_beolink_jid = ""
+34 -20
View File
@@ -14,21 +14,25 @@ from homeassistant.components.media_player import (
)
class BangOlufsenSource:
class BeoSource:
"""Class used for associating device source ids with friendly names. May not include all sources."""
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
BEO_STATES: dict[str, MediaPlayerState] = {
# Dict used for translating device states to Home Assistant states.
"started": MediaPlayerState.PLAYING,
"buffering": MediaPlayerState.PLAYING,
"idle": MediaPlayerState.IDLE,
"paused": MediaPlayerState.PAUSED,
"stopped": MediaPlayerState.PAUSED,
"stopped": MediaPlayerState.IDLE,
"ended": MediaPlayerState.PAUSED,
"error": MediaPlayerState.IDLE,
# A device's initial state is "unknown" and should be treated as "idle"
@@ -36,19 +40,19 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
}
# Dict used for translating Home Assistant settings to device repeat settings.
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BEO_REPEAT_FROM_HA.items()
}
# Media types for play_media
class BangOlufsenMediaType(StrEnum):
class BeoMediaType(StrEnum):
"""Bang & Olufsen specific media types."""
FAVOURITE = "favourite"
@@ -59,7 +63,7 @@ class BangOlufsenMediaType(StrEnum):
OVERLAY_TTS = "overlay_tts"
class BangOlufsenModel(StrEnum):
class BeoModel(StrEnum):
"""Enum for compatible model names."""
# Mozart devices
@@ -78,8 +82,18 @@ class BangOlufsenModel(StrEnum):
BEOREMOTE_ONE = "Beoremote One"
class BeoAttribute(StrEnum):
"""Enum for extra_state_attribute keys."""
BEOLINK = "beolink"
BEOLINK_PEERS = "peers"
BEOLINK_SELF = "self"
BEOLINK_LEADER = "leader"
BEOLINK_LISTENERS = "listeners"
# Physical "buttons" on devices
class BangOlufsenButtons(StrEnum):
class BeoButtons(StrEnum):
"""Enum for device buttons."""
BLUETOOTH = "Bluetooth"
@@ -126,7 +140,7 @@ class WebsocketNotification(StrEnum):
DOMAIN: Final[str] = "bang_olufsen"
# Default values for configuration.
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
# Configuration.
CONF_SERIAL_NUMBER: Final = "serial_number"
@@ -134,7 +148,7 @@ CONF_BEOLINK_JID: Final = "jid"
# Models to choose from in manual configuration.
SELECTABLE_MODELS: list[str] = [
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
]
MANUFACTURER: Final[str] = "Bang & Olufsen"
@@ -146,15 +160,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
ATTR_FRIENDLY_NAME: Final[str] = "fn"
# Power states.
BANG_OLUFSEN_ON: Final[str] = "on"
BEO_ON: Final[str] = "on"
VALID_MEDIA_TYPES: Final[tuple] = (
BangOlufsenMediaType.FAVOURITE,
BangOlufsenMediaType.DEEZER,
BangOlufsenMediaType.RADIO,
BangOlufsenMediaType.TTS,
BangOlufsenMediaType.TIDAL,
BangOlufsenMediaType.OVERLAY_TTS,
BeoMediaType.FAVOURITE,
BeoMediaType.DEEZER,
BeoMediaType.RADIO,
BeoMediaType.TTS,
BeoMediaType.TIDAL,
BeoMediaType.OVERLAY_TTS,
MediaType.MUSIC,
MediaType.URL,
MediaType.CHANNEL,
@@ -232,7 +246,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
)
# Device events
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = {
@@ -249,7 +263,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import BangOlufsenConfigEntry
from . import BeoConfigEntry
from .const import DOMAIN
from .util import get_device_buttons
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
hass: HomeAssistant, config_entry: BeoConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class BangOlufsenBase:
"""Base class for BangOlufsen Home Assistant objects."""
class BeoBase:
"""Base class for Bang & Olufsen Home Assistant objects."""
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the object."""
@@ -51,8 +51,8 @@ class BangOlufsenBase:
)
class BangOlufsenEntity(Entity, BangOlufsenBase):
"""Base Entity for BangOlufsen entities."""
class BeoEntity(Entity, BeoBase):
"""Base Entity for Bang & Olufsen entities."""
_attr_has_entity_name = True
_attr_should_poll = False
+19 -20
View File
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BangOlufsenConfigEntry
from . import BeoConfigEntry
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS,
@@ -25,10 +25,10 @@ from .const import (
DEVICE_BUTTON_EVENTS,
DOMAIN,
MANUFACTURER,
BangOlufsenModel,
BeoModel,
WebsocketNotification,
)
from .entity import BangOlufsenEntity
from .entity import BeoEntity
from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BangOlufsenConfigEntry,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Event entities from config entry."""
entities: list[BangOlufsenEvent] = []
entities: list[BeoEvent] = []
async_add_entities(
BangOlufsenButtonEvent(config_entry, button_type)
BeoButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
)
@@ -54,7 +54,7 @@ async def async_setup_entry(
# Add Light keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
@@ -66,7 +66,7 @@ async def async_setup_entry(
# Add Control keys
entities.extend(
[
BangOlufsenRemoteKeyEvent(
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
@@ -84,10 +84,9 @@ async def async_setup_entry(
config_entry.entry_id
)
for device in devices:
if (
device.model == BangOlufsenModel.BEOREMOTE_ONE
and device.serial_number not in {remote.serial_number for remote in remotes}
):
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
remote.serial_number for remote in remotes
}:
device_registry.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id
)
@@ -95,13 +94,13 @@ async def async_setup_entry(
async_add_entities(new_entities=entities)
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
class BeoEvent(BeoEntity, EventEntity):
"""Base Event class."""
_attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Initialize Event."""
super().__init__(config_entry, config_entry.runtime_data.client)
@@ -112,12 +111,12 @@ class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
self.async_write_ha_state()
class BangOlufsenButtonEvent(BangOlufsenEvent):
class BeoButtonEvent(BeoEvent):
"""Event class for Button events."""
_attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
"""Initialize Button."""
super().__init__(config_entry)
@@ -146,14 +145,14 @@ class BangOlufsenButtonEvent(BangOlufsenEvent):
)
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
class BeoRemoteKeyEvent(BeoEvent):
"""Event class for Beoremote One key events."""
_attr_event_types = BEO_REMOTE_KEY_EVENTS
def __init__(
self,
config_entry: BangOlufsenConfigEntry,
config_entry: BeoConfigEntry,
remote: PairedRemote,
key_type: str,
) -> None:
@@ -166,8 +165,8 @@ class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BangOlufsenModel.BEOREMOTE_ONE,
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BeoModel.BEOREMOTE_ONE,
serial_number=remote.serial_number,
sw_version=remote.app_version,
manufacturer=MANUFACTURER,
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["mozart-api==5.1.0.247.1"],
"requirements": ["mozart-api==5.3.1.108.0"],
"zeroconf": ["_bangolufsen._tcp.local."]
}
@@ -69,11 +69,11 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.util.dt import utcnow
from . import BangOlufsenConfigEntry
from . import BeoConfigEntry
from .const import (
BANG_OLUFSEN_REPEAT_FROM_HA,
BANG_OLUFSEN_REPEAT_TO_HA,
BANG_OLUFSEN_STATES,
BEO_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA,
BEO_STATES,
BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID,
@@ -82,11 +82,12 @@ from .const import (
FALLBACK_SOURCES,
MANUFACTURER,
VALID_MEDIA_TYPES,
BangOlufsenMediaType,
BangOlufsenSource,
BeoAttribute,
BeoMediaType,
BeoSource,
WebsocketNotification,
)
from .entity import BangOlufsenEntity
from .entity import BeoEntity
from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0
@@ -95,7 +96,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BANG_OLUFSEN_FEATURES = (
BEO_FEATURES = (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.GROUPING
@@ -118,15 +119,13 @@ BANG_OLUFSEN_FEATURES = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BangOlufsenConfigEntry,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Media Player entity from config entry."""
# Add MediaPlayer entity
async_add_entities(
new_entities=[
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
],
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)],
update_before_add=True,
)
@@ -186,7 +185,7 @@ async def async_setup_entry(
)
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
"""Representation of a media player."""
_attr_name = None
@@ -224,7 +223,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Beolink compatible sources
self._beolink_sources: dict[str, bool] = {}
self._remote_leader: BeolinkLeader | None = None
# Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
# Extra state attributes:
# Beolink: peer(s), listener(s), leader and self
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
async def async_added_to_hass(self) -> None:
@@ -286,7 +286,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None:
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle
@@ -406,8 +406,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Check if source is line-in or optical and progress should be updated
if self._source_change.id in (
BangOlufsenSource.LINE_IN.id,
BangOlufsenSource.SPDIF.id,
BeoSource.LINE_IN.id,
BeoSource.SPDIF.id,
):
self._playback_progress = PlaybackProgress(progress=0)
@@ -436,7 +436,10 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
await self._async_update_beolink()
async def _async_update_beolink(self) -> None:
"""Update the current Beolink leader, listeners, peers and self."""
"""Update the current Beolink leader, listeners, peers and self.
Updates Home Assistant state.
"""
self._beolink_attributes = {}
@@ -445,18 +448,22 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Add Beolink self
self._beolink_attributes = {
"beolink": {"self": {self.device_entry.name: self._beolink_jid}}
BeoAttribute.BEOLINK: {
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid}
}
}
# Add Beolink peers
peers = await self._client.get_beolink_peers()
if len(peers) > 0:
self._beolink_attributes["beolink"]["peers"] = {}
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
] = {}
for peer in peers:
self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
peer.jid
)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS
][peer.friendly_name] = peer.jid
# Add Beolink listeners / leader
self._remote_leader = self._playback_metadata.remote_leader
@@ -477,7 +484,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# Add self
group_members.append(self.entity_id)
self._beolink_attributes["beolink"]["leader"] = {
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LEADER
] = {
self._remote_leader.friendly_name: self._remote_leader.jid,
}
@@ -514,9 +523,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
beolink_listener.jid
)
break
self._beolink_attributes["beolink"]["listeners"] = (
beolink_listeners_attribute
)
self._beolink_attributes[BeoAttribute.BEOLINK][
BeoAttribute.BEOLINK_LISTENERS
] = beolink_listeners_attribute
self._attr_group_members = group_members
@@ -574,7 +583,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
for sound_mode in sound_modes:
label = f"{sound_mode.name} ({sound_mode.id})"
self._sound_modes[label] = sound_mode.id
self._sound_modes[label] = cast(int, sound_mode.id)
if sound_mode.id == active_sound_mode.id:
self._attr_sound_mode = label
@@ -587,7 +596,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
features = BANG_OLUFSEN_FEATURES
features = BEO_FEATURES
# Add seeking if supported by the current source
if self._source_change.is_seekable is True:
@@ -598,7 +607,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState:
"""Return the current state of the media player."""
return BANG_OLUFSEN_STATES[self._state]
return BEO_STATES[self._state]
@property
def volume_level(self) -> float | None:
@@ -615,11 +624,18 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
return None
@property
def media_content_type(self) -> str:
def media_content_type(self) -> MediaType | str | None:
"""Return the current media type."""
# Hard to determine content type
if self._source_change.id == BangOlufsenSource.URI_STREAMER.id:
return MediaType.URL
content_type = {
BeoSource.URI_STREAMER.id: MediaType.URL,
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
}
# Hard to determine content type.
if self._source_change.id in content_type:
return content_type[self._source_change.id]
return MediaType.MUSIC
@property
@@ -632,6 +648,11 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Return the current playback progress."""
return self._playback_progress.progress
@property
def media_content_id(self) -> str | None:
"""Return internal ID of Deezer, Tidal and radio stations."""
return self._playback_metadata.source_internal_id
@property
def media_image_url(self) -> str | None:
"""Return URL of the currently playing music."""
@@ -740,9 +761,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat."""
await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat])
)
async def async_set_shuffle(self, shuffle: bool) -> None:
@@ -846,7 +865,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self._volume.level.level + offset_volume, 100
)
if media_type == BangOlufsenMediaType.OVERLAY_TTS:
if media_type == BeoMediaType.OVERLAY_TTS:
# Bang & Olufsen cloud TTS
overlay_play_request.text_to_speech = (
OverlayPlayRequestTextToSpeechTextToSpeech(
@@ -863,14 +882,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
# The "provider" media_type may not be suitable for overlay all the time.
# Use it for now.
elif media_type == BangOlufsenMediaType.TTS:
elif media_type == BeoMediaType.TTS:
await self._client.post_overlay_play(
overlay_play_request=OverlayPlayRequest(
uri=Uri(location=media_id),
)
)
elif media_type == BangOlufsenMediaType.RADIO:
elif media_type == BeoMediaType.RADIO:
await self._client.run_provided_scene(
scene_properties=SceneProperties(
action_list=[
@@ -882,13 +901,13 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
)
)
elif media_type == BangOlufsenMediaType.FAVOURITE:
elif media_type == BeoMediaType.FAVOURITE:
await self._client.activate_preset(id=int(media_id))
elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL):
try:
# Play Deezer flow.
if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
if media_id == "flow" and media_type == BeoMediaType.DEEZER:
deezer_id = None
if "id" in kwargs[ATTR_MEDIA_EXTRA]:
+19 -8
View File
@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -40,16 +40,27 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
]
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
def get_device_buttons(model: BeoModel) -> list[str]:
"""Get supported buttons for a given model."""
# Beoconnect Core does not have any buttons
if model == BeoModel.BEOCONNECT_CORE:
return []
buttons = DEVICE_BUTTONS.copy()
# Beosound Premiere does not have a bluetooth button
if model == BangOlufsenModel.BEOSOUND_PREMIERE:
buttons.remove(BangOlufsenButtons.BLUETOOTH)
# Models that don't have a microphone button
if model in (
BeoModel.BEOSOUND_A5,
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Beoconnect Core does not have any buttons
elif model == BangOlufsenModel.BEOCONNECT_CORE:
buttons = []
# Models that don't have a Bluetooth button
if model in (
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
return buttons
@@ -27,20 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum
from .const import (
BANG_OLUFSEN_WEBSOCKET_EVENT,
BEO_WEBSOCKET_EVENT,
CONNECTION_STATUS,
DOMAIN,
EVENT_TRANSLATION_MAP,
BangOlufsenModel,
BeoModel,
WebsocketNotification,
)
from .entity import BangOlufsenBase
from .entity import BeoBase
from .util import get_device, get_remotes
_LOGGER = logging.getLogger(__name__)
class BangOlufsenWebsocket(BangOlufsenBase):
class BeoWebsocket(BeoBase):
"""The WebSocket listeners."""
def __init__(
@@ -48,7 +48,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
) -> None:
"""Initialize the WebSocket listeners."""
BangOlufsenBase.__init__(self, entry, client)
BeoBase.__init__(self, entry, client)
self.hass = hass
self._device = get_device(hass, self._unique_id)
@@ -178,7 +178,7 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self.entry.entry_id
)
if device.serial_number is not None
and device.model == BangOlufsenModel.BEOREMOTE_ONE
and device.model == BeoModel.BEOREMOTE_ONE
]
# Get paired remotes from device
remote_serial_numbers = [
@@ -274,4 +274,4 @@ class BangOlufsenWebsocket(BangOlufsenBase):
}
_LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification)
@@ -174,5 +174,13 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}
@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -317,5 +321,36 @@
}
}
},
"title": "Binary sensor"
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
}
@@ -0,0 +1,67 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domain: str = DOMAIN
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_state = to_state
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS
@@ -0,0 +1,25 @@
.trigger_common_fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
occupancy_cleared:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy
occupancy_detected:
fields: *trigger_common_fields
target:
entity:
domain: binary_sensor
device_class: occupancy
+1 -1
View File
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.24.1"]
"requirements": ["blinkpy==0.25.1"]
}
@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==1.0.1",
"bleak==2.0.0",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.1.2",
"habluetooth==5.7.0"
"habluetooth==5.8.0"
]
}
@@ -68,9 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
model=panel.model,
model=panel.model.name,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -83,7 +83,7 @@ async def try_connect(
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
return (panel.model.name, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -20,7 +20,8 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"model": entry.runtime_data.model.name,
"family": entry.runtime_data.model.family.name,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,
@@ -26,7 +26,7 @@ class BoschAlarmEntity(Entity):
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
name=f"Bosch {panel.model.name}",
manufacturer="Bosch Security Systems",
)
@@ -12,5 +12,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["bosch-alarm-mode2==0.4.6"]
"requirements": ["bosch-alarm-mode2==0.4.10"]
}
@@ -36,6 +36,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/broadlink",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["broadlink"],
"requirements": ["broadlink==0.19.0"]
+33 -20
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Any
from typing import Any, Final
from bsblan import BSBLANError
@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.enum import try_parse_enum
@@ -39,6 +39,22 @@ PRESET_MODES = [
PRESET_NONE,
]
# Mapping from Home Assistant HVACMode to BSB-Lan integer values
# BSB-Lan uses: 0=off, 1=auto, 2=eco/reduced, 3=heat/comfort
HA_TO_BSBLAN_HVAC_MODE: Final[dict[HVACMode, int]] = {
HVACMode.OFF: 0,
HVACMode.AUTO: 1,
HVACMode.HEAT: 3,
}
# Mapping from BSB-Lan integer values to Home Assistant HVACMode
BSBLAN_TO_HA_HVAC_MODE: Final[dict[int, HVACMode]] = {
0: HVACMode.OFF,
1: HVACMode.AUTO,
2: HVACMode.AUTO, # eco/reduced maps to AUTO with preset
3: HVACMode.HEAT,
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -98,17 +114,20 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def hvac_mode(self) -> HVACMode | None:
"""Return hvac operation ie. heat, cool mode."""
if self.coordinator.data.state.hvac_mode.value == PRESET_ECO:
return HVACMode.AUTO
return try_parse_enum(HVACMode, self.coordinator.data.state.hvac_mode.value)
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
if hvac_mode_value is None:
return None
# BSB-Lan returns integer values: 0=off, 1=auto, 2=eco, 3=heat
if isinstance(hvac_mode_value, int):
return BSBLAN_TO_HA_HVAC_MODE.get(hvac_mode_value)
return try_parse_enum(HVACMode, hvac_mode_value)
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
if (
self.hvac_mode == HVACMode.AUTO
and self.coordinator.data.state.hvac_mode.value == PRESET_ECO
):
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
# BSB-Lan mode 2 is eco/reduced mode
if hvac_mode_value == 2:
return PRESET_ECO
return PRESET_NONE
@@ -118,13 +137,6 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
if self.hvac_mode != HVACMode.AUTO and preset_mode != PRESET_NONE:
raise ServiceValidationError(
"Preset mode can only be set when HVAC mode is set to 'auto'",
translation_domain=DOMAIN,
translation_key="set_preset_mode_error",
translation_placeholders={"preset_mode": preset_mode},
)
await self.async_set_data(preset_mode=preset_mode)
async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -133,16 +145,17 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
async def async_set_data(self, **kwargs: Any) -> None:
"""Set device settings using BSBLAN."""
data = {}
data: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
data[ATTR_TARGET_TEMPERATURE] = kwargs[ATTR_TEMPERATURE]
if ATTR_HVAC_MODE in kwargs:
data[ATTR_HVAC_MODE] = kwargs[ATTR_HVAC_MODE]
data[ATTR_HVAC_MODE] = HA_TO_BSBLAN_HVAC_MODE[kwargs[ATTR_HVAC_MODE]]
if ATTR_PRESET_MODE in kwargs:
# eco preset uses BSB-Lan mode 2, none preset uses mode 1 (auto)
if kwargs[ATTR_PRESET_MODE] == PRESET_ECO:
data[ATTR_HVAC_MODE] = PRESET_ECO
data[ATTR_HVAC_MODE] = 2
elif kwargs[ATTR_PRESET_MODE] == PRESET_NONE:
data[ATTR_HVAC_MODE] = PRESET_NONE
data[ATTR_HVAC_MODE] = 1
try:
await self.coordinator.client.thermostat(**data)
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==3.1.1"],
"requirements": ["python-bsblan==3.1.3"],
"zeroconf": [
{
"name": "bsb-lan*",
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from bsblan import BSBLANError
from bsblan import BSBLANError, SetHotWaterParam
from homeassistant.components.water_heater import (
STATE_ECO,
@@ -131,7 +131,9 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
try:
await self.coordinator.client.set_hot_water(nominal_setpoint=temperature)
await self.coordinator.client.set_hot_water(
SetHotWaterParam(nominal_setpoint=temperature)
)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -144,7 +146,9 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
"""Set new operation mode."""
bsblan_mode = OPERATION_MODES_REVERSE.get(operation_mode)
try:
await self.coordinator.client.set_hot_water(operating_mode=bsblan_mode)
await self.coordinator.client.set_hot_water(
SetHotWaterParam(operating_mode=bsblan_mode)
)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
+3 -22
View File
@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit, RTCIceServer
from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -37,6 +37,7 @@ from homeassistant.components.stream import (
Stream,
create_stream,
)
from homeassistant.components.web_rtc import async_get_ice_servers
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -84,7 +85,6 @@ from .prefs import (
get_dynamic_camera_stream_settings,
)
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
@@ -93,7 +93,6 @@ from .webrtc import (
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
async_register_ice_servers,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -400,20 +399,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
@callback
def get_ice_servers() -> list[RTCIceServer]:
if hass.config.webrtc.ice_servers:
return hass.config.webrtc.ice_servers
return [
RTCIceServer(
urls=[
"stun:stun.home-assistant.io:80",
"stun:stun.home-assistant.io:3478",
]
),
]
async_register_ice_servers(hass, get_ice_servers)
return True
@@ -731,11 +716,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
ice_servers = async_get_ice_servers(self.hass)
config.configuration.ice_servers.extend(ice_servers)
return config
@@ -3,7 +3,7 @@
"name": "Camera",
"after_dependencies": ["media_player"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["http"],
"dependencies": ["http", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
+2 -28
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import Awaitable, Callable, Iterable
from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
@@ -12,12 +12,7 @@ from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
from webrtc_models import (
RTCConfiguration,
RTCIceCandidate,
RTCIceCandidateInit,
RTCIceServer,
)
from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
@@ -38,9 +33,6 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
_WEBRTC = "WebRTC"
@@ -367,21 +359,3 @@ async def async_get_supported_provider(
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
) -> Callable[[], None]:
"""Register a ICE server.
The registering integration is responsible to implement caching if needed.
"""
servers = hass.data.setdefault(DATA_ICE_SERVERS, [])
def remove() -> None:
servers.remove(get_ice_server_fn)
servers.append(get_ice_server_fn)
return remove
@@ -98,6 +98,12 @@
}
},
"triggers": {
"started_cooling": {
"trigger": "mdi:snowflake"
},
"started_drying": {
"trigger": "mdi:water-percent"
},
"started_heating": {
"trigger": "mdi:fire"
},
+27 -10
View File
@@ -298,38 +298,55 @@
},
"title": "Climate",
"triggers": {
"started_heating": {
"description": "Triggers when a climate starts to heat.",
"description_configured": "[%key:component::climate::triggers::started_heating::description%]",
"started_cooling": {
"description": "Triggers after one or more climate-control devices start cooling.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "When a climate starts to heat"
"name": "Climate-control device started cooling"
},
"started_drying": {
"description": "Triggers after one or more climate-control devices start drying.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started drying"
},
"started_heating": {
"description": "Triggers after one or more climate-control devices start heating.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "Climate-control device started heating"
},
"turned_off": {
"description": "Triggers when a climate is turned off.",
"description_configured": "[%key:component::climate::triggers::turned_off::description%]",
"description": "Triggers after one or more climate-control devices turn off.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "When a climate is turned off"
"name": "Climate-control device turned off"
},
"turned_on": {
"description": "Triggers when a climate is turned on.",
"description_configured": "[%key:component::climate::triggers::turned_on::description%]",
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::trigger_behavior_description%]",
"name": "[%key:component::climate::common::trigger_behavior_name%]"
}
},
"name": "When a climate is turned on"
"name": "Climate-control device turned on"
}
}
}
@@ -11,6 +11,12 @@ from homeassistant.helpers.trigger import (
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
TRIGGERS: dict[str, type[Trigger]] = {
"started_cooling": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
),
"started_drying": make_entity_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
),
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_conditional_entity_state_trigger(
DOMAIN,
@@ -14,6 +14,8 @@
- last
- any
started_cooling: *trigger_common
started_drying: *trigger_common
started_heating: *trigger_common
turned_off: *trigger_common
turned_on: *trigger_common
+18 -5
View File
@@ -4,12 +4,13 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from contextlib import suppress
from datetime import datetime, timedelta
from enum import Enum
import logging
from typing import Any, cast
from hass_nabucasa import Cloud
from hass_nabucasa import Cloud, NabuCasaBaseError
import voluptuous as vol
from homeassistant.components import alexa, google_assistant
@@ -78,13 +79,16 @@ from .subscription import async_subscription_info
DEFAULT_MODE = MODE_PROD
PLATFORMS = [
Platform.AI_TASK,
Platform.BINARY_SENSOR,
Platform.CONVERSATION,
Platform.STT,
Platform.TTS,
]
LLM_PLATFORMS = [
Platform.AI_TASK,
Platform.CONVERSATION,
]
SERVICE_REMOTE_CONNECT = "remote_connect"
SERVICE_REMOTE_DISCONNECT = "remote_disconnect"
@@ -431,7 +435,14 @@ def _handle_prefs_updated(hass: HomeAssistant, cloud: Cloud[CloudClient]) -> Non
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
platforms = PLATFORMS.copy()
if (cloud := hass.data[DATA_CLOUD]).is_logged_in:
with suppress(NabuCasaBaseError):
await cloud.llm.async_ensure_token()
platforms += LLM_PLATFORMS
await hass.config_entries.async_forward_entry_setups(entry, platforms)
entry.runtime_data = {"platforms": platforms}
stt_tts_entities_added = hass.data[DATA_PLATFORMS_SETUP]["stt_tts_entities_added"]
stt_tts_entities_added.set()
@@ -440,7 +451,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
return await hass.config_entries.async_unload_platforms(
entry, entry.runtime_data["platforms"]
)
@callback
+4 -9
View File
@@ -19,7 +19,7 @@ from PIL import Image
from homeassistant.components import ai_task, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
@@ -94,15 +94,10 @@ async def async_setup_entry(
) -> None:
"""Set up Home Assistant Cloud AI Task entity."""
cloud = hass.data[DATA_CLOUD]
try:
await cloud.llm.async_ensure_token()
except LLMError:
return
async_add_entities([CloudLLMTaskEntity(cloud, config_entry)])
async_add_entities([CloudAITaskEntity(cloud, config_entry)])
class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
class CloudAITaskEntity(BaseCloudLLMEntity, ai_task.AITaskEntity):
"""Home Assistant Cloud AI Task entity."""
_attr_has_entity_name = True
@@ -179,7 +174,7 @@ class CloudLLMTaskEntity(ai_task.AITaskEntity, BaseCloudLLMEntity):
attachments=attachments,
)
except LLMAuthenticationError as err:
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
raise HomeAssistantError("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:
+10 -6
View File
@@ -19,8 +19,8 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
@@ -71,6 +71,7 @@ class CloudClient(Interface):
self._google_config_init_lock = asyncio.Lock()
self._relayer_region: str | None = None
self._cloud_ice_servers_listener: Callable[[], None] | None = None
self._ice_servers: list[RTCIceServer] = []
@property
def base_path(self) -> Path:
@@ -117,6 +118,11 @@ class CloudClient(Interface):
"""Return the connected relayer region."""
return self._relayer_region
@property
def ice_servers(self) -> list[RTCIceServer]:
"""Return the current ICE servers."""
return self._ice_servers
async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig:
"""Return Alexa config."""
if self._alexa_config is None:
@@ -203,11 +209,8 @@ class CloudClient(Interface):
ice_servers: list[RTCIceServer],
) -> Callable[[], None]:
"""Register cloud ice server."""
def get_ice_servers() -> list[RTCIceServer]:
return ice_servers
return async_register_ice_servers(self._hass, get_ice_servers)
self._ice_servers = ice_servers
return async_register_ice_servers(self._hass, lambda: self._ice_servers)
async def async_register_cloud_ice_servers_listener(
prefs: CloudPreferences,
@@ -268,6 +271,7 @@ class CloudClient(Interface):
async def logout_cleanups(self) -> None:
"""Cleanup some stuff after logout."""
self._ice_servers = []
await self.prefs.async_set_username(None)
if self._alexa_config:
@@ -4,8 +4,6 @@ from __future__ import annotations
from typing import Literal
from hass_nabucasa.llm import LLMError
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
@@ -24,17 +22,12 @@ async def async_setup_entry(
) -> None:
"""Set up the Home Assistant Cloud conversation entity."""
cloud = hass.data[DATA_CLOUD]
try:
await cloud.llm.async_ensure_token()
except LLMError:
return
async_add_entities([CloudConversationEntity(cloud, config_entry)])
class CloudConversationEntity(
conversation.ConversationEntity,
BaseCloudLLMEntity,
conversation.ConversationEntity,
):
"""Home Assistant Cloud conversation agent."""
+5 -6
View File
@@ -8,10 +8,9 @@ import logging
import re
from typing import Any, Literal, cast
from hass_nabucasa import Cloud
from hass_nabucasa import Cloud, NabuCasaBaseError
from hass_nabucasa.llm import (
LLMAuthenticationError,
LLMError,
LLMRateLimitError,
LLMResponseError,
LLMServiceError,
@@ -37,7 +36,7 @@ from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import llm
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
@@ -562,7 +561,7 @@ class BaseCloudLLMEntity(Entity):
"schema": _format_structured_output(
structure, chat_log.llm_api
),
"strict": True,
"strict": False,
},
}
@@ -601,14 +600,14 @@ class BaseCloudLLMEntity(Entity):
)
except LLMAuthenticationError as err:
raise ConfigEntryAuthFailed("Cloud LLM authentication failed") from err
raise HomeAssistantError("Cloud LLM authentication failed") from err
except LLMRateLimitError as err:
raise HomeAssistantError("Cloud LLM is rate limited") from err
except LLMResponseError as err:
raise HomeAssistantError(str(err)) from err
except LLMServiceError as err:
raise HomeAssistantError("Error talking to Cloud LLM") from err
except LLMError as err:
except NabuCasaBaseError as err:
raise HomeAssistantError(str(err)) from err
if not chat_log.unresponded_tool_results:
@@ -99,6 +99,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_hook_delete)
websocket_api.async_register_command(hass, websocket_remote_connect)
websocket_api.async_register_command(hass, websocket_remote_disconnect)
websocket_api.async_register_command(hass, websocket_webrtc_ice_servers)
websocket_api.async_register_command(hass, google_assistant_get)
websocket_api.async_register_command(hass, google_assistant_list)
@@ -1107,6 +1108,7 @@ async def alexa_sync(
@websocket_api.websocket_command({"type": "cloud/tts/info"})
@callback
def tts_info(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
@@ -1134,3 +1136,22 @@ def tts_info(
)
connection.send_result(msg["id"], {"languages": result})
@websocket_api.websocket_command(
{
vol.Required("type"): "cloud/webrtc/ice_servers",
}
)
@_require_cloud_login
@callback
def websocket_webrtc_ice_servers(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Handle get WebRTC ICE servers websocket command."""
connection.send_result(
msg["id"],
[server.to_dict() for server in hass.data[DATA_CLOUD].client.ice_servers],
)
+2 -2
View File
@@ -8,11 +8,11 @@
"google_assistant"
],
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["auth", "http", "repairs", "webhook"],
"dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/cloud",
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.6.1"],
"requirements": ["hass-nabucasa==1.7.0"],
"single_config_entry": true
}
@@ -1,157 +1,19 @@
"""Module for color_extractor (RGB extraction from images) component."""
import asyncio
import io
import logging
import aiohttp
from colorthief import ColorThief
from PIL import UnidentifiedImageError
import voluptuous as vol
from homeassistant.components.light import (
ATTR_RGB_COLOR,
DOMAIN as LIGHT_DOMAIN,
LIGHT_TURN_ON_SCHEMA,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN
from .services import async_setup_services
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
# Extend the existing light.turn_on service schema
SERVICE_SCHEMA = vol.All(
cv.has_at_least_one_key(ATTR_URL, ATTR_PATH),
cv.make_entity_service_schema(
{
**LIGHT_TURN_ON_SCHEMA,
vol.Exclusive(ATTR_PATH, "color_extractor"): cv.isfile,
vol.Exclusive(ATTR_URL, "color_extractor"): cv.url,
}
),
)
def _get_file(file_path):
"""Get a PIL acceptable input file reference.
Allows us to mock patch during testing to make BytesIO stream.
"""
return file_path
def _get_color(file_handler) -> tuple:
"""Given an image file, extract the predominant color from it."""
color_thief = ColorThief(file_handler)
# get_color returns a SINGLE RGB value for the given image
color = color_thief.get_color(quality=1)
_LOGGER.debug("Extracted RGB color %s from image", color)
return color
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Color extractor component."""
async def async_handle_service(service_call: ServiceCall) -> None:
"""Decide which color_extractor method to call based on service."""
service_data = dict(service_call.data)
try:
if ATTR_URL in service_data:
image_type = "URL"
image_reference = service_data.pop(ATTR_URL)
color = await async_extract_color_from_url(image_reference)
elif ATTR_PATH in service_data:
image_type = "file path"
image_reference = service_data.pop(ATTR_PATH)
color = await hass.async_add_executor_job(
extract_color_from_path, image_reference
)
except UnidentifiedImageError as ex:
_LOGGER.error(
"Bad image from %s '%s' provided, are you sure it's an image? %s",
image_type,
image_reference,
ex,
)
return
if color:
service_data[ATTR_RGB_COLOR] = color
await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_SERVICE_TURN_ON, service_data, blocking=True
)
hass.services.async_register(
DOMAIN,
SERVICE_TURN_ON,
async_handle_service,
schema=SERVICE_SCHEMA,
)
async def async_extract_color_from_url(url):
"""Handle call for URL based image."""
if not hass.config.is_allowed_external_url(url):
_LOGGER.error(
(
"External URL '%s' is not allowed, please add to"
" 'allowlist_external_urls'"
),
url,
)
return None
_LOGGER.debug("Getting predominant RGB from image URL '%s'", url)
# Download the image into a buffer for ColorThief to check against
try:
session = aiohttp_client.async_get_clientsession(hass)
async with asyncio.timeout(10):
response = await session.get(url)
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Failed to get ColorThief image due to HTTPError: %s", err)
return None
content = await response.content.read()
with io.BytesIO(content) as _file:
_file.name = "color_extractor.jpg"
_file.seek(0)
return _get_color(_file)
def extract_color_from_path(file_path):
"""Handle call for local file based image."""
if not hass.config.is_allowed_path(file_path):
_LOGGER.error(
(
"File path '%s' is not allowed, please add to"
" 'allowlist_external_dirs'"
),
file_path,
)
return None
_LOGGER.debug("Getting predominant RGB from file path '%s'", file_path)
_file = _get_file(file_path)
return _get_color(_file)
async_setup_services(hass)
return True
@@ -0,0 +1,156 @@
"""Module for color_extractor (RGB extraction from images) component."""
import asyncio
import io
import logging
import aiohttp
from colorthief import ColorThief
from PIL import UnidentifiedImageError
import voluptuous as vol
from homeassistant.components.light import (
ATTR_RGB_COLOR,
DOMAIN as LIGHT_DOMAIN,
LIGHT_TURN_ON_SCHEMA,
)
from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
_LOGGER = logging.getLogger(__name__)
# Extend the existing light.turn_on service schema
SERVICE_SCHEMA = vol.All(
cv.has_at_least_one_key(ATTR_URL, ATTR_PATH),
cv.make_entity_service_schema(
{
**LIGHT_TURN_ON_SCHEMA,
vol.Exclusive(ATTR_PATH, "color_extractor"): cv.isfile,
vol.Exclusive(ATTR_URL, "color_extractor"): cv.url,
}
),
)
def _get_file(file_path: str) -> str:
"""Get a PIL acceptable input file reference.
Allows us to mock patch during testing to make BytesIO stream.
"""
return file_path
def _get_color(file_handler: io.BytesIO | str) -> tuple[int, int, int]:
"""Given an image file, extract the predominant color from it."""
color_thief = ColorThief(file_handler)
# get_color returns a SINGLE RGB value for the given image
color = color_thief.get_color(quality=1)
_LOGGER.debug("Extracted RGB color %s from image", color)
return color
async def _async_extract_color_from_url(
hass: HomeAssistant, url: str
) -> tuple[int, int, int] | None:
"""Handle call for URL based image."""
if not hass.config.is_allowed_external_url(url):
_LOGGER.error(
(
"External URL '%s' is not allowed, please add to"
" 'allowlist_external_urls'"
),
url,
)
return None
_LOGGER.debug("Getting predominant RGB from image URL '%s'", url)
# Download the image into a buffer for ColorThief to check against
try:
session = aiohttp_client.async_get_clientsession(hass)
async with asyncio.timeout(10):
response = await session.get(url)
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Failed to get ColorThief image due to HTTPError: %s", err)
return None
content = await response.content.read()
with io.BytesIO(content) as _file:
_file.name = "color_extractor.jpg"
_file.seek(0)
return _get_color(_file)
def _extract_color_from_path(
hass: HomeAssistant, file_path: str
) -> tuple[int, int, int] | None:
"""Handle call for local file based image."""
if not hass.config.is_allowed_path(file_path):
_LOGGER.error(
"File path '%s' is not allowed, please add to 'allowlist_external_dirs'",
file_path,
)
return None
_LOGGER.debug("Getting predominant RGB from file path '%s'", file_path)
_file = _get_file(file_path)
return _get_color(_file)
async def async_handle_service(service_call: ServiceCall) -> None:
"""Decide which color_extractor method to call based on service."""
service_data = dict(service_call.data)
try:
if ATTR_URL in service_data:
image_type = "URL"
image_reference = service_data.pop(ATTR_URL)
color = await _async_extract_color_from_url(
service_call.hass, image_reference
)
elif ATTR_PATH in service_data:
image_type = "file path"
image_reference = service_data.pop(ATTR_PATH)
color = await service_call.hass.async_add_executor_job(
_extract_color_from_path, service_call.hass, image_reference
)
except UnidentifiedImageError as ex:
_LOGGER.error(
"Bad image from %s '%s' provided, are you sure it's an image? %s",
image_type,
image_reference,
ex,
)
return
if color:
service_data[ATTR_RGB_COLOR] = color
await service_call.hass.services.async_call(
LIGHT_DOMAIN, LIGHT_SERVICE_TURN_ON, service_data, blocking=True
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the services."""
hass.services.async_register(
DOMAIN,
SERVICE_TURN_ON,
async_handle_service,
schema=SERVICE_SCHEMA,
)

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