Compare commits

...

201 Commits

Author SHA1 Message Date
ludeeus ea33a8040f Bump hass-nabucasa from 0.106.0 to 0.107.0 2025-07-17 11:43:01 +00:00
Brett Adams bcec29763f Fix button platform parent class in Teslemetry (#148863) 2025-07-16 08:16:36 +02:00
epenet 27ad459ae0 Add tuya snapshots for more humidifiers (cs category) (#148797) 2025-07-16 08:11:55 +02:00
hahn-th 9c933ef01f Add support for HmIPW-DRBL4 in homematicip_cloud (#148844) 2025-07-16 08:10:56 +02:00
Norbert Rittel 2011e64390 Different fixes in user-facing strings of nasweb (#148830) 2025-07-16 08:10:29 +02:00
Brett Adams ffc2b0a8cf Add mock for listen in Teslemetry tests (#148853) 2025-07-16 08:09:54 +02:00
Pete Sage 549069e22c Add guard to prevent exception in Sonos Favorites (#148854) 2025-07-16 08:09:24 +02:00
Jan-Philipp Benecke 57e4270b7b Make exceptions translatable in inexogy integration (#148865) 2025-07-16 08:06:49 +02:00
Maciej Bieniek 38e4e18f60 Bump IMGW-PIB to version 1.4.1 (#148849) 2025-07-16 00:41:56 +01:00
Robert Resch 7f2a32d4eb Remove not needed go2rtc stream config (#148836) 2025-07-15 22:11:55 +01:00
Manu d46e0e132b Add reconfigure flow to Uptime Kuma (#148833) 2025-07-15 22:46:37 +02:00
Álvaro Fernández Rojas 828f0f8b26 Update aioairzone-cloud to v0.6.14 (#148820) 2025-07-15 22:43:40 +02:00
Erik Montnemery 849a25e3cc Handle changes to source entities in mold_indicator helper (#148823)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-07-15 22:19:32 +02:00
Erik Montnemery 3cb579d585 Do not add statistics config entry to source device (#148731) 2025-07-15 22:13:26 +02:00
Erik Montnemery 381bd489d8 Do not add template config entry to source device (#148756) 2025-07-15 22:13:03 +02:00
Marc Mueller f5b785acd5 Update youtubeaio to 2.0.0 (#148814) 2025-07-15 20:44:32 +02:00
Manu 648dce2fa3 Add diagnostics platform to Uptime Kuma (#148835) 2025-07-15 20:19:14 +02:00
Manu d14a0e0191 Bump pythonkuma to v0.3.1 (#148834) 2025-07-15 20:18:47 +02:00
Maciej Bieniek 9caf46c68b Bump imgw_pib library to version 1.4.0 (#148831) 2025-07-15 20:17:54 +02:00
Artur Pragacz e89ae021d8 Clean up validate_supported_features in selector helper (#148843) 2025-07-15 20:10:16 +02:00
Marc Mueller 36156d9c54 Update orjson to 3.11.0 (#148840) 2025-07-15 19:43:44 +02:00
Artur Pragacz 3e0628cec2 Fix entity and device selectors (#148580) 2025-07-15 18:58:42 +02:00
Robert Resch 8bd51a7fd1 Use ffmpeg for generic cameras in go2rtc (#148818) 2025-07-15 16:38:19 +01:00
Paul Bottein 5b29d6bbdf Set icon for off state for light domain (#148749) 2025-07-15 16:25:22 +01:00
karwosts 2c2ac4b669 Throw an error from reload_themes if themes are invalid (#148827)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-15 17:08:19 +02:00
Petro31 35097602d7 Remove unnecessary hass if check in AbstractTemplateEntity (#148828) 2025-07-15 17:04:31 +02:00
Petro31 e5fe243a86 Remove device id references from button and image (#148826) 2025-07-15 17:03:47 +02:00
Manu fd10fa1fba Add reauthentication flow to Uptime Kuma (#148772) 2025-07-15 16:49:08 +02:00
Myles Eftos 087a938a7d Add forecast service to amberelectric (#144848)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-07-15 16:32:59 +02:00
Petro31 c058561162 Add initalize for abstract template entities (#147504) 2025-07-15 15:53:01 +02:00
epenet b89b248b4c Add tuya snapshots for qxj category (#148802) 2025-07-15 14:18:14 +02:00
Samuel Xiao cd94685b7d Add Fan platform to Switchbot cloud (#148304)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-15 13:55:13 +02:00
Matrix 0acfb81d50 Clean up YoLink entities on startup (#148718) 2025-07-15 13:53:29 +02:00
Andrea Turri 7d06aec8da Discovery of Miele temperature sensors (#144585)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-15 12:50:28 +02:00
Åke Strandberg ee4325a927 Replace deprecated battery property on Miele vacuum with sensor (#148765) 2025-07-15 11:40:48 +01:00
Alex Leversen c7aadcdd20 Add file name/size sensors to OctoPrint integration (#148636) 2025-07-15 12:35:20 +02:00
wuede 8256401f7f expose schedule id as an extra state attribute in Netatmo (#147076) 2025-07-15 12:16:59 +02:00
hahn-th ab187f39c2 Add support for HmIP-RGBW and HmIP-LSC in homematicip_cloud integration (#148639) 2025-07-15 12:16:07 +02:00
hahn-th 1cb278966c Handle connection issues after websocket reconnected in homematicip_cloud (#147731) 2025-07-15 12:15:19 +02:00
Josef Zweck b522bd5ef2 Get media player features elsewhere for jellyfin (#148805) 2025-07-15 12:07:57 +02:00
Álvaro Fernández Rojas a6e1d96852 Update aioairzone-cloud to v0.6.13 (#148798)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2025-07-15 11:21:54 +02:00
Marc Mueller 3d74d02704 Update pytouchlinesl to 0.4.0 (#148801) 2025-07-15 11:15:06 +02:00
Artur Pragacz db45f46c8a Fan support in WiZ (#146440) 2025-07-15 11:14:47 +02:00
ehendrix23 4f938d032d Bump elevenlabs to 2.3.0 (#147224) 2025-07-15 10:45:55 +02:00
nasWebio e1f15dac39 Add Sensor platform to NASweb integration (#133063)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-07-15 10:39:53 +02:00
epenet 41e261096a Use suggested unit of measurement in Tuya (#148599) 2025-07-15 10:34:16 +02:00
Max Velitchko f6aa4aa788 Bump amcrest to 1.9.9 (#148769) 2025-07-15 10:14:26 +02:00
Brett Adams 7d7767c93a Bump Tesla Fleet API to 1.2.2 (#148776) 2025-07-15 09:21:00 +02:00
J. Nick Koston 5e883cfb12 Fix flaky nuki tests by preventing teardown race condition (#148795) 2025-07-14 21:03:49 -10:00
Paulus Schoutsen e2cc51f21d Allow AI Task to handle camera attachments (#148753) 2025-07-15 08:51:08 +02:00
Petro31 816977dd75 Refactor async_setup_platform for template platforms (#147379) 2025-07-15 08:26:34 +02:00
kingy444 a81e83cb28 Manually register powerview hub (#146709)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-14 22:38:01 +01:00
Maciej Bieniek c476500c49 Fix Shelly n_current sensor removal condition (#148740) 2025-07-14 23:40:46 +03:00
Matthias Alphart f65fa38429 Add reconfigure flow for KNX (#145067)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-14 21:49:52 +02:00
Manu 66641356cc Add Uptime Kuma integration (#146393) 2025-07-14 21:35:57 +02:00
Willem-Jan van Rootselaar 37ae476c67 Add Zeroconf support for bsblan integration (#146137)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-14 21:26:03 +02:00
Manu 5ec9c4e6e3 Add PS Vita support to PlayStation Network integration (#148186) 2025-07-14 21:24:50 +02:00
Marc Mueller 80eb4fb2f6 Replace asyncio.iscoroutinefunction (#148738) 2025-07-14 20:24:32 +01:00
Joost Lekkerkerker 9e3a78b7ef Bump pySmartThings to 3.2.8 (#148761) 2025-07-14 20:18:12 +01:00
hahn-th c08c402409 Add switches for HmIPW-DRI16, HmIPW-DRI32, HmIPW-DRS4, HmIPW-DRS8 (#148571) 2025-07-14 21:16:29 +02:00
Vincent Wolsink d42d270fb2 Bump Huum to version 0.8.0 (#148763) 2025-07-14 20:16:26 +01:00
fwestenberg 9068a09620 Add Stookwijzer forecast service (#138392)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-14 21:08:16 +02:00
Stephan Traub 1ef07544d5 Fix for ignored devices issue #137114 (#146562) 2025-07-14 21:07:47 +02:00
Erik Montnemery ed4a23d104 Override connect method in RecorderPool (#148490) 2025-07-14 20:57:00 +02:00
peteS-UK 0729b3a2f1 Change hass.data storage to runtime.data for Squeezebox (#146482) 2025-07-14 20:53:53 +02:00
Manu c9356868f7 Add add-on discovery flow to pyLoad integration (#148494) 2025-07-14 19:29:57 +01:00
Abílio Costa 1753baf186 Add method to track entity state changes from target selectors (#148086)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-07-14 19:28:53 +01:00
Petro31 8421ca7802 Add assumed optimistic state to template select (#148513) 2025-07-14 19:28:27 +01:00
Paulus Schoutsen 124931b2ee TTS to always stream when available (#148695)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-07-14 20:23:43 +02:00
Erik Montnemery c27a67db82 Do not add integration config entry to source device (#148730) 2025-07-14 20:18:41 +02:00
Erik Montnemery 3ae9ea3f19 Do not add generic_thermostat config entry to source device (#148728) 2025-07-14 20:18:21 +02:00
Erik Montnemery e35f7b12f1 Do not add generic_hygrostat config entry to source device (#148727) 2025-07-14 20:18:11 +02:00
Erik Montnemery 1a1e9e9f57 Add test for combining state change and state report listeners (#148721) 2025-07-14 20:15:39 +02:00
Erik Montnemery 254f766357 Do not add history_stats config entry to source device (#148729) 2025-07-14 20:05:34 +02:00
Erik Montnemery 7df0016fab Do not add threshold config entry to source device (#148732) 2025-07-14 20:05:20 +02:00
Erik Montnemery 57f89dd606 Do not add trend config entry to source device (#148733) 2025-07-14 20:00:49 +02:00
Erik Montnemery 92bb1f2551 Do not add utility_meter config entry to source device (#148735) 2025-07-14 20:00:21 +02:00
kanshurichard f680e992ff Add support for Broadlink A2 air quality sensor (#142203)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-07-14 19:07:50 +02:00
Thomas55555 f08d1e547f Fix adding a work area in Husqvarna Automower (#148358) 2025-07-14 19:04:00 +02:00
Tsvi Mostovicz 9e022ad75e Quality fixes for Jewish Calendar (#148689) 2025-07-14 17:44:11 +02:00
Paulus Schoutsen 14ff04200e Make AI Task instructions multiline (#148606) 2025-07-14 15:24:44 +01:00
Michael 5e4ce46dae Use absolute humidity device class in Airq (#148568) 2025-07-14 14:38:33 +02:00
Erik Montnemery 155fc134b6 Do not add derivative config entry to source device (#148674)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-07-14 13:33:00 +02:00
ekutner 25f64a2f36 Do not specify the code_format when a code is not required (#148698) 2025-07-14 12:11:36 +02:00
Erik Montnemery dcbdce4b2b Improve docstrings of event helpers related to state changes (#148722) 2025-07-14 11:57:27 +02:00
Michael 50047f0a4e Add new device class for absolute humidity (#148567) 2025-07-14 11:46:17 +02:00
Fredrik Mårtensson 21b1122f83 Add test fixture for Tuya cover (#148660) 2025-07-14 11:43:02 +02:00
Jan Bouwhuis 09104fca4d Fix hide empty sections in mqtt subentry flows (#148692) 2025-07-14 11:26:37 +02:00
Jan Bouwhuis ad4e5459b1 Fix - only enable AlexaModeController if at least one mode is offered (#148614) 2025-07-14 11:25:22 +02:00
Joost Lekkerkerker 334d5f09fb Create Google Generative AI sub entries for an enabled entry (#148161)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-07-14 11:24:00 +02:00
Niccolò Maggioni 9f3d890e91 Bump pysnmp to v7 and brother to v5 (#129761)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2025-07-14 10:46:13 +02:00
Hessel eae9f4f925 Wallbox Integration - Add repair action for insufficient rights (#148610)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-14 10:30:48 +02:00
Brett Adams 5e50c723a7 Fix Charge Cable binary sensor in Teslemetry (#148675) 2025-07-14 10:29:29 +02:00
MattMorgan f761f7628a Minor update to keymitt_ble manifest. (#148708) 2025-07-14 08:50:25 +02:00
karwosts 26d71fcdba Fix derivative migration from 'none' unit_prefix (#147820) 2025-07-14 08:17:20 +02:00
Christopher Fenner e4359e74c6 Bump PyViCare to 2.50.0 (#148679) 2025-07-14 08:08:54 +02:00
Marc Mueller 5e30e6cb91 Update python-mystrom to 2.4.0 (#148682) 2025-07-14 08:02:43 +02:00
Simone Chemelli bc07030304 Bump aioamazondevices to 3.2.10 (#148709) 2025-07-14 01:18:35 +03:00
Shay Levy 25ba2437dd Bump aioshelly to 13.7.2 (#148706) 2025-07-14 01:15:50 +03:00
Kevin Worrel cfc7cfcf37 Bump screenlogicpy to 0.10.2 (#148703) 2025-07-13 11:44:55 -10:00
J. Diego Rodríguez Royo 74288a3bc8 Re-enable Home Connect updates automatically (#148657)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-07-13 22:46:42 +02:00
Marc Mueller b2fe17c6d4 Update PyMicroBot to 0.0.23 (#148700) 2025-07-13 22:12:00 +02:00
Paulus Schoutsen 611f86cf8c OpenAI: Add attachment support to AI task (#148676) 2025-07-13 21:51:46 +02:00
Paulus Schoutsen 23a8442abe Make attachments native to chat log (#148693)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-13 19:35:11 +02:00
Paulus Schoutsen f3ad6bd9b6 Report correctly when no funds for OpenAI (#148677) 2025-07-13 17:55:24 +02:00
Robert Meijers 023dd9d523 Discover Heos players using Zeroconf (#144763) 2025-07-13 09:56:31 -05:00
Steven Tegreeny f7d132b043 Add Z-WAVE discovery entry for the GE/JASCO in-wall smart fan control (#148246) 2025-07-13 13:46:37 +02:00
Brett Adams bb17f34bae Remove history first refresh from Teslemetry (#148531) 2025-07-13 13:01:38 +02:00
Erik Montnemery d22dd68119 Fix exception in EntityRegistry.async_device_modified (#148645) 2025-07-13 10:37:48 +02:00
Alex Leversen 4122af1d33 Bump pyoctoprintapi version to 0.1.14 (#148651) 2025-07-13 09:04:01 +02:00
J. Nick Koston 87fd45d4ab Add device_id parameter to ESPHome command calls for sub-device support (#148667) 2025-07-12 20:12:14 -10:00
asafhas 1c35aff510 Add configuration entities to Tuya multifunction alarm (#148556) 2025-07-13 07:55:37 +02:00
J. Nick Koston ab6ac94af9 Bump aioesphomeapi to 35.0.0 (#148666) 2025-07-12 18:49:59 -10:00
Marc Mueller d33f73fce2 Cleanup bleak warnings (#148665) 2025-07-12 16:26:31 -10:00
Marc Mueller fca6dc264f Update bleak to 1.0.1 (#147742)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-12 13:11:37 -10:00
Amit Finkelstein 5287f4de81 Bump pyatv to 0.16.1 (#148659) 2025-07-12 21:52:26 +01:00
jvits227 ccc1f01ff6 Add lamp states to smartthings selector (#148302)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-07-12 20:51:09 +02:00
falconindy 531f1f1964 snoo: use correct value for right safety clip binary sensor (#148647) 2025-07-12 20:46:03 +02:00
Hessel 72dc2b15d5 Wallbox Add translation to exception config entry auth failed (#148649) 2025-07-12 20:40:39 +02:00
0xEF cf2ef4cec1 Bump nyt_games to 0.5.0 (#148654) 2025-07-12 20:30:26 +02:00
Hessel 28994152ae Wallbox - Add translation to exception (#148644) 2025-07-12 12:24:59 +02:00
AlCalzone ad881d892b Keep entities of dead Z-Wave devices available (#148611) 2025-07-11 23:45:57 +02:00
Paulus Schoutsen 87e641bf59 Update recommended model for Ollama to Qwen3 (#148627) 2025-07-11 16:15:13 -05:00
Paulus Schoutsen 6ecaca753d Update Anthropic max tokens to 3000 and recommended model to claude-3-5-haiku-latest (#148624)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-11 23:00:04 +02:00
Paulus Schoutsen 017cd0bf45 Update OpenAI conversation max tokens to 3000 (#148623)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-11 22:59:51 +02:00
Paulus Schoutsen 1920edd712 Update Google Generative AI Conversation max tokens to 3000 (#148625)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-11 13:10:12 -07:00
Erik Montnemery 2dca78efbb Improve entity registry handling of device changes (#148425) 2025-07-11 20:56:50 +02:00
Lưu Quang Vũ e0179a7d45 Fix Google Cloud 504 Deadline Exceeded (#148589) 2025-07-11 11:53:38 -07:00
Abílio Costa d393d5fdbb Use non-autospec mock for Reolink's util and view tests (#148579) 2025-07-11 16:27:06 +02:00
Joost Lekkerkerker a34264f345 Add SmartThings RVC fixture (#148552) 2025-07-11 14:01:11 +02:00
epenet 73c9d99abf Add tuya snapshot tests for wxkg category (#148609) 2025-07-11 13:55:01 +02:00
Avi Miller ec5991bc68 Add support for LIFX 26"x13" Ceiling (#148459)
Signed-off-by: Avi Miller <me@dje.li>
2025-07-11 13:42:50 +02:00
Arjan 87aecf0ed9 Linkplay: add select entity to set Audio Output hardware (#143329) 2025-07-11 12:45:21 +02:00
Norbert Rittel 0b2ce73eac Fix description of html5.dismiss action (#148591) 2025-07-11 11:43:29 +02:00
Hessel 22828568e2 Wallbox Integration - Type Config Entry (#148594) 2025-07-11 11:37:24 +02:00
Jan Bouwhuis 5a4c837328 Fix entity_id should be based on object_id the first time an entity is added (#148484) 2025-07-11 11:19:54 +02:00
Jan-Philipp Benecke cd73824e3e Ensure response is fully read to prevent premature connection closure in rest command (#148532) 2025-07-11 09:06:18 +02:00
Robin Thoni 32121a073c Add release URL for Tessie updates (#148548)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-11 07:56:23 +02:00
Matrix c6c622797d Add YoLink YS7A12 support (#148588) 2025-07-11 07:55:13 +02:00
jlestel 193b32218f Fix domain validation in Tesla Fleet (#148555) 2025-07-11 00:41:03 +01:00
Paulus Schoutsen e6702d2392 Serialize Object Selector correctly if a field is required (#148577) 2025-07-10 22:45:56 +01:00
Paulus Schoutsen 19b3b6cb28 Add attachment support to Google Gemini (#148208) 2025-07-10 23:45:11 +02:00
Harry Heymann a2220cc2e6 Add LED intensity custom attributes for Matter Inovelli Dimmers (#148074)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-10 23:36:51 +02:00
J. Nick Koston 18a89d5815 Bump aiohttp to 3.12.14 (#148565) 2025-07-10 23:10:48 +02:00
Bram Kragten 6eeec948a8 Update frontend to 20250702.2 (#148573) 2025-07-10 23:09:47 +02:00
Paulus Schoutsen 0e09a47476 Add OpenAI AI Task entity (#148295) 2025-07-10 23:08:56 +02:00
karwosts f0a636949a Support all Energy units in Energy integration (#148566) 2025-07-10 19:29:48 +01:00
Allen Porter d15baf9f9f Drop homeassistant agent and assist_pipeline migration code (#147968)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-07-10 17:30:54 +02:00
epenet 4f27058a68 Add fault binary sensors to tuya dehumidifer (#148485) 2025-07-10 16:15:07 +02:00
epenet 058e1ede10 Add tuya snapshot tests for wsdcg and zndb category (#148554) 2025-07-10 15:55:22 +02:00
epenet d23321cf54 Add tuya snapshot tests for dlq category (#148549) 2025-07-10 15:55:03 +02:00
epenet eb20292683 Move tuya models to separate module (#148550) 2025-07-10 15:54:05 +02:00
Norbert Rittel 12f913e737 Improve names and descriptions of rainmachine.push_weather_data (#148534) 2025-07-10 13:38:42 +02:00
tronikos 7e405d4ddb 100% test coverage in Google Assistant SDK (#148536) 2025-07-10 13:21:19 +02:00
Nathan Spencer 2829cc1248 Add visits today sensor for pets (#147459) 2025-07-10 11:24:54 +01:00
Matrix 8881919efd Add YS8009 support to Yolink (#148538) 2025-07-10 12:10:15 +02:00
Åke Strandberg a00f61f7be Remove vg argument from miele auth flow (#148541) 2025-07-10 12:09:24 +02:00
Josh Barnard c37b0a8f1d Adding precision for voltage and wind speed sensors in Ecowitt (#148462) 2025-07-10 11:21:44 +02:00
Kristof Mariën c75b34a911 Fix for Renson set Breeze fan speed (#148537) 2025-07-10 10:52:03 +02:00
Denis Shulyaka cbe2fbdc34 Encrypted reasoning items support for OpenAI Conversation (#148279) 2025-07-10 10:46:10 +02:00
J. Diego Rodríguez Royo c2bc4a990e Use the link to the issue instead of creating new issues at Home Connect (#148523) 2025-07-10 09:35:30 +02:00
J. Diego Rodríguez Royo 49baa65f61 Add Home Connect resume command button when an appliance is paused (#148512) 2025-07-10 00:26:13 +02:00
Matthias Alphart 24a7ebd2bb Move KNXModule class to separate module (#146100) 2025-07-09 23:51:40 +02:00
Noah Husby a4b9efa1b1 Support AM/FM channel name in Russound RIO (#148421) 2025-07-09 23:23:04 +02:00
Manu 15544769b6 Add action for activity reactions to Bring! (#138175) 2025-07-09 23:08:24 +02:00
Tsvi Mostovicz 3307132441 Jewish calendar: appropriate polling for sensors (2/3) (#144906)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-09 22:50:09 +02:00
Thomas55555 da255af8de Bump aioautomower to 1.2.2 (#148497) 2025-07-09 22:02:31 +02:00
Maciej Bieniek a7e879714b Add water flow sensor to IMGW PIB integration (#148517) 2025-07-09 21:59:08 +02:00
Michael 8aaf5756e0 Add workaround for sub units without main device in AVM Fritz!SmartHome (#148507) 2025-07-09 21:44:50 +02:00
Maciej Bieniek ce5f06b1e5 Add new sensors to GIOS integration (#148510) 2025-07-09 21:43:02 +02:00
Denis Shulyaka e42ca06173 Bump openai to 1.93.3 (#148501) 2025-07-09 21:41:50 +02:00
Thomas55555 2807f057de Fix flaky test in Husqvarna Automower (#148515) 2025-07-09 22:34:37 +03:00
Mickael Goubin 283d0d16c0 Linkplay - when grouped, the first media player returned is the coordinator (#146295)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-07-09 21:33:15 +02:00
Michael 84959a0077 Add platinum quality scale to Pegel Online (#131382) 2025-07-09 21:33:07 +02:00
Michael e012196af8 Bump aioimmich to 0.10.2 (#148503) 2025-07-09 21:22:31 +02:00
Maciej Bieniek 5d43938f0d Bump imgw_pib to version 1.2.0 (#148511) 2025-07-09 21:20:38 +02:00
Nathan Spencer cbdc8e3800 Bump pylitterbot to 2024.2.2 (#148505) 2025-07-09 20:45:45 +02:00
Jan-Philipp Benecke 1b5bbda6b0 Add response headers to action response of rest command (#148480) 2025-07-09 20:37:00 +02:00
G Johansson 57083d877e Add repairs from issue registry to integration diagnostics (#148498) 2025-07-09 18:52:16 +01:00
Petro31 3045f67ae5 Modernize binary sensor template tests (#148367) 2025-07-09 17:49:28 +02:00
Joost Lekkerkerker 6f31057d30 Rework Snapcast config flow tests (#148434) 2025-07-09 16:01:17 +01:00
epenet 511ffdc03c Add tuya snapshot tests for kg category (#148492) 2025-07-09 16:20:29 +02:00
epenet 59fe6da47c Adjust tuya test docstrings (#148493) 2025-07-09 15:59:43 +02:00
epenet e1cdc1af1c Add diagnostics tests to tuya (#148489) 2025-07-09 15:47:48 +02:00
epenet f6e2b962fd Use SnapshotAssertion in lifx diagnostics tests (#148491) 2025-07-09 15:30:17 +02:00
epenet fe0ce9bc6d Use real product_id in tuya fixture (#148415) 2025-07-09 14:44:18 +02:00
Robert Resch b083919031 Revert "Deprecate hddtemp" (#148482) 2025-07-09 13:53:15 +02:00
epenet ef2e699d2c Add tuya snapshot tests for curtain switch (#148465)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-07-09 13:05:53 +02:00
Raphael Hehl 71df8ffe6e Bump uiprotect to version 7.14.2 (#148453) 2025-07-09 12:37:45 +02:00
Joakim Sørensen 98604f09fc Bump hass-nabucasa from 0.105.0 to 0.106.0 (#148473) 2025-07-09 12:30:43 +02:00
Artur Pragacz b97b04661e Improve logging in bootstrap (#148469) 2025-07-09 11:29:56 +01:00
Andrew Jackson 828037de1f Set quality scale on Mealie to silver (#148467) 2025-07-09 11:25:56 +01:00
Norbert Rittel 659504c91f Fix friendly name of increased_non_neutral_output in zha (#148468) 2025-07-09 12:24:44 +02:00
Franck Nijhof 434ac421d1 Tiny tweaks to task form (#148475) 2025-07-09 12:04:00 +02:00
Denis Shulyaka de849b920a Enable web search for OpenAI reasoning models (#148393) 2025-07-09 10:54:49 +02:00
Artur Pragacz e387d4834f Fix unloading update listener in Unifi (#148471) 2025-07-09 10:44:21 +02:00
Artur Pragacz 39ed877a17 Fix unloading update listener in Axis (#148470) 2025-07-09 10:43:55 +02:00
epenet 13d05a338b Sort tuya definitions by category (#148472) 2025-07-09 10:42:55 +02:00
Avi Miller cb2095bcbe Bump aiolifx to 1.2.1 (#148464)
Signed-off-by: Avi Miller <me@dje.li>
2025-07-09 09:43:29 +02:00
Norbert Rittel 6de630ef3e Fix sentence-casing of trigger subtypes in xiaomi_ble (#148463) 2025-07-09 10:43:22 +03:00
Rico Hageman a02359b25d Add dew point to Awair integration (#148403) 2025-07-09 09:28:55 +02:00
707 changed files with 36028 additions and 7980 deletions
+4 -2
View File
@@ -21,7 +21,7 @@ body:
- type: textarea
id: description
attributes:
label: Task description
label: Description
description: |
Provide a clear and detailed description of the task that needs to be accomplished.
@@ -43,9 +43,11 @@ body:
Include links to related issues, research, prototypes, roadmap opportunities etc.
placeholder: |
- Roadmap opportunity: [links]
- Roadmap opportunity: [link]
- Epic: [link]
- Feature request: [link]
- Technical design documents: [link]
- Prototype/mockup: [link]
- Dependencies: [links]
validations:
required: false
+1
View File
@@ -535,6 +535,7 @@ homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.*
homeassistant.components.update.*
homeassistant.components.uptime.*
homeassistant.components.uptime_kuma.*
homeassistant.components.uptimerobot.*
homeassistant.components.usb.*
homeassistant.components.uvc.*
Generated
+4 -2
View File
@@ -1658,6 +1658,8 @@ build.json @home-assistant/supervisor
/tests/components/upnp/ @StevenLooman
/homeassistant/components/uptime/ @frenck
/tests/components/uptime/ @frenck
/homeassistant/components/uptime_kuma/ @tr4nt0r
/tests/components/uptime_kuma/ @tr4nt0r
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
/tests/components/uptimerobot/ @ludeeus @chemelli74
/homeassistant/components/usb/ @bdraco
@@ -1756,8 +1758,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/wirelesstag/ @sergeymaysak
/homeassistant/components/withings/ @joostlek
/tests/components/withings/ @joostlek
/homeassistant/components/wiz/ @sbidy
/tests/components/wiz/ @sbidy
/homeassistant/components/wiz/ @sbidy @arturpragacz
/tests/components/wiz/ @sbidy @arturpragacz
/homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck
/homeassistant/components/wmspro/ @mback2k
+11 -9
View File
@@ -332,6 +332,9 @@ async def async_setup_hass(
if not is_virtual_env():
await async_mount_local_lib_path(runtime_config.config_dir)
if hass.config.safe_mode:
_LOGGER.info("Starting in safe mode")
basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None
)
@@ -384,8 +387,6 @@ async def async_setup_hass(
{"recovery_mode": {}, "http": http_conf},
hass,
)
elif hass.config.safe_mode:
_LOGGER.info("Starting in safe mode")
if runtime_config.open_ui:
hass.add_job(open_hass_ui, hass)
@@ -870,9 +871,9 @@ async def _async_set_up_integrations(
domains = set(integrations) & all_domains
_LOGGER.info(
"Domains to be set up: %s | %s",
domains,
all_domains - domains,
"Domains to be set up: %s\nDependencies: %s",
domains or "{}",
(all_domains - domains) or "{}",
)
async_set_domains_to_be_loaded(hass, all_domains)
@@ -913,12 +914,13 @@ async def _async_set_up_integrations(
stage_all_domains = stage_domains | stage_dep_domains
_LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
"Setting up stage %s: %s; already set up: %s\n"
"Dependencies: %s; already set up: %s",
name,
stage_domains,
stage_domains_unfiltered - stage_domains,
stage_dep_domains,
stage_dep_domains_unfiltered - stage_dep_domains,
(stage_domains_unfiltered - stage_domains) or "{}",
stage_dep_domains or "{}",
(stage_dep_domains_unfiltered - stage_dep_domains) or "{}",
)
if timeout is None:
+1 -2
View File
@@ -33,7 +33,7 @@ from .const import (
)
from .entity import AITaskEntity
from .http import async_setup as async_setup_http
from .task import GenDataTask, GenDataTaskResult, PlayMediaWithId, async_generate_data
from .task import GenDataTask, GenDataTaskResult, async_generate_data
__all__ = [
"DOMAIN",
@@ -41,7 +41,6 @@ __all__ = [
"AITaskEntityFeature",
"GenDataTask",
"GenDataTaskResult",
"PlayMediaWithId",
"async_generate_data",
"async_setup",
"async_setup_entry",
+7 -4
View File
@@ -13,7 +13,7 @@ from homeassistant.components.conversation import (
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.helpers import llm
from homeassistant.helpers.chat_session import async_get_chat_session
from homeassistant.helpers.chat_session import ChatSession
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import dt as dt_util
@@ -56,12 +56,12 @@ class AITaskEntity(RestoreEntity):
@contextlib.asynccontextmanager
async def _async_get_ai_task_chat_log(
self,
session: ChatSession,
task: GenDataTask,
) -> AsyncGenerator[ChatLog]:
"""Context manager used to manage the ChatLog used during an AI Task."""
# pylint: disable-next=contextmanager-generator-missing-cleanup
with (
async_get_chat_session(self.hass) as session,
async_get_chat_log(
self.hass,
session,
@@ -79,19 +79,22 @@ class AITaskEntity(RestoreEntity):
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
)
chat_log.async_add_user_content(UserContent(task.instructions))
chat_log.async_add_user_content(
UserContent(task.instructions, attachments=task.attachments)
)
yield chat_log
@final
async def internal_async_generate_data(
self,
session: ChatSession,
task: GenDataTask,
) -> GenDataTaskResult:
"""Run a gen data task."""
self.__last_activity = dt_util.utcnow().isoformat()
self.async_write_ha_state()
async with self._async_get_ai_task_chat_log(task) as chat_log:
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
return await self._async_generate_data(task, chat_log)
async def _async_generate_data(
@@ -1,6 +1,7 @@
{
"domain": "ai_task",
"name": "AI Task",
"after_dependencies": ["camera"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["conversation", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/ai_task",
@@ -10,13 +10,15 @@ generate_data:
required: true
selector:
text:
multiline: true
entity_id:
required: false
selector:
entity:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
filter:
domain: ai_task
supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
structure:
advanced: true
required: false
+82 -38
View File
@@ -2,28 +2,31 @@
from __future__ import annotations
from dataclasses import dataclass, fields
from dataclasses import dataclass
import mimetypes
from pathlib import Path
import tempfile
from typing import Any
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.core import HomeAssistant
from homeassistant.components import camera, conversation, media_source
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.chat_session import async_get_chat_session
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
@dataclass(slots=True)
class PlayMediaWithId(media_source.PlayMedia):
"""Play media with a media content ID."""
media_content_id: str
"""Media source ID to play."""
def __str__(self) -> str:
"""Return media source ID as a string."""
return f"<PlayMediaWithId {self.media_content_id}>"
def _save_camera_snapshot(image: camera.Image) -> Path:
"""Save camera snapshot to temp file."""
with tempfile.NamedTemporaryFile(
mode="wb",
suffix=mimetypes.guess_extension(image.content_type, False),
delete=False,
) as temp_file:
temp_file.write(image.content)
return Path(temp_file.name)
async def async_generate_data(
@@ -52,38 +55,79 @@ async def async_generate_data(
)
# Resolve attachments
resolved_attachments: list[PlayMediaWithId] | None = None
resolved_attachments: list[conversation.Attachment] = []
created_files: list[Path] = []
if attachments:
if AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features:
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
if (
attachments
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
):
raise HomeAssistantError(
f"AI Task entity {entity_id} does not support attachments"
)
for attachment in attachments or []:
media_content_id = attachment["media_content_id"]
# Special case for camera media sources
if media_content_id.startswith("media-source://camera/"):
# Extract entity_id from the media content ID
entity_id = media_content_id.removeprefix("media-source://camera/")
# Get snapshot from camera
image = await camera.async_get_image(hass, entity_id)
temp_filename = await hass.async_add_executor_job(
_save_camera_snapshot, image
)
created_files.append(temp_filename)
resolved_attachments = []
for attachment in attachments:
media = await media_source.async_resolve_media(
hass, attachment["media_content_id"], None
)
resolved_attachments.append(
PlayMediaWithId(
**{
field.name: getattr(media, field.name)
for field in fields(media)
},
media_content_id=attachment["media_content_id"],
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image.content_type,
path=temp_filename,
)
)
else:
# Handle regular media sources
media = await media_source.async_resolve_media(hass, media_content_id, None)
if media.path is None:
raise HomeAssistantError(
"Only local attachments are currently supported"
)
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=media.mime_type,
path=media.path,
)
)
return await entity.internal_async_generate_data(
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
attachments=resolved_attachments,
with async_get_chat_session(hass) as session:
if created_files:
def cleanup_files() -> None:
"""Cleanup temporary files."""
for file in created_files:
file.unlink(missing_ok=True)
@callback
def cleanup_files_callback() -> None:
"""Cleanup temporary files."""
hass.async_add_executor_job(cleanup_files)
session.async_on_cleanup(cleanup_files_callback)
return await entity.internal_async_generate_data(
session,
GenDataTask(
name=task_name,
instructions=instructions,
structure=structure,
attachments=resolved_attachments or None,
),
)
)
@dataclass(slots=True)
@@ -99,7 +143,7 @@ class GenDataTask:
structure: vol.Schema | None = None
"""Optional structure for the data to be generated."""
attachments: list[PlayMediaWithId] | None = None
attachments: list[conversation.Attachment] | None = None
"""List of attachments to go along the instructions."""
def __str__(self) -> str:
-1
View File
@@ -6,6 +6,5 @@ CONF_RETURN_AVERAGE: Final = "return_average"
CONF_CLIP_NEGATIVE: Final = "clip_negatives"
DOMAIN: Final = "airq"
MANUFACTURER: Final = "CorantGmbH"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
UPDATE_INTERVAL: float = 10.0
-3
View File
@@ -4,9 +4,6 @@
"health_index": {
"default": "mdi:heart-pulse"
},
"absolute_humidity": {
"default": "mdi:water"
},
"oxygen": {
"default": "mdi:leaf"
},
+3 -5
View File
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@@ -28,10 +29,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirQConfigEntry, AirQCoordinator
from .const import (
ACTIVITY_BECQUEREL_PER_CUBIC_METER,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
)
from .const import ACTIVITY_BECQUEREL_PER_CUBIC_METER
_LOGGER = logging.getLogger(__name__)
@@ -195,7 +193,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
),
AirQEntityDescription(
key="humidity_abs",
translation_key="absolute_humidity",
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
@@ -93,9 +93,6 @@
"health_index": {
"name": "Health index"
},
"absolute_humidity": {
"name": "Absolute humidity"
},
"hydrogen": {
"name": "Hydrogen"
},
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.12"]
"requirements": ["aioairzone-cloud==0.6.14"]
}
+18 -5
View File
@@ -505,8 +505,13 @@ class ClimateCapabilities(AlexaEntity):
):
yield AlexaThermostatController(self.hass, self.entity)
yield AlexaTemperatureSensor(self.hass, self.entity)
if self.entity.domain == water_heater.DOMAIN and (
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
if (
self.entity.domain == water_heater.DOMAIN
and (
supported_features
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
)
and self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST)
):
yield AlexaModeController(
self.entity,
@@ -634,7 +639,9 @@ class FanCapabilities(AlexaEntity):
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
)
force_range_controller = False
if supported & fan.FanEntityFeature.PRESET_MODE:
if supported & fan.FanEntityFeature.PRESET_MODE and self.entity.attributes.get(
fan.ATTR_PRESET_MODES
):
yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
)
@@ -672,7 +679,11 @@ class RemoteCapabilities(AlexaEntity):
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
if (
activities
and (supported & remote.RemoteEntityFeature.ACTIVITY)
and self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
):
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
@@ -692,7 +703,9 @@ class HumidifierCapabilities(AlexaEntity):
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & humidifier.HumidifierEntityFeature.MODES:
if (
supported & humidifier.HumidifierEntityFeature.MODES
) and self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES):
yield AlexaModeController(
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
)
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "silver",
"requirements": ["aioamazondevices==3.2.8"]
"requirements": ["aioamazondevices==3.2.10"]
}
@@ -2,11 +2,22 @@
import amberelectric
from homeassistant.components.sensor import ConfigType
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from .const import CONF_SITE_ID, PLATFORMS
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
from .services import setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Amber component."""
setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
@@ -1,14 +1,24 @@
"""Amber Electric Constants."""
import logging
from typing import Final
from homeassistant.const import Platform
DOMAIN = "amberelectric"
DOMAIN: Final = "amberelectric"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_CHANNEL_TYPE = "channel_type"
ATTRIBUTION = "Data provided by Amber Electric"
LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
SERVICE_GET_FORECASTS = "get_forecasts"
GENERAL_CHANNEL = "general"
CONTROLLED_LOAD_CHANNEL = "controlled_load"
FEED_IN_CHANNEL = "feed_in"
@@ -10,7 +10,6 @@ from amberelectric.models.actual_interval import ActualInterval
from amberelectric.models.channel import ChannelType
from amberelectric.models.current_interval import CurrentInterval
from amberelectric.models.forecast_interval import ForecastInterval
from amberelectric.models.price_descriptor import PriceDescriptor
from amberelectric.rest import ApiException
from homeassistant.config_entries import ConfigEntry
@@ -18,6 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
from .helpers import normalize_descriptor
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
@@ -49,27 +49,6 @@ def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) ->
return interval.channel_type == ChannelType.FEEDIN
def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
"""Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
if descriptor is None:
return None
if descriptor.value == "spike":
return "spike"
if descriptor.value == "high":
return "high"
if descriptor.value == "neutral":
return "neutral"
if descriptor.value == "low":
return "low"
if descriptor.value == "veryLow":
return "very_low"
if descriptor.value == "extremelyLow":
return "extremely_low"
if descriptor.value == "negative":
return "negative"
return None
class AmberUpdateCoordinator(DataUpdateCoordinator):
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
@@ -103,7 +82,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"grid": {},
}
try:
data = self._api.get_current_prices(self.site_id, next=48)
data = self._api.get_current_prices(self.site_id, next=288)
intervals = [interval.actual_instance for interval in data]
except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception
@@ -0,0 +1,25 @@
"""Formatting helpers used to convert things."""
from amberelectric.models.price_descriptor import PriceDescriptor
DESCRIPTOR_MAP: dict[str, str] = {
PriceDescriptor.SPIKE: "spike",
PriceDescriptor.HIGH: "high",
PriceDescriptor.NEUTRAL: "neutral",
PriceDescriptor.LOW: "low",
PriceDescriptor.VERYLOW: "very_low",
PriceDescriptor.EXTREMELYLOW: "extremely_low",
PriceDescriptor.NEGATIVE: "negative",
}
def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
"""Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
if descriptor in DESCRIPTOR_MAP:
return DESCRIPTOR_MAP[descriptor]
return None
def format_cents_to_dollars(cents: float) -> float:
"""Return a formatted conversion from cents to dollars."""
return round(cents / 100, 2)
@@ -22,5 +22,10 @@
}
}
}
},
"services": {
"get_forecasts": {
"service": "mdi:transmission-tower"
}
}
}
@@ -23,16 +23,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator, normalize_descriptor
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
from .helpers import format_cents_to_dollars, normalize_descriptor
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
def format_cents_to_dollars(cents: float) -> float:
"""Return a formatted conversion from cents to dollars."""
return round(cents / 100, 2)
def friendly_channel_type(channel_type: str) -> str:
"""Return a human readable version of the channel type."""
if channel_type == "controlled_load":
@@ -0,0 +1,121 @@
"""Amber Electric Service class."""
from amberelectric.models.channel import ChannelType
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.util.json import JsonValueType
from .const import (
ATTR_CHANNEL_TYPE,
ATTR_CONFIG_ENTRY_ID,
CONTROLLED_LOAD_CHANNEL,
DOMAIN,
FEED_IN_CHANNEL,
GENERAL_CHANNEL,
SERVICE_GET_FORECASTS,
)
from .coordinator import AmberConfigEntry
from .helpers import format_cents_to_dollars, normalize_descriptor
GET_FORECASTS_SCHEMA = vol.Schema(
{
ATTR_CONFIG_ENTRY_ID: ConfigEntrySelector({"integration": DOMAIN}),
ATTR_CHANNEL_TYPE: vol.In(
[GENERAL_CHANNEL, CONTROLLED_LOAD_CHANNEL, FEED_IN_CHANNEL]
),
}
)
def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> AmberConfigEntry:
"""Get the Amber config entry."""
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_not_found",
translation_placeholders={"target": config_entry_id},
)
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": entry.title},
)
return entry
def get_forecasts(channel_type: str, data: dict) -> list[JsonValueType]:
"""Return an array of forecasts."""
results: list[JsonValueType] = []
if channel_type not in data["forecasts"]:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="channel_not_found",
translation_placeholders={"channel_type": channel_type},
)
intervals = data["forecasts"][channel_type]
for interval in intervals:
datum = {}
datum["duration"] = interval.duration
datum["date"] = interval.var_date.isoformat()
datum["nem_date"] = interval.nem_time.isoformat()
datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
if interval.channel_type == ChannelType.FEEDIN:
datum["per_kwh"] = datum["per_kwh"] * -1
datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
datum["start_time"] = interval.start_time.isoformat()
datum["end_time"] = interval.end_time.isoformat()
datum["renewables"] = round(interval.renewables)
datum["spike_status"] = interval.spike_status.value
datum["descriptor"] = normalize_descriptor(interval.descriptor)
if interval.range is not None:
datum["range_min"] = format_cents_to_dollars(interval.range.min)
datum["range_max"] = format_cents_to_dollars(interval.range.max)
if interval.advanced_price is not None:
multiplier = -1 if interval.channel_type == ChannelType.FEEDIN else 1
datum["advanced_price_low"] = multiplier * format_cents_to_dollars(
interval.advanced_price.low
)
datum["advanced_price_predicted"] = multiplier * format_cents_to_dollars(
interval.advanced_price.predicted
)
datum["advanced_price_high"] = multiplier * format_cents_to_dollars(
interval.advanced_price.high
)
results.append(datum)
return results
def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Amber integration."""
async def handle_get_forecasts(call: ServiceCall) -> ServiceResponse:
channel_type = call.data[ATTR_CHANNEL_TYPE]
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
coordinator = entry.runtime_data
forecasts = get_forecasts(channel_type, coordinator.data)
return {"forecasts": forecasts}
hass.services.async_register(
DOMAIN,
SERVICE_GET_FORECASTS,
handle_get_forecasts,
GET_FORECASTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
@@ -0,0 +1,16 @@
get_forecasts:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: amberelectric
channel_type:
required: true
selector:
select:
options:
- general
- controlled_load
- feed_in
translation_key: channel_type
@@ -1,25 +1,61 @@
{
"config": {
"error": {
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
"no_site": "No site provided",
"unknown_error": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"site": {
"data": {
"site_id": "Site NMI",
"site_name": "Site name"
},
"description": "Select the NMI of the site you would like to add"
},
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]",
"site_id": "Site ID"
},
"description": "Go to {api_url} to generate an API key"
},
"site": {
"data": {
"site_id": "Site NMI",
"site_name": "Site Name"
},
"description": "Select the NMI of the site you would like to add"
}
}
},
"services": {
"get_forecasts": {
"name": "Get price forecasts",
"description": "Retrieves price forecasts from Amber Electric for a site.",
"fields": {
"config_entry_id": {
"description": "The config entry of the site to get forecasts for.",
"name": "Config entry"
},
"channel_type": {
"name": "Channel type",
"description": "The channel to get forecasts for."
}
}
}
},
"exceptions": {
"integration_not_found": {
"message": "Config entry \"{target}\" not found in registry."
},
"error": {
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
"no_site": "No site provided",
"unknown_error": "[%key:common::config_flow::error::unknown%]"
"not_loaded": {
"message": "{target} is not loaded."
},
"channel_not_found": {
"message": "There is no {channel_type} channel at this site."
}
},
"selector": {
"channel_type": {
"options": {
"general": "General",
"controlled_load": "Controlled load",
"feed_in": "Feed-in"
}
}
}
}
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["amcrest"],
"quality_scale": "legacy",
"requirements": ["amcrest==1.9.8"]
"requirements": ["amcrest==1.9.9"]
}
+2 -2
View File
@@ -10,9 +10,9 @@ DEFAULT_CONVERSATION_NAME = "Claude conversation"
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
RECOMMENDED_CHAT_MODEL = "claude-3-5-haiku-latest"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
RECOMMENDED_MAX_TOKENS = 3000
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_THINKING_BUDGET = "thinking_budget"
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.0"],
"requirements": ["pyatv==0.16.1"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
@@ -38,8 +38,6 @@ from .pipeline import (
async_create_default_pipeline,
async_get_pipeline,
async_get_pipelines,
async_migrate_engine,
async_run_migrations,
async_setup_pipeline_store,
async_update_pipeline,
)
@@ -61,7 +59,6 @@ __all__ = (
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",
"async_migrate_engine",
"async_pipeline_from_audio_stream",
"async_setup",
"async_update_pipeline",
@@ -87,7 +84,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.data[DATA_LAST_WAKE_UP] = {}
await async_setup_pipeline_store(hass)
await async_run_migrations(hass)
async_register_websocket_api(hass)
return True
@@ -3,7 +3,6 @@
DOMAIN = "assist_pipeline"
DATA_CONFIG = f"{DOMAIN}.config"
DATA_MIGRATIONS = f"{DOMAIN}_migrations"
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
@@ -13,7 +13,7 @@ from pathlib import Path
from queue import Empty, Queue
from threading import Thread
import time
from typing import TYPE_CHECKING, Any, Literal, cast
from typing import TYPE_CHECKING, Any, cast
import wave
import hass_nabucasa
@@ -49,7 +49,6 @@ from .const import (
CONF_DEBUG_RECORDING_DIR,
DATA_CONFIG,
DATA_LAST_WAKE_UP,
DATA_MIGRATIONS,
DOMAIN,
MS_PER_CHUNK,
SAMPLE_CHANNELS,
@@ -2059,50 +2058,6 @@ async def async_setup_pipeline_store(hass: HomeAssistant) -> PipelineData:
return PipelineData(pipeline_store)
@callback
def async_migrate_engine(
hass: HomeAssistant,
engine_type: Literal["conversation", "stt", "tts", "wake_word"],
old_value: str,
new_value: str,
) -> None:
"""Register a migration of an engine used in pipelines."""
hass.data.setdefault(DATA_MIGRATIONS, {})[engine_type] = (old_value, new_value)
# Run migrations when config is already loaded
if DATA_CONFIG in hass.data:
hass.async_create_background_task(
async_run_migrations(hass), "assist_pipeline_migration", eager_start=True
)
async def async_run_migrations(hass: HomeAssistant) -> None:
"""Run pipeline migrations."""
if not (migrations := hass.data.get(DATA_MIGRATIONS)):
return
engine_attr = {
"conversation": "conversation_engine",
"stt": "stt_engine",
"tts": "tts_engine",
"wake_word": "wake_word_entity",
}
updates = []
for pipeline in async_get_pipelines(hass):
attr_updates = {}
for engine_type, (old_value, new_value) in migrations.items():
if getattr(pipeline, engine_attr[engine_type]) == old_value:
attr_updates[engine_attr[engine_type]] = new_value
if attr_updates:
updates.append((pipeline, attr_updates))
for pipeline, attr_updates in updates:
await async_update_pipeline(hass, pipeline, **attr_updates)
@dataclass
class PipelineConversationData:
"""Hold data for the duration of a conversation."""
@@ -68,9 +68,10 @@ ask_question:
required: true
selector:
entity:
domain: assist_satellite
supported_features:
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
filter:
domain: assist_satellite
supported_features:
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
question:
required: false
example: "What kind of music would you like to play?"
+1
View File
@@ -6,6 +6,7 @@ from datetime import timedelta
import logging
API_CO2 = "carbon_dioxide"
API_DEW_POINT = "dew_point"
API_DUST = "dust"
API_HUMID = "humidity"
API_LUX = "illuminance"
+10
View File
@@ -34,6 +34,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
API_CO2,
API_DEW_POINT,
API_DUST,
API_HUMID,
API_LUX,
@@ -110,6 +111,15 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
unique_id_tag="CO2", # matches legacy format
state_class=SensorStateClass.MEASUREMENT,
),
AwairSensorEntityDescription(
key=API_DEW_POINT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
translation_key="dew_point",
unique_id_tag="dew_point",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
)
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
@@ -57,6 +57,9 @@
},
"sound_level": {
"name": "Sound level"
},
"dew_point": {
"name": "Dew point"
}
}
}
+3 -1
View File
@@ -30,7 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hub.setup()
config_entry.add_update_listener(hub.async_new_address_callback)
config_entry.async_on_unload(
config_entry.add_update_listener(hub.async_new_address_callback)
)
config_entry.async_on_unload(hub.teardown)
config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown)
@@ -15,12 +15,12 @@
],
"quality_scale": "internal",
"requirements": [
"bleak==0.22.3",
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bleak==1.0.1",
"bleak-retry-connector==4.0.0",
"bluetooth-adapters==2.0.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.43.0",
"habluetooth==3.49.0"
"habluetooth==4.0.1"
]
}
@@ -8,20 +8,33 @@ from bring_api import Bring
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .coordinator import (
BringActivityCoordinator,
BringConfigEntry,
BringCoordinators,
BringDataUpdateCoordinator,
)
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Bring! services."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
"""Set up Bring! from a config entry."""
+4 -1
View File
@@ -7,5 +7,8 @@ DOMAIN = "bring"
ATTR_SENDER: Final = "sender"
ATTR_ITEM_NAME: Final = "item"
ATTR_NOTIFICATION_TYPE: Final = "message"
ATTR_REACTION: Final = "reaction"
ATTR_ACTIVITY: Final = "uuid"
ATTR_RECEIVER: Final = "publicUserUuid"
SERVICE_PUSH_NOTIFICATION = "send_message"
SERVICE_ACTIVITY_STREAM_REACTION = "send_reaction"
@@ -35,6 +35,9 @@
"services": {
"send_message": {
"service": "mdi:cellphone-message"
},
"send_reaction": {
"service": "mdi:thumb-up"
}
}
}
+110
View File
@@ -0,0 +1,110 @@
"""Actions for Bring! integration."""
import logging
from typing import TYPE_CHECKING
from bring_api import (
ActivityType,
BringAuthException,
BringNotificationType,
BringRequestException,
ReactionType,
)
import voluptuous as vol
from homeassistant.components.event import ATTR_EVENT_TYPE
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from .const import (
ATTR_ACTIVITY,
ATTR_REACTION,
ATTR_RECEIVER,
DOMAIN,
SERVICE_ACTIVITY_STREAM_REACTION,
)
from .coordinator import BringConfigEntry
_LOGGER = logging.getLogger(__name__)
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_REACTION): vol.All(
vol.Upper,
vol.Coerce(ReactionType),
),
}
)
def get_config_entry(hass: HomeAssistant, entry_id: str) -> BringConfigEntry:
"""Return config entry or raise if not found or not loaded."""
entry = hass.config_entries.async_get_entry(entry_id)
if TYPE_CHECKING:
assert entry
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
return entry
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Bring! integration."""
async def async_send_activity_stream_reaction(call: ServiceCall) -> None:
"""Send a reaction in response to recent activity of a list member."""
if (
not (state := hass.states.get(call.data[ATTR_ENTITY_ID]))
or not (entity := er.async_get(hass).async_get(call.data[ATTR_ENTITY_ID]))
or not entity.config_entry_id
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={
ATTR_ENTITY_ID: call.data[ATTR_ENTITY_ID],
},
)
config_entry = get_config_entry(hass, entity.config_entry_id)
coordinator = config_entry.runtime_data.data
list_uuid = entity.unique_id.split("_")[1]
activity = state.attributes[ATTR_EVENT_TYPE]
reaction: ReactionType = call.data[ATTR_REACTION]
if not activity:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="activity_not_found",
)
try:
await coordinator.bring.notify(
list_uuid,
BringNotificationType.LIST_ACTIVITY_STREAM_REACTION,
receiver=state.attributes[ATTR_RECEIVER],
activity=state.attributes[ATTR_ACTIVITY],
activity_type=ActivityType(activity.upper()),
reaction=reaction,
)
except (BringRequestException, BringAuthException) as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="reaction_request_failed",
) from e
hass.services.async_register(
DOMAIN,
SERVICE_ACTIVITY_STREAM_REACTION,
async_send_activity_stream_reaction,
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA,
)
@@ -21,3 +21,28 @@ send_message:
required: false
selector:
text:
send_reaction:
fields:
entity_id:
required: true
selector:
entity:
filter:
- integration: bring
domain: event
example: event.shopping_list
reaction:
required: true
selector:
select:
options:
- label: 👍🏼
value: thumbs_up
- label: 🧐
value: monocle
- label: 🤤
value: drooling
- label: ❤️
value: heart
mode: dropdown
example: thumbs_up
@@ -144,6 +144,19 @@
},
"notify_request_failed": {
"message": "Failed to send push notification for Bring! due to a connection error, try again later"
},
"reaction_request_failed": {
"message": "Failed to send reaction for Bring! due to a connection error, try again later"
},
"activity_not_found": {
"message": "Failed to send reaction for Bring! — No recent activity found"
},
"entity_not_found": {
"message": "Failed to send reaction for Bring! — Unknown entity {entity_id}"
},
"entry_not_loaded": {
"message": "The account associated with this Bring! list is either not loaded or disabled in Home Assistant."
}
},
"services": {
@@ -164,6 +177,20 @@
"description": "Item name(s) to include in an urgent message e.g. 'Attention! Attention! - We still urgently need: [Items]'"
}
}
},
"send_reaction": {
"name": "Send reaction",
"description": "Sends a reaction to a recent activity on a Bring! list by a member of the shared list.",
"fields": {
"entity_id": {
"name": "Activities",
"description": "Select the Bring! activities event entity for reacting to its most recent event"
},
"reaction": {
"name": "Reaction",
"description": "Type of reaction to send in response."
}
}
}
},
"selector": {
@@ -11,6 +11,7 @@ DOMAINS_AND_TYPES = {
Platform.SELECT: {"HYS"},
Platform.SENSOR: {
"A1",
"A2",
"MP1S",
"RM4MINI",
"RM4PRO",
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
PERCENTAGE,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -34,6 +35,24 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
key="air_quality",
device_class=SensorDeviceClass.AQI,
),
SensorEntityDescription(
key="pm10",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pm2_5",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pm1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="humidity",
native_unit_of_measurement=PERCENTAGE,
@@ -25,6 +25,7 @@ def get_update_manager(device: BroadlinkDevice[_ApiT]) -> BroadlinkUpdateManager
"""Return an update manager for a given Broadlink device."""
update_managers: dict[str, type[BroadlinkUpdateManager]] = {
"A1": BroadlinkA1UpdateManager,
"A2": BroadlinkA2UpdateManager,
"BG1": BroadlinkBG1UpdateManager,
"HYS": BroadlinkThermostatUpdateManager,
"LB1": BroadlinkLB1UpdateManager,
@@ -118,6 +119,16 @@ class BroadlinkA1UpdateManager(BroadlinkUpdateManager[blk.a1]):
return await self.device.async_request(self.device.api.check_sensors_raw)
class BroadlinkA2UpdateManager(BroadlinkUpdateManager[blk.a2]):
"""Manages updates for Broadlink A2 devices."""
SCAN_INTERVAL = timedelta(seconds=10)
async def async_fetch_data(self) -> dict[str, Any]:
"""Fetch data from the device."""
return await self.device.async_request(self.device.api.check_sensors_raw)
class BroadlinkMP1UpdateManager(BroadlinkUpdateManager[blk.mp1]):
"""Manages updates for Broadlink MP1 devices."""
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"requirements": ["brother==4.3.1"],
"requirements": ["brother==5.0.0"],
"zeroconf": [
{
"type": "_printer._tcp.local.",
+129 -13
View File
@@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_PASSKEY, DEFAULT_PORT, DOMAIN
@@ -21,12 +22,15 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
host: str
port: int
mac: str
passkey: str | None = None
username: str | None = None
password: str | None = None
def __init__(self) -> None:
"""Initialize BSBLan flow."""
self.host: str | None = None
self.port: int = DEFAULT_PORT
self.mac: str | None = None
self.passkey: str | None = None
self.username: str | None = None
self.password: str | None = None
self._auth_required = True
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -41,9 +45,111 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
self.username = user_input.get(CONF_USERNAME)
self.password = user_input.get(CONF_PASSWORD)
return await self._validate_and_create()
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle Zeroconf discovery."""
self.host = str(discovery_info.ip_address)
self.port = discovery_info.port or DEFAULT_PORT
# Get MAC from properties
self.mac = discovery_info.properties.get("mac")
# If MAC was found in zeroconf, use it immediately
if self.mac:
await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self.host,
CONF_PORT: self.port,
}
)
else:
# MAC not available from zeroconf - check for existing host/port first
self._async_abort_entries_match(
{CONF_HOST: self.host, CONF_PORT: self.port}
)
# Try to get device info without authentication to minimize discovery popup
config = BSBLANConfig(host=self.host, port=self.port)
session = async_get_clientsession(self.hass)
bsblan = BSBLAN(config, session)
try:
device = await bsblan.device()
except BSBLANError:
# Device requires authentication - proceed to discovery confirm
self.mac = None
else:
self.mac = device.MAC
# Got MAC without auth - set unique ID and check for existing device
await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self.host,
CONF_PORT: self.port,
}
)
# No auth needed, so we can proceed to a confirmation step without fields
self._auth_required = False
# Proceed to get credentials
self.context["title_placeholders"] = {"name": f"BSBLAN {self.host}"}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle getting credentials for discovered device."""
if user_input is None:
data_schema = vol.Schema(
{
vol.Optional(CONF_PASSKEY): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
}
)
if not self._auth_required:
data_schema = vol.Schema({})
return self.async_show_form(
step_id="discovery_confirm",
data_schema=data_schema,
description_placeholders={"host": str(self.host)},
)
if not self._auth_required:
return self._async_create_entry()
self.passkey = user_input.get(CONF_PASSKEY)
self.username = user_input.get(CONF_USERNAME)
self.password = user_input.get(CONF_PASSWORD)
return await self._validate_and_create(is_discovery=True)
async def _validate_and_create(
self, is_discovery: bool = False
) -> ConfigFlowResult:
"""Validate device connection and create entry."""
try:
await self._get_bsblan_info()
await self._get_bsblan_info(is_discovery=is_discovery)
except BSBLANError:
if is_discovery:
return self.async_show_form(
step_id="discovery_confirm",
data_schema=vol.Schema(
{
vol.Optional(CONF_PASSKEY): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
}
),
errors={"base": "cannot_connect"},
description_placeholders={"host": str(self.host)},
)
return self._show_setup_form({"base": "cannot_connect"})
return self._async_create_entry()
@@ -67,6 +173,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
@callback
def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry."""
return self.async_create_entry(
title=format_mac(self.mac),
data={
@@ -78,8 +185,10 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
},
)
async def _get_bsblan_info(self, raise_on_progress: bool = True) -> None:
"""Get device information from an BSBLAN device."""
async def _get_bsblan_info(
self, raise_on_progress: bool = True, is_discovery: bool = False
) -> None:
"""Get device information from a BSBLAN device."""
config = BSBLANConfig(
host=self.host,
passkey=self.passkey,
@@ -90,11 +199,18 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
session = async_get_clientsession(self.hass)
bsblan = BSBLAN(config, session)
device = await bsblan.device()
self.mac = device.MAC
retrieved_mac = device.MAC
await self.async_set_unique_id(
format_mac(self.mac), raise_on_progress=raise_on_progress
)
# Handle unique ID assignment based on whether MAC was available from zeroconf
if not self.mac:
# MAC wasn't available from zeroconf, now we have it from API
self.mac = retrieved_mac
await self.async_set_unique_id(
format_mac(self.mac), raise_on_progress=raise_on_progress
)
# Always allow updating host/port for both user and discovery flows
# This ensures connectivity is maintained when devices change IP addresses
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self.host,
@@ -7,5 +7,11 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==2.1.0"]
"requirements": ["python-bsblan==2.1.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",
"name": "bsb-lan*"
}
]
}
@@ -20,6 +20,8 @@ from . import BSBLanConfigEntry, BSBLanData
from .coordinator import BSBLanCoordinatorData
from .entity import BSBLanEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class BSBLanSensorEntityDescription(SensorEntityDescription):
+19 -1
View File
@@ -13,7 +13,25 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your BSB-Lan device."
"host": "The hostname or IP address of your BSB-Lan device.",
"port": "The port number of your BSB-Lan device.",
"passkey": "The passkey for your BSB-Lan device.",
"username": "The username for your BSB-Lan device.",
"password": "The password for your BSB-Lan device."
}
},
"discovery_confirm": {
"title": "BSB-Lan device discovered",
"description": "A BSB-Lan device was discovered at {host}. Please provide credentials if required.",
"data": {
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
}
}
},
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.105.0"],
"requirements": ["hass-nabucasa==0.107.0"],
"single_config_entry": true
}
@@ -34,6 +34,7 @@ from .agent_manager import (
from .chat_log import (
AssistantContent,
AssistantContentDeltaDict,
Attachment,
ChatLog,
Content,
ConverseError,
@@ -51,7 +52,6 @@ from .const import (
DATA_DEFAULT_ENTITY,
DOMAIN,
HOME_ASSISTANT_AGENT,
OLD_HOME_ASSISTANT_AGENT,
SERVICE_PROCESS,
SERVICE_RELOAD,
ConversationEntityFeature,
@@ -65,9 +65,9 @@ from .trace import ConversationTraceEventType, async_conversation_trace_append
__all__ = [
"DOMAIN",
"HOME_ASSISTANT_AGENT",
"OLD_HOME_ASSISTANT_AGENT",
"AssistantContent",
"AssistantContentDeltaDict",
"Attachment",
"ChatLog",
"Content",
"ConversationEntity",
@@ -270,15 +270,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, entity_component, config.get(DOMAIN, {}).get("intents", {})
)
# Temporary migration. We can remove this in 2024.10
from homeassistant.components.assist_pipeline import ( # noqa: PLC0415
async_migrate_engine,
)
async_migrate_engine(
hass, "conversation", OLD_HOME_ASSISTANT_AGENT, HOME_ASSISTANT_AGENT
)
async def handle_process(service: ServiceCall) -> ServiceResponse:
"""Parse text into commands."""
text = service.data[ATTR_TEXT]
@@ -12,12 +12,7 @@ from homeassistant.core import Context, HomeAssistant, async_get_hass, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent, singleton
from .const import (
DATA_COMPONENT,
DATA_DEFAULT_ENTITY,
HOME_ASSISTANT_AGENT,
OLD_HOME_ASSISTANT_AGENT,
)
from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY, HOME_ASSISTANT_AGENT
from .entity import ConversationEntity
from .models import (
AbstractConversationAgent,
@@ -54,7 +49,7 @@ def async_get_agent(
hass: HomeAssistant, agent_id: str | None = None
) -> AbstractConversationAgent | ConversationEntity | None:
"""Get specified agent."""
if agent_id is None or agent_id in (HOME_ASSISTANT_AGENT, OLD_HOME_ASSISTANT_AGENT):
if agent_id is None or agent_id == HOME_ASSISTANT_AGENT:
return hass.data[DATA_DEFAULT_ENTITY]
if "." in agent_id:
@@ -8,6 +8,7 @@ from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import asdict, dataclass, field, replace
import logging
from pathlib import Path
from typing import Any, Literal, TypedDict
import voluptuous as vol
@@ -136,6 +137,21 @@ class UserContent:
role: Literal["user"] = field(init=False, default="user")
content: str
attachments: list[Attachment] | None = field(default=None)
@dataclass(frozen=True)
class Attachment:
"""Attachment for a chat message."""
media_content_id: str
"""Media content ID of the attachment."""
mime_type: str
"""MIME type of the attachment."""
path: Path
"""Path to the attachment on disk."""
@dataclass(frozen=True)
@@ -16,7 +16,6 @@ if TYPE_CHECKING:
DOMAIN = "conversation"
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "conversation.home_assistant"
OLD_HOME_ASSISTANT_AGENT = "homeassistant"
ATTR_TEXT = "text"
ATTR_LANGUAGE = "language"
@@ -2,6 +2,8 @@
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant
@@ -9,12 +11,18 @@ from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
async_remove_helper_config_entry_from_source_device,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Derivative from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass, entry.entry_id, entry.options[CONF_SOURCE]
)
@@ -25,20 +33,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options={**entry.options, CONF_SOURCE: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
add_helper_config_entry_to_device=False,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_SOURCE]
),
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
@@ -54,3 +58,51 @@ async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,))
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
if config_entry.minor_version < 2:
new_options = {**config_entry.options}
if new_options.get("unit_prefix") == "none":
# Before we had support for optional selectors, "none" was used for selecting nothing
del new_options["unit_prefix"]
hass.config_entries.async_update_entry(
config_entry, options=new_options, version=1, minor_version=2
)
if config_entry.minor_version < 3:
# Remove the derivative config entry from the source device
if source_device_id := async_entity_id_to_device_id(
hass, config_entry.options[CONF_SOURCE]
):
async_remove_helper_config_entry_from_source_device(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=source_device_id,
)
hass.config_entries.async_update_entry(
config_entry, version=1, minor_version=3
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
@@ -141,6 +141,9 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
VERSION = 1
MINOR_VERSION = 3
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options[CONF_NAME])
+9 -15
View File
@@ -34,8 +34,7 @@ from homeassistant.core import (
callback,
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device import async_entity_id_to_device
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -118,30 +117,21 @@ async def async_setup_entry(
registry, config_entry.options[CONF_SOURCE]
)
device_info = async_device_info_to_link_from_entity(
hass,
source_entity_id,
)
if (unit_prefix := config_entry.options.get(CONF_UNIT_PREFIX)) == "none":
# Before we had support for optional selectors, "none" was used for selecting nothing
unit_prefix = None
if max_sub_interval_dict := config_entry.options.get(CONF_MAX_SUB_INTERVAL, None):
max_sub_interval = cv.time_period(max_sub_interval_dict)
else:
max_sub_interval = None
derivative_sensor = DerivativeSensor(
hass,
name=config_entry.title,
round_digits=int(config_entry.options[CONF_ROUND_DIGITS]),
source_entity=source_entity_id,
time_window=cv.time_period_dict(config_entry.options[CONF_TIME_WINDOW]),
unique_id=config_entry.entry_id,
unit_of_measurement=None,
unit_prefix=unit_prefix,
unit_prefix=config_entry.options.get(CONF_UNIT_PREFIX),
unit_time=config_entry.options[CONF_UNIT_TIME],
device_info=device_info,
max_sub_interval=max_sub_interval,
)
@@ -156,6 +146,7 @@ async def async_setup_platform(
) -> None:
"""Set up the derivative sensor."""
derivative = DerivativeSensor(
hass,
name=config.get(CONF_NAME),
round_digits=config[CONF_ROUND_DIGITS],
source_entity=config[CONF_SOURCE],
@@ -178,6 +169,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
*,
name: str | None,
round_digits: int,
@@ -188,11 +180,13 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
unit_time: UnitOfTime,
max_sub_interval: timedelta | None,
unique_id: str | None,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the derivative sensor."""
self._attr_unique_id = unique_id
self._attr_device_info = device_info
self.device_entry = async_entity_id_to_device(
hass,
source_entity,
)
self._sensor_source_id = source_entity
self._round_digits = round_digits
self._attr_native_value = round(Decimal(0), round_digits)
@@ -19,6 +19,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
integration_platform,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.json import (
@@ -187,6 +188,7 @@ def async_format_manifest(manifest: Manifest) -> Manifest:
async def _async_get_json_file_response(
hass: HomeAssistant,
data: Mapping[str, Any],
data_issues: list[dict[str, Any]] | None,
filename: str,
domain: str,
d_id: str,
@@ -213,6 +215,8 @@ async def _async_get_json_file_response(
"setup_times": async_get_domain_setup_times(hass, domain),
"data": data,
}
if data_issues is not None:
payload["issues"] = data_issues
try:
json_data = json.dumps(payload, indent=2, cls=ExtendedJSONEncoder)
except TypeError:
@@ -275,6 +279,14 @@ class DownloadDiagnosticsView(http.HomeAssistantView):
filename = f"{config_entry.domain}-{config_entry.entry_id}"
issue_registry = ir.async_get(hass)
issues = issue_registry.issues
data_issues = [
issue_reg.to_json()
for issue_id, issue_reg in issues.items()
if issue_id[0] == config_entry.domain
]
if not device_diagnostics:
# Config entry diagnostics
if info.config_entry_diagnostics is None:
@@ -282,7 +294,7 @@ class DownloadDiagnosticsView(http.HomeAssistantView):
data = await info.config_entry_diagnostics(hass, config_entry)
filename = f"{DiagnosticsType.CONFIG_ENTRY}-{filename}"
return await _async_get_json_file_response(
hass, data, filename, config_entry.domain, d_id
hass, data, data_issues, filename, config_entry.domain, d_id
)
# Device diagnostics
@@ -300,5 +312,5 @@ class DownloadDiagnosticsView(http.HomeAssistantView):
data = await info.device_diagnostics(hass, config_entry, device)
return await _async_get_json_file_response(
hass, data, filename, config_entry.domain, d_id, sub_id
hass, data, data_issues, filename, config_entry.domain, d_id, sub_id
)
@@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import create_async_httpx_client
from .const import DOMAIN
from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -30,10 +31,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -
# if no exception is raised everything is fine to go
meters = await client.meters()
except discovergyError.InvalidLogin as err:
raise ConfigEntryAuthFailed("Invalid email or password") from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except Exception as err:
raise ConfigEntryNotReady(
"Unexpected error while while getting meters"
translation_domain=DOMAIN,
translation_key="cannot_connect_meters_setup",
) from err
# Init coordinators for meters
@@ -14,6 +14,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]]
@@ -51,7 +53,12 @@ class DiscovergyUpdateCoordinator(DataUpdateCoordinator[Reading]):
)
except InvalidLogin as err:
raise ConfigEntryAuthFailed(
"Auth expired while fetching last reading"
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except (HTTPError, DiscovergyClientError) as err:
raise UpdateFailed(f"Error while fetching last reading: {err}") from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="reading_update_failed",
translation_placeholders={"meter_id": self.meter.meter_id},
) from err
@@ -72,12 +72,16 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
exception-translations: done
icon-translations:
status: exempt
comment: |
The integration does not provide any additional icons.
reconfiguration-flow: todo
reconfiguration-flow:
status: exempt
comment: |
No configuration besides credentials.
New credentials will create a new config entry.
repair-issues:
status: exempt
comment: |
@@ -23,6 +23,17 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"exceptions": {
"invalid_auth": {
"message": "Authentication failed. Please check your inexogy email and password."
},
"cannot_connect_meters_setup": {
"message": "Failed to connect and retrieve meters from inexogy during setup. Please ensure the service is reachable and try again."
},
"reading_update_failed": {
"message": "Error fetching the latest reading for meter {meter_id} from inexogy. The service might be temporarily unavailable or there's a connection issue. Check logs for more details."
}
},
"system_health": {
"info": {
"api_endpoint_reachable": "inexogy API endpoint reachable"
@@ -106,6 +106,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
EcoWittSensorTypes.CO2_PPM: SensorEntityDescription(
key="CO2_PPM",
@@ -191,12 +192,14 @@ ECOWITT_SENSORS_MAPPING: Final = {
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
EcoWittSensorTypes.SPEED_MPH: SensorEntityDescription(
key="SPEED_MPH",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
EcoWittSensorTypes.PRESSURE_HPA: SensorEntityDescription(
key="PRESSURE_HPA",
@@ -25,7 +25,8 @@ PLATFORMS: list[Platform] = [Platform.TTS]
async def get_model_by_id(client: AsyncElevenLabs, model_id: str) -> Model | None:
"""Get ElevenLabs model from their API by the model_id."""
models = await client.models.get_all()
models = await client.models.list()
for maybe_model in models:
if maybe_model.model_id == model_id:
return maybe_model
@@ -23,14 +23,12 @@ from . import ElevenLabsConfigEntry
from .const import (
CONF_CONFIGURE_VOICE,
CONF_MODEL,
CONF_OPTIMIZE_LATENCY,
CONF_SIMILARITY,
CONF_STABILITY,
CONF_STYLE,
CONF_USE_SPEAKER_BOOST,
CONF_VOICE,
DEFAULT_MODEL,
DEFAULT_OPTIMIZE_LATENCY,
DEFAULT_SIMILARITY,
DEFAULT_STABILITY,
DEFAULT_STYLE,
@@ -51,7 +49,8 @@ async def get_voices_models(
httpx_client = get_async_client(hass)
client = AsyncElevenLabs(api_key=api_key, httpx_client=httpx_client)
voices = (await client.voices.get_all()).voices
models = await client.models.get_all()
models = await client.models.list()
voices_dict = {
voice.voice_id: voice.name
for voice in sorted(voices, key=lambda v: v.name or "")
@@ -78,8 +77,13 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
voices, _ = await get_voices_models(self.hass, user_input[CONF_API_KEY])
except ApiError:
errors["base"] = "invalid_api_key"
except ApiError as exc:
errors["base"] = "unknown"
details = getattr(exc, "body", {}).get("detail", {})
if details:
status = details.get("status")
if status == "invalid_api_key":
errors["base"] = "invalid_api_key"
else:
return self.async_create_entry(
title="ElevenLabs",
@@ -206,12 +210,6 @@ class ElevenLabsOptionsFlow(OptionsFlow):
vol.Coerce(float),
vol.Range(min=0, max=1),
),
vol.Optional(
CONF_OPTIMIZE_LATENCY,
default=self.config_entry.options.get(
CONF_OPTIMIZE_LATENCY, DEFAULT_OPTIMIZE_LATENCY
),
): vol.All(int, vol.Range(min=0, max=4)),
vol.Optional(
CONF_STYLE,
default=self.config_entry.options.get(CONF_STYLE, DEFAULT_STYLE),
@@ -7,7 +7,6 @@ CONF_MODEL = "model"
CONF_CONFIGURE_VOICE = "configure_voice"
CONF_STABILITY = "stability"
CONF_SIMILARITY = "similarity"
CONF_OPTIMIZE_LATENCY = "optimize_streaming_latency"
CONF_STYLE = "style"
CONF_USE_SPEAKER_BOOST = "use_speaker_boost"
DOMAIN = "elevenlabs"
@@ -15,6 +14,5 @@ DOMAIN = "elevenlabs"
DEFAULT_MODEL = "eleven_multilingual_v2"
DEFAULT_STABILITY = 0.5
DEFAULT_SIMILARITY = 0.75
DEFAULT_OPTIMIZE_LATENCY = 0
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["elevenlabs"],
"requirements": ["elevenlabs==1.9.0"]
"requirements": ["elevenlabs==2.3.0"]
}
@@ -11,7 +11,8 @@
}
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
@@ -32,14 +33,12 @@
"data": {
"stability": "Stability",
"similarity": "Similarity",
"optimize_streaming_latency": "Latency",
"style": "Style",
"use_speaker_boost": "Speaker boost"
},
"data_description": {
"stability": "Stability of the generated audio. Higher values lead to less emotional audio.",
"similarity": "Similarity of the generated audio to the original voice. Higher values may result in more similar audio, but may also introduce background noise.",
"optimize_streaming_latency": "Optimize the model for streaming. This may reduce the quality of the generated audio.",
"style": "Style of the generated audio. Recommended to keep at 0 for most almost all use cases.",
"use_speaker_boost": "Use speaker boost to increase the similarity of the generated audio to the original voice."
}
+4 -11
View File
@@ -25,13 +25,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ElevenLabsConfigEntry
from .const import (
ATTR_MODEL,
CONF_OPTIMIZE_LATENCY,
CONF_SIMILARITY,
CONF_STABILITY,
CONF_STYLE,
CONF_USE_SPEAKER_BOOST,
CONF_VOICE,
DEFAULT_OPTIMIZE_LATENCY,
DEFAULT_SIMILARITY,
DEFAULT_STABILITY,
DEFAULT_STYLE,
@@ -75,9 +73,6 @@ async def async_setup_entry(
config_entry.entry_id,
config_entry.title,
voice_settings,
config_entry.options.get(
CONF_OPTIMIZE_LATENCY, DEFAULT_OPTIMIZE_LATENCY
),
)
]
)
@@ -98,7 +93,6 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
entry_id: str,
title: str,
voice_settings: VoiceSettings,
latency: int = 0,
) -> None:
"""Init ElevenLabs TTS service."""
self._client = client
@@ -115,7 +109,6 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
if voice_indices:
self._voices.insert(0, self._voices.pop(voice_indices[0]))
self._voice_settings = voice_settings
self._latency = latency
# Entity attributes
self._attr_unique_id = entry_id
@@ -144,14 +137,14 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
voice_id = options.get(ATTR_VOICE, self._default_voice_id)
model = options.get(ATTR_MODEL, self._model.model_id)
try:
audio = await self._client.generate(
audio = self._client.text_to_speech.convert(
text=message,
voice=voice_id,
optimize_streaming_latency=self._latency,
voice_id=voice_id,
voice_settings=self._voice_settings,
model=model,
model_id=model,
)
bytes_combined = b"".join([byte_seg async for byte_seg in audio])
except ApiError as exc:
_LOGGER.warning(
"Error during processing of TTS request %s", exc, exc_info=True
+2 -7
View File
@@ -41,13 +41,8 @@ SUPPORTED_STATE_CLASSES = {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,
}
VALID_ENERGY_UNITS: set[str] = {
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_JOULE,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
}
VALID_ENERGY_UNITS: set[str] = set(UnitOfEnergy)
VALID_ENERGY_UNITS_GAS = {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
+5 -14
View File
@@ -21,14 +21,9 @@ from .const import DOMAIN
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
ENERGY_USAGE_UNITS: dict[str, tuple[UnitOfEnergy, ...]] = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_JOULE,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
)
sensor.SensorDeviceClass.ENERGY: tuple(UnitOfEnergy)
}
ENERGY_PRICE_UNITS = tuple(
f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units
)
@@ -39,13 +34,9 @@ GAS_USAGE_DEVICE_CLASSES = (
sensor.SensorDeviceClass.GAS,
)
GAS_USAGE_UNITS: dict[str, tuple[UnitOfEnergy | UnitOfVolume, ...]] = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_JOULE,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
),
sensor.SensorDeviceClass.ENERGY: ENERGY_USAGE_UNITS[
sensor.SensorDeviceClass.ENERGY
],
sensor.SensorDeviceClass.GAS: (
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==2.16.0"]
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.1.0"]
}
@@ -100,49 +100,70 @@ class EsphomeAlarmControlPanel(
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.DISARM, code
self._key,
AlarmControlPanelCommand.DISARM,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.ARM_HOME, code
self._key,
AlarmControlPanelCommand.ARM_HOME,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.ARM_AWAY, code
self._key,
AlarmControlPanelCommand.ARM_AWAY,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm away command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.ARM_NIGHT, code
self._key,
AlarmControlPanelCommand.ARM_NIGHT,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send arm away command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.ARM_CUSTOM_BYPASS, code
self._key,
AlarmControlPanelCommand.ARM_CUSTOM_BYPASS,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm away command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.ARM_VACATION, code
self._key,
AlarmControlPanelCommand.ARM_VACATION,
code,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command."""
self._client.alarm_control_panel_command(
self._key, AlarmControlPanelCommand.TRIGGER, code
self._key,
AlarmControlPanelCommand.TRIGGER,
code,
device_id=self._static_info.device_id,
)
+1 -1
View File
@@ -48,7 +48,7 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity):
@convert_api_error_ha_error
async def async_press(self) -> None:
"""Press the button."""
self._client.button_command(self._key)
self._client.button_command(self._key, device_id=self._static_info.device_id)
async_setup_entry = partial(
+14 -6
View File
@@ -287,18 +287,24 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
data["target_temperature_low"] = kwargs[ATTR_TARGET_TEMP_LOW]
if ATTR_TARGET_TEMP_HIGH in kwargs:
data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
self._client.climate_command(**data)
self._client.climate_command(**data, device_id=self._static_info.device_id)
@convert_api_error_ha_error
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
self._client.climate_command(key=self._key, target_humidity=humidity)
self._client.climate_command(
key=self._key,
target_humidity=humidity,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode."""
self._client.climate_command(
key=self._key, mode=_CLIMATE_MODES.from_hass(hvac_mode)
key=self._key,
mode=_CLIMATE_MODES.from_hass(hvac_mode),
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
@@ -309,7 +315,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
kwargs["custom_preset"] = preset_mode
else:
kwargs["preset"] = _PRESETS.from_hass(preset_mode)
self._client.climate_command(**kwargs)
self._client.climate_command(**kwargs, device_id=self._static_info.device_id)
@convert_api_error_ha_error
async def async_set_fan_mode(self, fan_mode: str) -> None:
@@ -319,13 +325,15 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
kwargs["custom_fan_mode"] = fan_mode
else:
kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode)
self._client.climate_command(**kwargs)
self._client.climate_command(**kwargs, device_id=self._static_info.device_id)
@convert_api_error_ha_error
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new swing mode."""
self._client.climate_command(
key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode)
key=self._key,
swing_mode=_SWING_MODES.from_hass(swing_mode),
device_id=self._static_info.device_id,
)
+25 -7
View File
@@ -90,38 +90,56 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity):
@convert_api_error_ha_error
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._client.cover_command(key=self._key, position=1.0)
self._client.cover_command(
key=self._key, position=1.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
self._client.cover_command(key=self._key, position=0.0)
self._client.cover_command(
key=self._key, position=0.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._client.cover_command(key=self._key, stop=True)
self._client.cover_command(
key=self._key, stop=True, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
self._client.cover_command(key=self._key, position=kwargs[ATTR_POSITION] / 100)
self._client.cover_command(
key=self._key,
position=kwargs[ATTR_POSITION] / 100,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
self._client.cover_command(key=self._key, tilt=1.0)
self._client.cover_command(
key=self._key, tilt=1.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
self._client.cover_command(key=self._key, tilt=0.0)
self._client.cover_command(
key=self._key, tilt=0.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
tilt_position: int = kwargs[ATTR_TILT_POSITION]
self._client.cover_command(key=self._key, tilt=tilt_position / 100)
self._client.cover_command(
key=self._key,
tilt=tilt_position / 100,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
+7 -1
View File
@@ -28,7 +28,13 @@ class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity):
async def async_set_value(self, value: date) -> None:
"""Update the current date."""
self._client.date_command(self._key, value.year, value.month, value.day)
self._client.date_command(
self._key,
value.year,
value.month,
value.day,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
+3 -1
View File
@@ -29,7 +29,9 @@ class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity
async def async_set_value(self, value: datetime) -> None:
"""Update the current datetime."""
self._client.datetime_command(self._key, int(value.timestamp()))
self._client.datetime_command(
self._key, int(value.timestamp()), device_id=self._static_info.device_id
)
async_setup_entry = partial(
+17 -5
View File
@@ -71,7 +71,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
ORDERED_NAMED_FAN_SPEEDS, percentage
)
data["speed"] = named_speed
self._client.fan_command(**data)
self._client.fan_command(**data, device_id=self._static_info.device_id)
async def async_turn_on(
self,
@@ -85,24 +85,36 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
@convert_api_error_ha_error
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the fan."""
self._client.fan_command(key=self._key, state=False)
self._client.fan_command(
key=self._key, state=False, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
self._client.fan_command(key=self._key, oscillating=oscillating)
self._client.fan_command(
key=self._key,
oscillating=oscillating,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_set_direction(self, direction: str) -> None:
"""Set direction of the fan."""
self._client.fan_command(
key=self._key, direction=_FAN_DIRECTIONS.from_hass(direction)
key=self._key,
direction=_FAN_DIRECTIONS.from_hass(direction),
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
self._client.fan_command(key=self._key, preset_mode=preset_mode)
self._client.fan_command(
key=self._key,
preset_mode=preset_mode,
device_id=self._static_info.device_id,
)
@property
@esphome_state_property
+2 -2
View File
@@ -280,7 +280,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
# (fewest capabilities set)
data["color_mode"] = _least_complex_color_mode(color_modes)
self._client.light_command(**data)
self._client.light_command(**data, device_id=self._static_info.device_id)
@convert_api_error_ha_error
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -290,7 +290,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
if ATTR_TRANSITION in kwargs:
data["transition_length"] = kwargs[ATTR_TRANSITION]
self._client.light_command(**data)
self._client.light_command(**data, device_id=self._static_info.device_id)
@property
@esphome_state_property
+9 -3
View File
@@ -65,18 +65,24 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity):
@convert_api_error_ha_error
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock."""
self._client.lock_command(self._key, LockCommand.LOCK)
self._client.lock_command(
self._key, LockCommand.LOCK, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock."""
code = kwargs.get(ATTR_CODE)
self._client.lock_command(self._key, LockCommand.UNLOCK, code)
self._client.lock_command(
self._key, LockCommand.UNLOCK, code, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
self._client.lock_command(self._key, LockCommand.OPEN)
self._client.lock_command(
self._key, LockCommand.OPEN, device_id=self._static_info.device_id
)
async_setup_entry = partial(
@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==34.2.0",
"aioesphomeapi==35.0.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
"bleak-esphome==3.1.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -132,7 +132,10 @@ class EsphomeMediaPlayer(
media_id = proxy_url
self._client.media_player_command(
self._key, media_url=media_id, announcement=announcement
self._key,
media_url=media_id,
announcement=announcement,
device_id=self._static_info.device_id,
)
async def async_will_remove_from_hass(self) -> None:
@@ -214,22 +217,36 @@ class EsphomeMediaPlayer(
@convert_api_error_ha_error
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self._client.media_player_command(self._key, volume=volume)
self._client.media_player_command(
self._key, volume=volume, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_media_pause(self) -> None:
"""Send pause command."""
self._client.media_player_command(self._key, command=MediaPlayerCommand.PAUSE)
self._client.media_player_command(
self._key,
command=MediaPlayerCommand.PAUSE,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_media_play(self) -> None:
"""Send play command."""
self._client.media_player_command(self._key, command=MediaPlayerCommand.PLAY)
self._client.media_player_command(
self._key,
command=MediaPlayerCommand.PLAY,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_media_stop(self) -> None:
"""Send stop command."""
self._client.media_player_command(self._key, command=MediaPlayerCommand.STOP)
self._client.media_player_command(
self._key,
command=MediaPlayerCommand.STOP,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_mute_volume(self, mute: bool) -> None:
@@ -237,6 +254,7 @@ class EsphomeMediaPlayer(
self._client.media_player_command(
self._key,
command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE,
device_id=self._static_info.device_id,
)
+3 -1
View File
@@ -67,7 +67,9 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
@convert_api_error_ha_error
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
self._client.number_command(self._key, value)
self._client.number_command(
self._key, value, device_id=self._static_info.device_id
)
async_setup_entry = partial(
+3 -1
View File
@@ -76,7 +76,9 @@ class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity):
@convert_api_error_ha_error
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self._client.select_command(self._key, option)
self._client.select_command(
self._key, option, device_id=self._static_info.device_id
)
class EsphomeAssistPipelineSelect(EsphomeAssistEntity, AssistPipelineSelect):
+6 -2
View File
@@ -43,12 +43,16 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
@convert_api_error_ha_error
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
self._client.switch_command(self._key, True)
self._client.switch_command(
self._key, True, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
self._client.switch_command(self._key, False)
self._client.switch_command(
self._key, False, device_id=self._static_info.device_id
)
async_setup_entry = partial(
+3 -1
View File
@@ -50,7 +50,9 @@ class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity):
@convert_api_error_ha_error
async def async_set_value(self, value: str) -> None:
"""Update the current value."""
self._client.text_command(self._key, value)
self._client.text_command(
self._key, value, device_id=self._static_info.device_id
)
async_setup_entry = partial(
+7 -1
View File
@@ -28,7 +28,13 @@ class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity):
async def async_set_value(self, value: time) -> None:
"""Update the current time."""
self._client.time_command(self._key, value.hour, value.minute, value.second)
self._client.time_command(
self._key,
value.hour,
value.minute,
value.second,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
+10 -2
View File
@@ -334,11 +334,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
async def async_update(self) -> None:
"""Command device to check for update."""
if self.available:
self._client.update_command(key=self._key, command=UpdateCommand.CHECK)
self._client.update_command(
key=self._key,
command=UpdateCommand.CHECK,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Command device to install update."""
self._client.update_command(key=self._key, command=UpdateCommand.INSTALL)
self._client.update_command(
key=self._key,
command=UpdateCommand.INSTALL,
device_id=self._static_info.device_id,
)
+14 -4
View File
@@ -72,22 +72,32 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
@convert_api_error_ha_error
async def async_open_valve(self, **kwargs: Any) -> None:
"""Open the valve."""
self._client.valve_command(key=self._key, position=1.0)
self._client.valve_command(
key=self._key, position=1.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_close_valve(self, **kwargs: Any) -> None:
"""Close valve."""
self._client.valve_command(key=self._key, position=0.0)
self._client.valve_command(
key=self._key, position=0.0, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_stop_valve(self, **kwargs: Any) -> None:
"""Stop the valve."""
self._client.valve_command(key=self._key, stop=True)
self._client.valve_command(
key=self._key, stop=True, device_id=self._static_info.device_id
)
@convert_api_error_ha_error
async def async_set_valve_position(self, position: float) -> None:
"""Move the valve to a specific position."""
self._client.valve_command(key=self._key, position=position / 100)
self._client.valve_command(
key=self._key,
position=position / 100,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
@@ -171,14 +171,19 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
for device in new_data.devices.values():
# create device registry entry for new main devices
if (
device.ain not in self.data.devices
and device.device_and_unit_id[1] is None
if device.ain not in self.data.devices and (
device.device_and_unit_id[1] is None
or (
# workaround for sub units without a main device, e.g. Energy 250
# https://github.com/home-assistant/core/issues/145204
device.device_and_unit_id[1] == "1"
and device.device_and_unit_id[0] not in new_data.devices
)
):
dr.async_get(self.hass).async_get_or_create(
config_entry_id=self.config_entry.entry_id,
name=device.name,
identifiers={(DOMAIN, device.ain)},
identifiers={(DOMAIN, device.device_and_unit_id[0])},
manufacturer=device.manufacturer,
model=device.productname,
sw_version=device.fw_version,
@@ -26,6 +26,7 @@ from homeassistant.const import (
EVENT_THEMES_UPDATED,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.icon import async_get_icons
from homeassistant.helpers.json import json_dumps_sorted
@@ -543,6 +544,12 @@ async def _async_setup_themes(
"""Reload themes."""
config = await async_hass_config_yaml(hass)
new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {})
try:
THEME_SCHEMA(new_themes)
except vol.Invalid as err:
raise HomeAssistantError(f"Failed to reload themes: {err}") from err
hass.data[DATA_THEMES] = new_themes
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250702.1"]
"requirements": ["home-assistant-frontend==20250702.2"]
}
@@ -1,5 +1,7 @@
"""The generic_hygrostat component."""
import logging
import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass
@@ -16,7 +18,10 @@ from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
async_remove_helper_config_entry_from_source_device,
)
from homeassistant.helpers.typing import ConfigType
DOMAIN = "generic_hygrostat"
@@ -70,6 +75,8 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Generic Hygrostat component."""
@@ -89,6 +96,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
@@ -101,23 +109,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the humidifer,
# but not the humidity sensor because the generic_hygrostat adds itself to the
# humidifier's device.
async_handle_source_entity_changes(
hass,
add_helper_config_entry_to_device=False,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HUMIDIFIER]
),
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
source_entity_removed=source_entity_removed,
)
)
@@ -148,6 +152,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
options = {**config_entry.options}
if config_entry.minor_version < 2:
# Remove the generic_hygrostat config entry from the source device
if source_device_id := async_entity_id_to_device_id(
hass, options[CONF_HUMIDIFIER]
):
async_remove_helper_config_entry_from_source_device(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=source_device_id,
)
hass.config_entries.async_update_entry(
config_entry, options=options, minor_version=2
)
_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
@@ -92,6 +92,8 @@ OPTIONS_FLOW = {
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow."""
MINOR_VERSION = 2
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
@@ -42,7 +42,7 @@ from homeassistant.core import (
callback,
)
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device import async_entity_id_to_device
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -145,22 +145,22 @@ async def _async_setup_config(
[
GenericHygrostat(
hass,
name,
switch_entity_id,
sensor_entity_id,
min_humidity,
max_humidity,
target_humidity,
device_class,
min_cycle_duration,
dry_tolerance,
wet_tolerance,
keep_alive,
initial_state,
away_humidity,
away_fixed,
sensor_stale_duration,
unique_id,
name=name,
switch_entity_id=switch_entity_id,
sensor_entity_id=sensor_entity_id,
min_humidity=min_humidity,
max_humidity=max_humidity,
target_humidity=target_humidity,
device_class=device_class,
min_cycle_duration=min_cycle_duration,
dry_tolerance=dry_tolerance,
wet_tolerance=wet_tolerance,
keep_alive=keep_alive,
initial_state=initial_state,
away_humidity=away_humidity,
away_fixed=away_fixed,
sensor_stale_duration=sensor_stale_duration,
unique_id=unique_id,
)
]
)
@@ -174,6 +174,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity):
def __init__(
self,
hass: HomeAssistant,
*,
name: str,
switch_entity_id: str,
sensor_entity_id: str,
@@ -195,7 +196,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity):
self._name = name
self._switch_entity_id = switch_entity_id
self._sensor_entity_id = sensor_entity_id
self._attr_device_info = async_device_info_to_link_from_entity(
self.device_entry = async_entity_id_to_device(
hass,
switch_entity_id,
)
@@ -1,5 +1,7 @@
"""The generic_thermostat component."""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -8,14 +10,20 @@ from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.helper_integration import (
async_handle_source_entity_changes,
async_remove_helper_config_entry_from_source_device,
)
from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
# This can be removed in HA Core 2026.2
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
@@ -28,23 +36,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options={**entry.options, CONF_HEATER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the heater, but
# not the temperature sensor because the generic_hygrostat adds itself to the
# heater's device.
async_handle_source_entity_changes(
hass,
add_helper_config_entry_to_device=False,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HEATER]
),
source_entity_id_or_uuid=entry.options[CONF_HEATER],
source_entity_removed=source_entity_removed,
)
)
@@ -75,6 +79,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
options = {**config_entry.options}
if config_entry.minor_version < 2:
# Remove the generic_thermostat config entry from the source device
if source_device_id := async_entity_id_to_device_id(
hass, options[CONF_HEATER]
):
async_remove_helper_config_entry_from_source_device(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=source_device_id,
)
hass.config_entries.async_update_entry(
config_entry, options=options, minor_version=2
)
_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
@@ -48,7 +48,7 @@ from homeassistant.core import (
)
from homeassistant.exceptions import ConditionError
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device import async_entity_id_to_device
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -182,23 +182,23 @@ async def _async_setup_config(
[
GenericThermostat(
hass,
name,
heater_entity_id,
sensor_entity_id,
min_temp,
max_temp,
target_temp,
ac_mode,
min_cycle_duration,
cold_tolerance,
hot_tolerance,
keep_alive,
initial_hvac_mode,
presets,
precision,
target_temperature_step,
unit,
unique_id,
name=name,
heater_entity_id=heater_entity_id,
sensor_entity_id=sensor_entity_id,
min_temp=min_temp,
max_temp=max_temp,
target_temp=target_temp,
ac_mode=ac_mode,
min_cycle_duration=min_cycle_duration,
cold_tolerance=cold_tolerance,
hot_tolerance=hot_tolerance,
keep_alive=keep_alive,
initial_hvac_mode=initial_hvac_mode,
presets=presets,
precision=precision,
target_temperature_step=target_temperature_step,
unit=unit,
unique_id=unique_id,
)
]
)
@@ -212,6 +212,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
def __init__(
self,
hass: HomeAssistant,
*,
name: str,
heater_entity_id: str,
sensor_entity_id: str,
@@ -234,7 +235,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
self._attr_name = name
self.heater_entity_id = heater_entity_id
self.sensor_entity_id = sensor_entity_id
self._attr_device_info = async_device_info_to_link_from_entity(
self.device_entry = async_entity_id_to_device(
hass,
heater_entity_id,
)

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