Compare commits

..

254 Commits

Author SHA1 Message Date
Jan Bouwhuis
49022b69b0 Add MQTT time platform (#168898) 2026-04-23 21:12:28 +02:00
Mick Vleeshouwer
13105bd0b7 Migrate cover platform to entity descriptions in Overkiz (#141330)
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 19:58:17 +02:00
epenet
438c1e9c3d Remove leftover hass.data[DOMAIN] usage from insteon (#168880)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 15:22:44 +02:00
Christophe Gagnier
b0ecc2f36a Add reconfigure flow to TechnoVE integration (#168466)
Co-authored-by: Moustachauve <2206577+Moustachauve@users.noreply.github.com>
2026-04-23 15:09:35 +02:00
Ronald van der Meer
19f19e00f6 Add UCRH sensor support and warn on unknown node types in Duco (#168758) 2026-04-23 15:02:36 +02:00
Raphael Hehl
95ec39ac1a unifi_access: add direction attribute to access events (#168853)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: home-assistant[bot] <78085893+home-assistant[bot]@users.noreply.github.com>
2026-04-23 14:44:57 +02:00
Matthias Alphart
c6b4594e7a Update knx-frontend to 2026.4.22.141111 (#168837) 2026-04-23 14:44:24 +02:00
Joost Lekkerkerker
cf0b5c6e51 Migrate GitHub to subentries (#160564)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-04-23 14:23:47 +02:00
epenet
187fcd10b3 Add async_panel_exists helper to frontend and use it across integrations (#168884) 2026-04-23 14:14:45 +03:00
TheJulianJES
ed1cba02ae Migrate Matter integration to use runtime_data (#168862) 2026-04-23 13:03:08 +02:00
epenet
b213eb23c8 Reduce context switching in Tuya initialisation (#168830)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 12:48:15 +02:00
renovate[bot]
30d362dc8e Update uv to 0.11.7 (#168864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 10:43:51 +02:00
Erik Montnemery
67c818c7a8 Add comment to trigger base class (#168882) 2026-04-23 10:42:07 +02:00
epenet
5927f50bd2 Use runtime_data in Huawei LTE (#168876)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:48:45 +02:00
epenet
66d7afa442 Migrate flux_led to use HassKey for FLUX_LED_DISCOVERY (#168872)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:56:20 +02:00
epenet
51fcdaff7a Migrate slimproto to use runtime_data (#168869)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:55:37 +02:00
Raphael Hehl
67baec27cf unifi_access: add missing WebSocket handlers for remote_view and device_update events (#168850)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-23 08:50:09 +02:00
epenet
d45941d648 Migrate kraken to use runtime_data (#168870)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:24:56 +02:00
Raphael Hehl
a338d04441 unifi_access: bump py-unifi-access to 1.3.0 (#168851)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-23 08:24:41 +02:00
epenet
69eca62446 Clean up leftover hass.data[DOMAIN] usage in keenetic_ndms2 (#168871)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:20:48 +02:00
Franck Nijhof
507b5f1bbf Add pylint plugin to detect polling interval fields in config flows (#168849) 2026-04-22 23:41:43 +02:00
A. Gideonse
ee8a15b368 Fix incorrect sensor definition for Indevolt Gen-1 devices (#168835)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-04-22 22:03:13 +02:00
Erik Montnemery
7f92d88606 Replace climate-control device with thermostat in climate translations (#161419) 2026-04-22 21:02:54 +02:00
epenet
cc1c5e788f Revert Tuya camera quirk changes (#168820) 2026-04-22 20:54:49 +02:00
epenet
1159946391 Bump tuya-device-handlers to 0.0.18 (#168821) 2026-04-22 20:53:37 +02:00
Erik Montnemery
46208c034e Add tests asserting air_quality condition features (#168731) 2026-04-22 20:42:42 +02:00
puddly
abdd132bdc Register optimized ESPHome serial proxy transport with serialx (#168817) 2026-04-22 13:16:56 -04:00
Denis Shulyaka
1b71ef2a60 Add gpt-image-2 model support for OpenAI (#168826) 2026-04-22 18:13:04 +01:00
Abílio Costa
f0445a792d Add dummy Claude skill instruction for testing (#168829) 2026-04-22 18:35:24 +02:00
Abílio Costa
24e3842319 Rename Claude's integration skill (#168825) 2026-04-22 17:04:49 +01:00
epenet
54aae2c7de Ensure Tuya (stale) device is removed before adding new (#168721) 2026-04-22 16:58:00 +01:00
epenet
ea3e8cf9b0 Add tests for Tuya dynamic add/remove device (#168824) 2026-04-22 16:13:56 +01:00
Abílio Costa
a16f6f965e Improve claude gh pr review summary + business logic lib note (#168819) 2026-04-22 16:05:28 +01:00
Manu
d772320f06 Record notifications sent via ntfy.publish action in ntfy integration (#166352) 2026-04-22 17:01:31 +02:00
Michael Hansen
8a74b41db5 Add audio processing settings to speech-to-text entities (#167246) 2026-04-22 08:43:21 -05:00
Raphael Hehl
fddc6aaf38 Add entity translations to UniFi integration (#168739)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-22 15:40:35 +02:00
Franck Nijhof
fab59d7a13 Add pylint plugin to enforce entry.runtime_data over hass.data[DOMAIN] (#168760) 2026-04-22 15:31:58 +02:00
Robert Resch
1345356bdc Validate local_only user property during ws auth phase (#168812) 2026-04-22 14:07:47 +02:00
Shay Levy
be07fed774 Remove unused hass.data[DOMAIN] in LG webOS TV (#168813) 2026-04-22 13:58:44 +02:00
Erwin Douna
d17f6a1509 Firefly III consistency with access token (#168565) 2026-04-22 11:12:40 +02:00
Thijs W.
f3932f2342 Improve exception handling for frontier_silicon (#168635)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-04-22 10:58:09 +02:00
Mick Vleeshouwer
598be31daf Improve test structure for Overkiz (#168728) 2026-04-22 10:10:18 +02:00
epenet
9b2a81614f Simplify Tuya runtime_data (#168718)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 10:02:24 +02:00
Øyvind Matheson Wergeland
f53c89d3bc Translate override_type options in nobo_hub (#168752) 2026-04-22 09:59:51 +02:00
dependabot[bot]
ac6991072f Bump github/codeql-action from 4.35.1 to 4.35.2 (#168754)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 09:53:11 +02:00
Jan Bouwhuis
018e8e06fa Cancel and await idle_start future if the task was canceled after an IMAP connection was lost (#168662)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-22 09:43:22 +02:00
Ronald van der Meer
0ffc9694a7 Bump python-duco-client to 0.3.4 (#168757) 2026-04-22 09:41:21 +02:00
Marc Mueller
8d8b30a41e Update mypy to 1.20.2 (#168741) 2026-04-22 09:38:08 +02:00
Tomer
9b7f61d862 Victron GX: Diagnostics (#168700)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-22 09:36:49 +02:00
epenet
368f2f44be Use HassKey in zeroconf (#168707)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:26:13 +02:00
LG-ThinQ-Integration
ad6a910244 Bump thinqconnect to 1.0.12 (#168753)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-04-22 09:21:15 +02:00
Leonardo Rivera
840b44039d Fix OneDrive upload service to support multiple files (#168512) 2026-04-22 09:11:27 +02:00
Ronald van der Meer
1943675a64 Add DHCP discovery to Duco integration (#168730) 2026-04-22 08:32:05 +02:00
Linkplay2020
161e05b075 Updata wiim to 0.1.2 (#168671)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-04-22 08:07:17 +02:00
Paulus Schoutsen
f2d5ca3582 Rename SerialSelector to SerialPortSelector (#168744)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-22 07:47:28 +02:00
Florent Thoumie
551af8caef Rename iAqualink to iAquaLink (#168743) 2026-04-22 07:26:48 +02:00
Johan Henkens
201c575316 Bump aioesphomeapi to 44.18.0 (#168749) 2026-04-22 06:12:32 +02:00
tronikos
703860ee6e Add support for away mode in ESPHome water heater (#167951)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-22 05:37:47 +02:00
puddly
cb021f0b6b Allow integrations to contribute serial port scanning helpers (#168660)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2026-04-21 21:15:57 -04:00
Øyvind Matheson Wergeland
50dbff31b0 Fix nobo_hub override type description (#168740) 2026-04-21 23:30:06 +02:00
MohamedBarrak3
800299077e Fix case-sensitive MIME type check in Google Generative AI TTS (#168458) 2026-04-21 23:26:31 +02:00
Andrew Jackson
f40b269752 Version checking of Transmission (#168429)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-21 23:26:14 +02:00
David
f2105c07de Expose Lutron Caseta shade battery status on covers (#165180) 2026-04-21 23:25:45 +02:00
Erwin Douna
d23dbfb214 Add volumes to Portainer (#167326) 2026-04-21 23:23:27 +02:00
Erwin Douna
de6586684a Add recreate container button to Portainer (#167163) 2026-04-21 23:21:45 +02:00
Avi Miller
9a08b941bb Limit LIFX bulb changes to the values that are actually changing (#168618) 2026-04-21 23:08:04 +02:00
Øyvind Matheson Wergeland
51b9f004e9 Introduce NoboBaseEntity in nobo_hub (#168724)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-21 23:03:45 +02:00
epenet
fe443f4ce9 Use runtime_data in wyoming integration (#168619) 2026-04-21 22:50:06 +02:00
Thijs W.
b0ba7ec6ec Frontier silicon: use correct command to restart stopped stream (#168633)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 22:36:44 +02:00
Florent Thoumie
156901c290 iaqualink: Add basic DHCP discovery for iAquaLink devices (#168256)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 22:34:37 +02:00
Franck Nijhof
b6271e59fa Add sensor platform to Fumis integration (#168680)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 22:24:13 +02:00
Franck Nijhof
17cd0aa474 Add DHCP discovery to Fumis integration (#168735) 2026-04-21 22:20:51 +02:00
Stefan Agner
79f12f658a Improve Supervisor update entity progress and data refresh (#168712)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:01:01 +02:00
Simone Chemelli
e13b63342e Disable DNS queries in tests (#165603)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-21 20:42:30 +02:00
Erik Montnemery
3500f0a195 Revert "Add Broadlink infrared emitter support to native infrared platform" (#168717) 2026-04-21 18:19:22 +02:00
Øyvind Matheson Wergeland
4a93dcb936 Add data descriptions for nobo_hub config and options flows (#168723)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:02:54 +02:00
Ronald van der Meer
27ddb5b6a4 Claim platinum quality scale for Duco integration (#168719) 2026-04-21 17:30:58 +02:00
Raphael Hehl
0ff38cdc7f Fix/unifi access uah door and thumbnail (#168708) 2026-04-21 17:04:49 +02:00
Mick Vleeshouwer
1a8adea358 Add sensor entity tests to Overkiz (#168701) 2026-04-21 16:53:14 +02:00
Ariel Ebersberger
2a85046584 Fix shelly tests - bluetooth config flow (#166850) 2026-04-21 16:46:33 +02:00
Florent Thoumie
fc85d35d4c Add initial quality scale assessment to iaqualink, set to bronze (#167738)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-04-21 16:39:25 +02:00
Raphael Hehl
608b92be40 unifi: implement action-exceptions quality scale rule (#168559)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-21 16:25:41 +02:00
renovate[bot]
af01b41e52 Update infrared-protocols to 2.0.0 (#168667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-04-21 15:13:58 +01:00
MohamedBarrak3
f257d54d1e Bump mcstatus to 13.1.0 (#168716) 2026-04-21 16:09:14 +02:00
Denis Shulyaka
7c7c075df4 Filter Anthropic schema (#168542) 2026-04-21 09:55:00 -04:00
Denis Shulyaka
5a487d452d Remove retired Claude Haiku 3 model (#168657) 2026-04-21 09:53:56 -04:00
arsenicks
a4138fa4cd Sonos - Add support for TV Autoplay and Ungroup on Autoplay (#167956)
Co-authored-by: Gustav Åkerström <23389010+gustavakerstrom@users.noreply.github.com>
2026-04-21 15:28:39 +02:00
epenet
a6b4609313 Combine AWS hass.data entries into a single dataclass (#168711) 2026-04-21 15:24:14 +02:00
Aaron Ten Clay
95e9405cd0 Preserve Fahrenheit precision in google_assistant temperature range (#168672) 2026-04-21 15:22:21 +02:00
bkobus-bbx
d990ec1b65 Bump blebox_uniapi to v2.5.1 (#168713) 2026-04-21 15:21:24 +02:00
epenet
52d7dcbcc8 Drop redundant BackupManager annotation in aws_s3/google_drive diagnostics (#168714)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:18:57 +02:00
Ronald van der Meer
8e1346fd1f Add dynamic device discovery and stale device removal to Duco integration (#168675) 2026-04-21 15:18:27 +02:00
epenet
a2485960d8 Move Tuya listener classes to separate module (#168636) 2026-04-21 15:15:14 +02:00
epenet
a06ffe6379 Use runtime_data in abode integration (#168709) 2026-04-21 15:05:49 +02:00
Martin Claesson
966e8aeca4 Add Kiosker binary sensor platform (#168507)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-21 14:52:15 +02:00
Abílio Costa
d7f666a661 Implement doorbell.rang trigger (#168388) 2026-04-21 14:43:34 +02:00
Thomas Rupprecht
671b3e01ad Allow requesting spaceapi without authentication and with cors headers (#160797) 2026-04-21 14:31:07 +02:00
Erwin Douna
a85c82ae24 Add dynamic update interval to Tado (#160723)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-04-21 14:28:41 +02:00
Denis Shulyaka
d9af83a03f Fix telegram_bot.send_message_draft action description (#168212)
Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 14:54:08 +03:00
Erik Montnemery
c489980551 Add duration to more conditions (#168383) 2026-04-21 13:41:53 +02:00
epenet
06400ab688 Use runtime_data in zamg (#168699) 2026-04-21 13:06:14 +02:00
epenet
9d7d56c5bf Use runtime_data in Yardian (#168697) 2026-04-21 13:05:09 +02:00
epenet
b1fcc0ebde Use runtime_data in youtube (#168696) 2026-04-21 13:04:49 +02:00
epenet
12af4bd0f4 Use runtime_data in yolink (#168693) 2026-04-21 13:04:19 +02:00
Retha Runolfsson
6bb083ee61 Bump pySwitchbot to 2.1.0 (#168692) 2026-04-21 13:03:47 +02:00
Denis Shulyaka
a6f9246c2f Add myself as a codeowner for OpenAI integration (#168705) 2026-04-21 13:01:45 +02:00
epenet
3222472f10 Use runtime_data in youless (#168694)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-21 12:44:18 +02:00
epenet
e620426002 Use runtime_data in yamaha_musiccast (#168691) 2026-04-21 11:33:02 +02:00
Mike Degatano
6e61a60eba refactor(hassio): store aiohasupervisor models directly in hass.data using typed HassKey (#168400)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 11:24:07 +02:00
epenet
6942066930 Use runtime_data in wiffi integration (#168687) 2026-04-21 10:58:47 +02:00
Marc Mueller
7c1fd1a237 Update aiousbwatcher to 1.1.2 (#168688) 2026-04-21 10:56:00 +02:00
epenet
3fd77b0d7a Use runtime_data in wilight integration (#168686) 2026-04-21 10:47:53 +02:00
Allen Porter
f73f1df5a2 Add Roborock fan speed validation and error handling (#168623) 2026-04-21 10:47:32 +02:00
Florent Thoumie
fb89d94957 Add missing data_description strings to iaqualink (#168670) 2026-04-21 10:30:15 +02:00
epenet
a9c3854d69 Use runtime_data in whois (#168684) 2026-04-21 10:28:45 +02:00
renovate[bot]
ef1a5ea2df Update zizmor (#168666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 10:14:26 +02:00
Raphael Hehl
514d5e570a Bump py-unifi-access to version 1.2.0 (#168679) 2026-04-21 10:13:31 +02:00
epenet
9de658b918 Use runtime_data in WeatherKit (#168682) 2026-04-21 09:43:54 +02:00
Franck Nijhof
ac4e746977 Add reauthentication flow to Fumis integration (#168645) 2026-04-21 09:32:13 +02:00
Mick Vleeshouwer
e10f59c936 Add additional cover fixtures to Overkiz (#168661) 2026-04-21 08:57:28 +02:00
Andres Ruiz
fb171809ec Update waterfurnace to 1.7.1 (#168665) 2026-04-21 08:56:45 +02:00
epenet
137122ebb5 Use runtime_data in weatherflow integration (#168622) 2026-04-21 08:55:50 +02:00
epenet
502dc5075d Use runtime_data in weatherflow_cloud integration (#168624)
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2026-04-21 08:55:29 +02:00
Marc Mueller
42232cfe3f Fix esphome test ResourceWarning (#168181) 2026-04-21 08:55:05 +02:00
epenet
0ae1236acb Use runtime_data in ws66i integration (#168628) 2026-04-21 08:54:49 +02:00
Ariel Ebersberger
63f84af4ff Fix tplink tests for Python 3.14.3 (#168361) 2026-04-21 08:54:21 +02:00
Ronald van der Meer
89fe56c599 Add reconfiguration flow to Duco integration (#168652) 2026-04-21 07:46:50 +02:00
Rene Nulsch
2fb1ed443a Validate directory_path and file_name in telegram_bot.download_file (#168656) 2026-04-21 07:46:43 +02:00
Glenn Vandeuren (aka Iondependent)
ea8f82e9ba Bump nhc to 0.8.0 (#168651)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com>
2026-04-20 22:09:19 +01:00
puddly
31dc02c3ee Bump universal-silabs-flasher to 1.1.0 (#168647) 2026-04-20 23:02:53 +02:00
Nils Ove Erstad
70ec6fa654 Fix MQTT JSON light restoring None color_mode on startup (#168608)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2026-04-20 21:59:03 +02:00
puddly
c2946404ea Bump ZHA to 1.2.1 (#168644) 2026-04-20 15:42:04 -04:00
Abílio Costa
f715bcd7c1 Change Claude gh review agent back to skill (#168642) 2026-04-20 20:59:20 +02:00
Manu
0c0e61e133 Remove hunterjm from Xbox integration codeowners (#167024) 2026-04-20 20:58:43 +02:00
Tomer
305761e7de Victron GX: device_tracker platfrom (#168462)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 20:54:58 +02:00
puddly
3b81f09765 Bump serialx to 1.4.1 (#168640) 2026-04-20 20:53:51 +02:00
epenet
a2cc7d0fca Use runtime_data in watttime integration (#168630) 2026-04-20 20:46:41 +02:00
Ronald van der Meer
038b56e5eb Claim Silver quality scale for Duco integration (#168620) 2026-04-20 19:45:57 +01:00
Franck Nijhof
0edcb8d60f Set parallel updates for PVOutput sensor platform (#168643) 2026-04-20 20:45:11 +02:00
Stefan Agner
cc8000ed89 Remove hassio-main panel registration (#168626)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:42:10 +02:00
Mick Vleeshouwer
a92dcaaf5f Add first cover entity tests to Overkiz (#165670)
Co-authored-by: Copilot <copilot@github.com>
2026-04-20 18:30:26 +01:00
Joakim Plate
e889541d2e Correct state/device class for water in gardena (#168637) 2026-04-20 19:02:29 +02:00
Michael
85e9d3c6a8 Migrate Z-Wave.Me to use runtime_data (#168562) 2026-04-20 18:29:46 +02:00
Robert Resch
fe9db39684 Add docker syntax to all Docker files (#168350) 2026-04-20 17:31:04 +02:00
Assaf Akrabi
253d3e1758 Migrate lib to aiorussound for Russound RNET (#168484) 2026-04-20 17:21:45 +02:00
Raphael Hehl
dcb5f0d533 Improve UniFi config flow quality scale: config-flow and config-flow-test-coverage (#168477)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-20 17:16:51 +02:00
epenet
d5e4be317c Use runtime_data in wolflink integration (#168625) 2026-04-20 17:09:49 +02:00
albaintor
0ebf4d86f5 Fixed Kodi Media Browsing (#165819) 2026-04-20 17:09:03 +02:00
Klaas Schoute
1a86913239 Merge config flows for powerfox integration (#164019) 2026-04-20 17:07:45 +02:00
Max R
f2c010aaaf feat(citybikes): add number of ebikes attribute (#166229) 2026-04-20 17:02:24 +02:00
Erik Montnemery
74de32377e Improve async_get_system_info tests (#168586)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 16:47:40 +02:00
Franck Nijhof
901925ad54 Add Fumis pellet stove integration (#168515) 2026-04-20 16:25:12 +02:00
Øyvind Matheson Wergeland
defbfe17a3 Fix nobo_hub via_device warning (#168595) 2026-04-20 16:25:05 +02:00
Merlin Schumacher
9795f55af3 Remove reference to deprecated state STANDBY from universal media player (#160930)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-04-20 16:10:34 +02:00
johanzander
967c5d2092 Fix KeyError in Growatt server login response handling (#168482)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 15:41:38 +02:00
Paulus Schoutsen
cdecff9380 Use dedicated power commands for LG infrared (#168488)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2026-04-20 15:36:00 +02:00
Robert Resch
59ceb7c58c Revert "Update PyTurboJPEG to 2.2.0" (#168617) 2026-04-20 15:28:17 +02:00
Kurt Chrisford
d66b9f4316 Bump actron-neo-api to 0.5.3 (#167732) 2026-04-20 14:58:43 +02:00
Christophe Gagnier
40477ff87b Bump python-technove to 2.1.1 (#168403)
Co-authored-by: Moustachauve <2206577+Moustachauve@users.noreply.github.com>
2026-04-20 14:52:07 +02:00
epenet
d96b626497 Use runtime_data in vallox integration (#168604)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:41:53 +02:00
epenet
0c294b342c Use runtime_data in verisure integration (#168605)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:39:53 +02:00
Marc Mueller
1f64ca4a8d Update pydantic to 2.13.2 (#168601)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-04-20 14:36:07 +02:00
epenet
79ae0e6c49 Use runtime_data in venstar integration (#168613) 2026-04-20 14:32:13 +02:00
epenet
dc0052552a Use runtime_data in vera integration (#168614) 2026-04-20 14:31:22 +02:00
epenet
77f4baa79e Use runtime_data in volumio integration (#168616) 2026-04-20 14:30:34 +02:00
Matthias Alphart
52377b958b Update knx-frontend to 2026.4.19.175239 (#168568) 2026-04-20 14:28:46 +02:00
Denis Shulyaka
09105693c7 Filter OpenAI schema (#168543) 2026-04-20 14:28:13 +02:00
epenet
db838f67d7 Move vallox service registration to services.py (#168612)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:57 +02:00
renovate[bot]
720fd6d802 Update ruff (#168240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-04-20 14:25:43 +02:00
epenet
b43d6a70da Fix cookie file suppression in verisure (#168609) 2026-04-20 13:41:41 +02:00
Erik Montnemery
b5caabcbae Fix quantum_gateway tests (#168610) 2026-04-20 13:37:37 +02:00
Raphael Hehl
9bb46494d3 unifi: implement parallel-updates quality scale rule (#168563) 2026-04-20 13:26:17 +02:00
Erik Montnemery
ca066b94c5 Deprecate legacy device tracker (#168387) 2026-04-20 13:20:36 +02:00
Erik Montnemery
8de6fa63cd Allow passing a set of event types to logbook.async_subscribe_events (#168163) 2026-04-20 13:19:31 +02:00
Erik Montnemery
866f41791a Deprecate support for local installation of dependencies (#168164) 2026-04-20 13:19:13 +02:00
epenet
5b3d2f823f Always load all platforms in sfr_box (#168594)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 13:05:51 +02:00
Ariel Ebersberger
e1d38fa237 Fix flaky airtouch5 test for Python 3.14.3 (#168366) 2026-04-20 12:51:18 +02:00
epenet
8eef269ce3 Use runtime_data in upb integration (#168600)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 12:50:22 +02:00
David Bonnes
8afee640ef Remove device ids from extra_state_attrs of Evohome's Button entities (#168517)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:17:09 +02:00
Maciej Bieniek
10fd51b34d Add ice phenomena sensor to IMGW-PIB integration (#168548) 2026-04-20 12:07:26 +02:00
Copilot
a1cde0308a Clarify Copilot review guidance for validated entity action inputs (#168449)
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
2026-04-20 11:56:21 +02:00
Erik Montnemery
5c14025e70 Sort keys in dict returned by async_get_system_info (#168585) 2026-04-20 11:31:03 +02:00
epenet
7e5762dcee Use runtime_data in ukraine_alarm integration (#168597)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 11:09:10 +02:00
Franck Nijhof
c425b69373 Set parallel updates for Tailscale platforms (#168596) 2026-04-20 11:09:06 +02:00
Thijs W.
f73ee29ffb Add seek support to frontier_silicon (#168483) 2026-04-20 11:05:42 +02:00
Erik Montnemery
db9c5a6df4 Adjust repair text about unsupported installation method (#168156)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 11:03:48 +02:00
J. Nick Koston
00d16864e3 Bump habluetooth to 6.1.0 (#168576) 2026-04-20 04:01:16 -05:00
Erik Montnemery
2fb22e5654 Use hass_tmp_config_dir fixture in device_tracker tests (#168582) 2026-04-20 10:58:23 +02:00
epenet
65e09c3213 Use runtime_data in toon integration (#168591)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 10:51:13 +02:00
SeifEddineMezned
86eece57c8 Fix grammar and clarity in strings.json (#168577) 2026-04-20 10:47:37 +02:00
trevorvey
e449e28ff5 Updated H590 input source mapping (#168523) 2026-04-20 10:41:49 +02:00
TheJulianJES
6e5b72ea87 Bump matter-python-client to 0.6.0 (#168312) 2026-04-20 10:39:38 +02:00
dependabot[bot]
450aa6d73b Bump actions/cache from 5.0.4 to 5.0.5 (#168583)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 10:39:15 +02:00
dependabot[bot]
953fda87c8 Bump j178/prek-action from 2.0.1 to 2.0.2 (#168584)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 10:39:01 +02:00
epenet
4b38b79ac5 Use runtime_data in todoist integration (#168590)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 10:37:58 +02:00
epenet
7acc412902 Use runtime_data in thethingsnetwork integration (#168589)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 10:28:36 +02:00
Nick Berardi
bf2364e4cb Add First Alert app selection to Lyric auth (#168427)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-04-20 10:20:34 +02:00
Amit Finkelstein
2a6fba3990 Bump hdate to 1.2.1 (#168538) 2026-04-20 10:10:21 +02:00
epenet
6a8220a9df Use runtime_data in tami4 integration (#168587)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:56:18 +02:00
Øyvind Matheson Wergeland
b005fb236f Improve nobo_hub config entry setup (#168550)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-04-20 09:08:49 +02:00
renovate[bot]
528f7625f4 Update zizmor (#168581) 2026-04-20 08:34:51 +02:00
Franck Nijhof
0358696028 Update tailscale to 0.7.0 (#168544)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:31:26 +02:00
Franck Nijhof
ca4b4de20e Migrate Tailscale to use runtime_data (#168556) 2026-04-20 08:30:53 +02:00
Ronald van der Meer
34530810db Enable strict typing for Duco integration (#168572) 2026-04-20 08:30:25 +02:00
Michael
c201275fef Migrate Zeversolar to use runtime_data (#168574) 2026-04-20 08:29:45 +02:00
Tomer
ef2fa67c36 Victron GX: dedupe strings.json (#168460) 2026-04-20 08:26:43 +02:00
Fabian Munkes
0af4dfb7fd Add initial support for PlayerOptions: Select entities to Music Assistant (#167974)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-04-20 05:14:21 +02:00
Thomas55555
894b3bd6a4 Add suggested uom to mop_drying_remaining_time in roborock (#168516) 2026-04-20 03:13:14 +02:00
Franck Nijhof
a317bf9ed1 Remove leftover YAML import code from PVOutput config flow (#168560) 2026-04-19 22:29:34 +02:00
Ronald van der Meer
18a4440668 Handle rate limit error separately in Duco fan platform (#168558) 2026-04-19 22:26:15 +02:00
Franck Nijhof
fc45201f93 Update pvo to v3.0.0 (#168561) 2026-04-19 21:53:11 +02:00
Franck Nijhof
829d3da432 Update peblar to v0.5.1 (#168386)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 21:43:26 +02:00
Franck Nijhof
94cc1e6aed Migrate Rituals Perfume Genie to use runtime_data (#168564) 2026-04-19 21:40:50 +02:00
Yuval Weiss
746846fa74 Add Broadlink infrared emitter support to native infrared platform (#168385) 2026-04-19 14:54:59 -04:00
J. Nick Koston
59986f2a13 Revert "Bump habluetooth to 6.0.0" (#168552) 2026-04-19 20:14:46 +02:00
Franck Nijhof
025a5d31ae Mark reconfiguration-flow as exempt for Twente Milieu (#168040)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:22:02 +02:00
Raphael Hehl
77bd066a71 Improve UniFi quality scale: has-entity-name (#168490)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-19 14:13:11 +02:00
Erwin Douna
4ac4ead186 Firefly III update installation instructions (#168529) 2026-04-19 14:10:52 +02:00
Maciej Bieniek
a532c72459 Bump imgw_pib to 2.1.1 (#168534) 2026-04-19 14:09:27 +02:00
Maciej Bieniek
4a0152b4d7 Bump aioshelly to 13.24.0 (#168533) 2026-04-19 14:09:01 +02:00
Ronald van der Meer
75e9608631 Add zeroconf discovery to Duco integration (#168439) 2026-04-19 14:08:55 +02:00
Simone Chemelli
5301f1d49e Bump aioamazondevices to 13.4.3 (#168536) 2026-04-19 14:06:04 +02:00
Ronald van der Meer
8f75131829 Bump python-duco-client to 0.3.2 (#168528)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-04-19 11:35:04 +02:00
Artem Draft
7ba4b92fa8 Long polling Bravia TV in standby mode (#167364) 2026-04-19 10:30:47 +02:00
David Knowles
d9b4d633d2 Bump pydrawise to 2026.4.0 (#168500) 2026-04-19 08:51:46 +02:00
Denis Shulyaka
93b236ff94 Remove Temperature parameter from Anthropic integration (#168504) 2026-04-19 08:51:02 +02:00
superdingo101
2ec1b12a94 Fix local_calendar recurring event creation failing with UNTIL when HA timezone is non-UTC (#167735) 2026-04-18 17:42:57 -07:00
Paulus Schoutsen
0c2dd5b02f Include uid, recurrence_id, and rrule in calendar event subscription data (#168318)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
Co-authored-by: allenporter <6026418+allenporter@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-19 00:30:01 +02:00
Alistair Francis
5b255d476a husqvarna_automower_ble: Wait for product data (#168426)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2026-04-18 21:18:05 +02:00
Denis Shulyaka
476a04dcb2 Add Claude Opus 4.7 support (#168496) 2026-04-18 16:58:31 +02:00
Denis Shulyaka
2fde105979 Bump anthropic to 0.96.0 (#168487) 2026-04-18 14:47:11 +02:00
David Bonnes
8adc9600f2 Refactor how Evohome allocates names and IDs (#168485)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 14:46:48 +02:00
Khole
e7d26d8a60 Hive - Bump pyhive-integration to 1.0.9 (#168489) 2026-04-18 14:39:31 +02:00
Denis Shulyaka
9fa2430e5f Get deprecated Anthropic models from SDK (#168464)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2026-04-18 13:19:59 +03:00
Maikel Punie
ba0fc4c8be Bump velbusaio to 2026.4.1 (#168473) 2026-04-18 09:17:01 +02:00
Denis Shulyaka
771b1cac6f Use model info from API for Anthropic (#168459) 2026-04-18 07:47:51 +02:00
Joakim Plate
1091a089b4 Allow removing devices that are no longer available in fjaraskupan (#167937) 2026-04-18 00:43:45 +02:00
Tomer
f4649f7fb5 Bump victron-mqtt to 2026.4.17 (#168435) 2026-04-18 00:39:57 +02:00
Denis Shulyaka
67f11f686f Use model info from API for Maximum tokens in Anthropic config flow (#167941) 2026-04-18 00:20:43 +02:00
Steve Easley
6144180f55 Remove inactive codeowner from jvcprojector (#167451) 2026-04-18 00:20:01 +02:00
Martin Claesson
d11d88bb76 Add Kiosker integration (#164543)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-18 00:03:02 +02:00
Ian Foster
9e123b429c Add MAC filter option to ruckus integration (#164706) 2026-04-18 00:00:44 +02:00
Raphael Hehl
07db7f0024 Add new sensor entities for MELCloud Air-to-Water (ATW/Ecodan) heat pump devices (#168105)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-17 23:55:37 +02:00
Michael Rademaker
e0535fb1b2 Add EARN-E P1 Meter integration (#164412)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 23:53:00 +02:00
Raj Laud
0da7c0c15d Fix Victron BLE false reauth on unrecognised advertisement mode bytes (#168209)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 23:49:16 +02:00
Emmanuel Sciara
375a9aa575 Add teleinfo integration for French electricity meters (#167554)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-17 23:39:19 +02:00
Raphael Hehl
1dc03c84a8 Use DoorbellEventType.RING in unifiprotect (#168317)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-17 23:24:21 +02:00
Thijs W.
42d47f7d62 Add repeat and shuffle support to frontier_silicon (#168433) 2026-04-17 23:10:01 +02:00
G Johansson
a55827c01a Add reconfigure to Scrape subentry flows (#168428) 2026-04-17 23:09:06 +02:00
G Johansson
32632cc114 Automatically start subentry flow when creating a scrape config entry (#168437) 2026-04-17 23:07:42 +02:00
Anis Kadri
26d937c36c Add UniFi Access lock rule service (#167949)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 23:07:24 +02:00
Tom Matheussen
ff595b627d Handle specific connection errors for Satel Integra (#168442) 2026-04-17 23:03:30 +02:00
rrooggiieerr
36ba9a1a59 Remove _enable_turn_on_off_backwards_compatibility teslemetry (#168333) 2026-04-17 22:29:07 +02:00
Stefan Agner
32a8344554 Catch HomeAssistantError in ZHA migration retry loops (#168420) 2026-04-17 21:06:12 +02:00
1081 changed files with 85809 additions and 6267 deletions

View File

@@ -1,7 +1,6 @@
---
name: github-pr-reviewer
description: Reviews GitHub pull requests and provides feedback comments.
disallowedTools: Write, Edit
description: Reviews GitHub pull requests and provides feedback comments. This is the top skill to use for reviewing Pull Requests from GitHub.
---
# Review GitHub Pull Request
@@ -28,12 +27,13 @@ disallowedTools: Write, Edit
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.

View File

@@ -1,5 +1,5 @@
---
name: Home Assistant Integration knowledge
name: ha-integration-knowledge
description: Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
---
@@ -12,6 +12,10 @@ description: Everything you need to know to build, test and review Home Assistan
- When looking for examples, prefer integrations with the platinum or gold quality scale level first.
- Polling intervals are NOT user-configurable. Never add scan_interval, update_interval, or polling frequency options to config flows or config entries.
- Do NOT allow users to set config entry names in config flows. Names are automatically generated or can be customized later in UI. Exception: helper integrations may allow custom names.
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection

View File

@@ -32,7 +32,10 @@ Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) ov
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
# Skills
- Home Assistant Integration knowledge: .claude/skills/integrations/SKILL.md
- ha-integration-knowledge: .claude/skills/ha-integration-knowledge/SKILL.md

11
.github/renovate.json vendored
View File

@@ -6,6 +6,7 @@
"pep621",
"pip_requirements",
"pre-commit",
"regex",
"homeassistant-manifest"
],
@@ -26,6 +27,16 @@
]
},
"regexManagers": [
{
"description": "Update ruff required-version in pyproject.toml",
"managerFilePatterns": ["/^pyproject\\.toml$/"],
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
"depNameTemplate": "ruff",
"datasourceTemplate": "pypi"
}
],
"minimumReleaseAge": "7 days",
"prConcurrentLimit": 10,
"prHourlyLimit": 2,

View File

@@ -282,7 +282,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -303,7 +303,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
with:
extra-args: --all-files zizmor
@@ -366,7 +366,7 @@ jobs:
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
@@ -374,7 +374,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -386,7 +386,7 @@ jobs:
env.HA_SHORT_VERSION }}-
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
@@ -432,7 +432,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -486,7 +486,7 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -517,7 +517,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -554,7 +554,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -645,7 +645,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -696,7 +696,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -749,7 +749,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -806,7 +806,7 @@ jobs:
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -814,7 +814,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .mypy_cache
key: >-
@@ -856,7 +856,7 @@ jobs:
- base
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -889,7 +889,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -932,7 +932,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -966,7 +966,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -1084,7 +1084,7 @@ jobs:
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1119,7 +1119,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -1242,7 +1242,7 @@ jobs:
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1279,7 +1279,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true
@@ -1425,7 +1425,7 @@ jobs:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- name: Restore apt cache
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.APT_CACHE_DIR }}
@@ -1459,7 +1459,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
fail-on-cache-miss: true

View File

@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
category: "/language:python"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.1
rev: v0.15.10
hooks:
- id: ruff-check
args:
@@ -18,7 +18,7 @@ repos:
exclude_types: [csv, json, html]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.23.1
rev: v1.24.1
hooks:
- id: zizmor
args:

View File

@@ -46,6 +46,7 @@ homeassistant.components.accuweather.*
homeassistant.components.acer_projector.*
homeassistant.components.acmeda.*
homeassistant.components.actiontec.*
homeassistant.components.actron_air.*
homeassistant.components.adax.*
homeassistant.components.adguard.*
homeassistant.components.aftership.*
@@ -178,6 +179,7 @@ homeassistant.components.dropbox.*
homeassistant.components.droplet.*
homeassistant.components.dsmr.*
homeassistant.components.duckdns.*
homeassistant.components.duco.*
homeassistant.components.dunehd.*
homeassistant.components.duotecno.*
homeassistant.components.easyenergy.*
@@ -222,6 +224,7 @@ homeassistant.components.fronius.*
homeassistant.components.frontend.*
homeassistant.components.fujitsu_fglair.*
homeassistant.components.fully_kiosk.*
homeassistant.components.fumis.*
homeassistant.components.fyta.*
homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.*
@@ -552,6 +555,7 @@ homeassistant.components.tcp.*
homeassistant.components.technove.*
homeassistant.components.tedee.*
homeassistant.components.telegram_bot.*
homeassistant.components.teleinfo.*
homeassistant.components.teslemetry.*
homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*

View File

@@ -22,3 +22,6 @@ Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) ov
## Good practices
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.

20
CODEOWNERS generated
View File

@@ -400,6 +400,8 @@ CLAUDE.md @home-assistant/core
/tests/components/dnsip/ @gjohansson-ST
/homeassistant/components/door/ @home-assistant/core
/tests/components/door/ @home-assistant/core
/homeassistant/components/doorbell/ @home-assistant/core
/tests/components/doorbell/ @home-assistant/core
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery
@@ -430,6 +432,8 @@ CLAUDE.md @home-assistant/core
/tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k
/tests/components/eafm/ @Jc2k
/homeassistant/components/earn_e_p1/ @Miggets7
/tests/components/earn_e_p1/ @Miggets7
/homeassistant/components/easyenergy/ @klaasnicolaas
/tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecoforest/ @pjanuario
@@ -590,6 +594,8 @@ CLAUDE.md @home-assistant/core
/tests/components/fujitsu_fglair/ @crevetor
/homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fumis/ @frenck
/tests/components/fumis/ @frenck
/homeassistant/components/fyta/ @dontinelli
/tests/components/fyta/ @dontinelli
/homeassistant/components/garage_door/ @home-assistant/core
@@ -900,8 +906,8 @@ CLAUDE.md @home-assistant/core
/tests/components/jewish_calendar/ @tsvi
/homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
/tests/components/jvc_projector/ @SteveEasley @msavazzi
/homeassistant/components/jvc_projector/ @SteveEasley
/tests/components/jvc_projector/ @SteveEasley
/homeassistant/components/kaiterra/ @Michsior14
/homeassistant/components/kaleidescape/ @SteveEasley
/tests/components/kaleidescape/ @SteveEasley
@@ -914,6 +920,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/keyboard_remote/ @bendavid @lanrat
/homeassistant/components/keymitt_ble/ @spycle
/tests/components/keymitt_ble/ @spycle
/homeassistant/components/kiosker/ @Claeysson
/tests/components/kiosker/ @Claeysson
/homeassistant/components/kitchen_sink/ @home-assistant/core
/tests/components/kitchen_sink/ @home-assistant/core
/homeassistant/components/kmtronic/ @dgomes
@@ -1247,6 +1255,8 @@ CLAUDE.md @home-assistant/core
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek @ab3lson
/tests/components/open_router/ @joostlek @ab3lson
/homeassistant/components/openai_conversation/ @Shulyaka
/tests/components/openai_conversation/ @Shulyaka
/homeassistant/components/opendisplay/ @g4bri3lDev
/tests/components/opendisplay/ @g4bri3lDev
/homeassistant/components/openerz/ @misialq
@@ -1728,6 +1738,8 @@ CLAUDE.md @home-assistant/core
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/telegram_bot/ @hanwg
/tests/components/telegram_bot/ @hanwg
/homeassistant/components/teleinfo/ @esciara
/tests/components/teleinfo/ @esciara
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/teltonika/ @karlbeecken
@@ -1983,8 +1995,8 @@ CLAUDE.md @home-assistant/core
/tests/components/wsdot/ @ucodery
/homeassistant/components/wyoming/ @synesthesiam
/tests/components/wyoming/ @synesthesiam
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
/tests/components/xbox/ @hunterjm @tr4nt0r
/homeassistant/components/xbox/ @tr4nt0r
/tests/components/xbox/ @tr4nt0r
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79

1
Dockerfile generated
View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile@sha256:2780b5c3bab67f1f76c781860de469442999ed1a0d7992a5efdf2cffc0e3d769
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

View File

@@ -30,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import CONF_POLLING, DOMAIN, DOMAIN_DATA, LOGGER
from .const import CONF_POLLING, DOMAIN, LOGGER
from .services import async_setup_services
ATTR_DEVICE_NAME = "device_name"
@@ -67,13 +67,16 @@ class AbodeSystem:
logout_listener: CALLBACK_TYPE | None = None
type AbodeConfigEntry = ConfigEntry[AbodeSystem]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
"""Set up Abode integration from a config entry."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
@@ -99,50 +102,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except (AbodeException, ConnectTimeout, HTTPError) as ex:
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex
hass.data[DOMAIN_DATA] = AbodeSystem(abode, polling)
entry.runtime_data = AbodeSystem(abode, polling)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await setup_hass_events(hass)
await hass.async_add_executor_job(setup_abode_events, hass)
await setup_hass_events(hass, entry)
await hass.async_add_executor_job(setup_abode_events, hass, entry)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def _shutdown_client(abode: Abode) -> None:
"""Shutdown client."""
abode.events.stop()
abode.logout()
async def async_unload_entry(hass: HomeAssistant, entry: AbodeConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.stop)
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.logout)
await hass.async_add_executor_job(_shutdown_client, entry.runtime_data.abode)
if logout_listener := hass.data[DOMAIN_DATA].logout_listener:
if logout_listener := entry.runtime_data.logout_listener:
logout_listener()
hass.data.pop(DOMAIN_DATA)
return unload_ok
async def setup_hass_events(hass: HomeAssistant) -> None:
async def setup_hass_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
"""Home Assistant start and stop callbacks."""
def logout(event: Event) -> None:
"""Logout of Abode."""
if not hass.data[DOMAIN_DATA].polling:
hass.data[DOMAIN_DATA].abode.events.stop()
if not entry.runtime_data.polling:
entry.runtime_data.abode.events.stop()
hass.data[DOMAIN_DATA].abode.logout()
entry.runtime_data.abode.logout()
LOGGER.info("Logged out of Abode")
if not hass.data[DOMAIN_DATA].polling:
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.start)
if not entry.runtime_data.polling:
await hass.async_add_executor_job(entry.runtime_data.abode.events.start)
hass.data[DOMAIN_DATA].logout_listener = hass.bus.async_listen_once(
entry.runtime_data.logout_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, logout
)
def setup_abode_events(hass: HomeAssistant) -> None:
def setup_abode_events(hass: HomeAssistant, entry: AbodeConfigEntry) -> None:
"""Event callbacks."""
def event_callback(event: str, event_json: dict[str, str]) -> None:
@@ -179,6 +186,6 @@ def setup_abode_events(hass: HomeAssistant) -> None:
]
for event in events:
hass.data[DOMAIN_DATA].abode.events.add_event_callback(
entry.runtime_data.abode.events.add_event_callback(
event, partial(event_callback, event)
)

View File

@@ -9,21 +9,20 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode alarm control panel device."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
)

View File

@@ -10,22 +10,21 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode binary sensor devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
device_types = [
"connectivity",

View File

@@ -12,14 +12,13 @@ import requests
from requests.models import Response
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import Throttle
from . import AbodeSystem
from .const import DOMAIN_DATA, LOGGER
from . import AbodeConfigEntry, AbodeSystem
from .const import LOGGER
from .entity import AbodeDevice
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
@@ -27,11 +26,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode camera devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)

View File

@@ -3,17 +3,10 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import AbodeSystem
LOGGER = logging.getLogger(__package__)
DOMAIN = "abode"
DOMAIN_DATA: HassKey[AbodeSystem] = HassKey(DOMAIN)
ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = "polling"

View File

@@ -5,21 +5,20 @@ from typing import Any
from jaraco.abode.devices.cover import Cover
from homeassistant.components.cover import CoverEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode cover devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
AbodeCover(data, device)

View File

@@ -7,7 +7,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from . import AbodeSystem
from .const import ATTRIBUTION, DOMAIN, DOMAIN_DATA
from .const import ATTRIBUTION, DOMAIN
class AbodeEntity(Entity):
@@ -29,7 +29,7 @@ class AbodeEntity(Entity):
self._update_connection_status,
)
self.hass.data[DOMAIN_DATA].entity_ids.add(self.entity_id)
self._data.entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from Abode connection status updates."""

View File

@@ -16,21 +16,20 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode light devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
AbodeLight(data, device)

View File

@@ -5,21 +5,20 @@ from typing import Any
from jaraco.abode.devices.lock import Lock
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeDevice
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode lock devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
AbodeLock(data, device)

View File

@@ -14,13 +14,11 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AbodeSystem
from .const import DOMAIN_DATA
from . import AbodeConfigEntry, AbodeSystem
from .entity import AbodeDevice
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
@@ -66,11 +64,11 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode sensor devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
async_add_entities(
AbodeSensor(data, device, description)

View File

@@ -2,15 +2,21 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, DOMAIN_DATA, LOGGER
from .const import DOMAIN, LOGGER
if TYPE_CHECKING:
from . import AbodeConfigEntry, AbodeSystem
ATTR_SETTING = "setting"
ATTR_VALUE = "value"
@@ -25,13 +31,21 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def _get_abode_system(hass: HomeAssistant) -> AbodeSystem:
"""Return the Abode system for the loaded config entry."""
entries: list[AbodeConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError("Abode integration is not loaded")
return entries[0].runtime_data
def _change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
call.hass.data[DOMAIN_DATA].abode.set_setting(setting, value)
_get_abode_system(call.hass).abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
@@ -42,7 +56,7 @@ def _capture_image(call: ServiceCall) -> None:
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
for entity_id in _get_abode_system(call.hass).entity_ids
if entity_id in entity_ids
]
@@ -57,7 +71,7 @@ def _trigger_automation(call: ServiceCall) -> None:
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
for entity_id in _get_abode_system(call.hass).entity_ids
if entity_id in entity_ids
]

View File

@@ -7,12 +7,11 @@ from typing import Any, cast
from jaraco.abode.devices.switch import Switch
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN_DATA
from . import AbodeConfigEntry
from .entity import AbodeAutomation, AbodeDevice
DEVICE_TYPES = ["switch", "valve"]
@@ -20,11 +19,11 @@ DEVICE_TYPES = ["switch", "valve"]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: AbodeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Abode switch devices."""
data = hass.data[DOMAIN_DATA]
data = entry.runtime_data
entities: list[SwitchEntity] = [
AbodeSwitch(data, device)

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["serialx==1.2.2"]
"requirements": ["serialx==1.4.1"]
}

View File

@@ -15,8 +15,10 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, ActronAirZoneEntity, actron_air_command
@@ -139,20 +141,24 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
@actron_air_command
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set a new fan mode."""
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR[fan_mode]
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR[hvac_mode]
await self._status.ac_system.set_system_mode(ac_mode)
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
await self._status.user_aircon_settings.set_temperature(temperature=temp)
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="temperature_missing",
)
await self._status.user_aircon_settings.set_temperature(temperature=temperature)
class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
@@ -221,4 +227,9 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="temperature_missing",
)
await self._zone.set_temperature(temperature=temperature)

View File

@@ -23,7 +23,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
self._user_code: str = ""
self._verification_uri: str = ""
self._expires_minutes: str = "30"
self.login_task: asyncio.Task | None = None
self.login_task: asyncio.Task[None] | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -94,7 +94,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("Error getting user info: %s", err)
return self.async_abort(reason="oauth2_error")
unique_id = str(user_data["id"])
unique_id = user_data.sub
await self.async_set_unique_id(unique_id)
# Check if this is a reauth flow
@@ -107,7 +107,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_data["email"],
title=user_data.email,
data={CONF_API_TOKEN: self._api.refresh_token_value},
)

View File

@@ -78,7 +78,14 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
translation_placeholders={"error": repr(err)},
) from err
self.status = self.api.state_manager.get_status(self.serial_number)
status = self.api.state_manager.get_status(self.serial_number)
if status is None:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": "Status not available"},
)
self.status = status
self.last_seen = dt_util.utcnow()
return self.status

View File

@@ -24,7 +24,7 @@ def actron_air_command[_EntityT: ActronAirEntity, **_P](
"""
@wraps(func)
async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
async def wrapper(self: _EntityT, /, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap API calls with exception handling."""
try:
await func(self, *args, **kwargs)

View File

@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.0"]
"requirements": ["actron-neo-api==0.5.3"]
}

View File

@@ -69,4 +69,4 @@ rules:
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: todo
strict-typing: done

View File

@@ -58,6 +58,9 @@
"setup_connection_error": {
"message": "Failed to connect to the Actron Air API"
},
"temperature_missing": {
"message": "Provide a temperature value when adjusting the climate entity."
},
"update_error": {
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
}

View File

@@ -36,7 +36,9 @@ def _make_detected_condition(
) -> type[Condition]:
"""Create a detected condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_ON,
support_duration=True,
)
@@ -45,7 +47,9 @@ def _make_cleared_condition(
) -> type[Condition]:
"""Create a cleared condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_OFF,
support_duration=True,
)

View File

@@ -249,6 +249,11 @@
.condition_binary_common: &condition_binary_common
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_gas_detected:
<<: *condition_binary_common

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -24,6 +25,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Carbon monoxide cleared"
@@ -33,6 +37,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Carbon monoxide detected"
@@ -54,6 +61,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Gas cleared"
@@ -63,6 +73,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Gas detected"
@@ -168,6 +181,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Smoke cleared"
@@ -177,6 +193,9 @@
"fields": {
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
}
},
"name": "Smoke detected"

View File

@@ -4,6 +4,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
Condition,
EntityStateConditionBase,
make_entity_state_condition,
@@ -25,6 +26,7 @@ class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
"""State condition."""
_required_features: int
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain with the required features."""
@@ -82,9 +84,11 @@ CONDITIONS: dict[str, type[Condition]] = {
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
"is_disarmed": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.DISARMED, support_duration=True
),
"is_triggered": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.TRIGGERED
DOMAIN, AlarmControlPanelState.TRIGGERED, support_duration=True
),
}

View File

@@ -1,9 +1,9 @@
.condition_common: &condition_common
target:
target: &condition_common_target
entity:
domain: alarm_control_panel
fields: &condition_common_fields
behavior:
behavior: &condition_common_behavior
required: true
default: any
selector:
@@ -13,10 +13,20 @@
- all
- any
.condition_common_for: &condition_common_for
target: *condition_common_target
fields: &condition_common_for_fields
behavior: *condition_common_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_armed: *condition_common
is_armed_away:
fields: *condition_common_fields
fields: *condition_common_for_fields
target:
entity:
domain: alarm_control_panel
@@ -24,7 +34,7 @@ is_armed_away:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
is_armed_home:
fields: *condition_common_fields
fields: *condition_common_for_fields
target:
entity:
domain: alarm_control_panel
@@ -32,7 +42,7 @@ is_armed_home:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
is_armed_night:
fields: *condition_common_fields
fields: *condition_common_for_fields
target:
entity:
domain: alarm_control_panel
@@ -40,13 +50,13 @@ is_armed_night:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
is_armed_vacation:
fields: *condition_common_fields
fields: *condition_common_for_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
is_disarmed: *condition_common
is_disarmed: *condition_common_for
is_triggered: *condition_common
is_triggered: *condition_common_for

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -19,6 +20,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed away"
@@ -28,6 +32,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed home"
@@ -37,6 +44,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed night"
@@ -46,6 +56,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed vacation"
@@ -55,6 +68,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is disarmed"
@@ -64,6 +80,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is triggered"

View File

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

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from anthropic.resources.messages.messages import DEPRECATED_MODELS
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
@@ -13,13 +15,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CHAT_MODEL,
DEFAULT_CONVERSATION_NAME,
DEPRECATED_MODELS,
DOMAIN,
LOGGER,
)
from .const import CONF_CHAT_MODEL, DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
@@ -44,9 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
entry.async_on_unload(entry.add_update_listener(async_update_options))
for subentry in entry.subentries.values():
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model.startswith(
tuple(DEPRECATED_MODELS)
):
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model in DEPRECATED_MODELS:
ir.async_create_issue(
hass,
DOMAIN,
@@ -236,6 +230,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
)
hass.config_entries.async_update_entry(entry, minor_version=3)
if entry.version == 2 and entry.minor_version == 3:
# Remove Temperature parameter
CONF_TEMPERATURE = "temperature"
for subentry in entry.subentries.values():
data = subentry.data.copy()
if CONF_TEMPERATURE not in data:
continue
data.pop(CONF_TEMPERATURE, None)
hass.config_entries.async_update_subentry(entry, subentry, data=data)
hass.config_entries.async_update_entry(entry, minor_version=4)
LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)

View File

@@ -29,6 +29,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.selector import (
NumberSelector,
@@ -42,14 +43,12 @@ from homeassistant.helpers.selector import (
from homeassistant.helpers.typing import VolDictType
from .const import (
CODE_EXECUTION_UNSUPPORTED_MODELS,
CONF_CHAT_MODEL,
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_PROMPT_CACHING,
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
CONF_THINKING_EFFORT,
CONF_TOOL_SEARCH,
@@ -64,10 +63,8 @@ from .const import (
DEFAULT_AI_TASK_NAME,
DEFAULT_CONVERSATION_NAME,
DOMAIN,
NON_ADAPTIVE_THINKING_MODELS,
NON_THINKING_MODELS,
MIN_THINKING_BUDGET,
TOOL_SEARCH_UNSUPPORTED_MODELS,
WEB_SEARCH_UNSUPPORTED_MODELS,
PromptCaching,
)
from .coordinator import model_alias
@@ -109,7 +106,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 2
MINOR_VERSION = 3
MINOR_VERSION = 4
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -324,14 +321,6 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
): SelectSelector(
SelectSelectorConfig(options=self._get_model_list(), custom_value=True)
),
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT[CONF_MAX_TOKENS],
): int,
vol.Optional(
CONF_TEMPERATURE,
default=DEFAULT[CONF_TEMPERATURE],
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
vol.Optional(
CONF_PROMPT_CACHING,
default=DEFAULT[CONF_PROMPT_CACHING],
@@ -384,30 +373,59 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
"""Manage model-specific options."""
errors: dict[str, str] = {}
step_schema: VolDictType = {}
step_schema: VolDictType = {
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT[CONF_MAX_TOKENS],
): vol.All(
NumberSelector(
NumberSelectorConfig(min=0, max=self.model_info.max_tokens)
),
vol.Coerce(int),
)
if self.model_info.max_tokens
else cv.positive_int,
}
model = self.options[CONF_CHAT_MODEL]
if not model.startswith(tuple(NON_THINKING_MODELS)) and model.startswith(
tuple(NON_ADAPTIVE_THINKING_MODELS)
if (
self.model_info.capabilities
and self.model_info.capabilities.thinking.supported
and not self.model_info.capabilities.thinking.types.adaptive.supported
):
step_schema[
vol.Optional(
CONF_THINKING_BUDGET, default=DEFAULT[CONF_THINKING_BUDGET]
)
] = vol.All(
NumberSelector(
NumberSelectorConfig(
min=0,
max=self.options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
)
),
vol.Coerce(int),
] = (
vol.All(
NumberSelector(
NumberSelectorConfig(min=0, max=self.model_info.max_tokens)
),
vol.Coerce(int),
)
if self.model_info.max_tokens
else cv.positive_int
)
else:
self.options.pop(CONF_THINKING_BUDGET, None)
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
if (
self.model_info.capabilities
and (effort_capability := self.model_info.capabilities.effort).supported
):
effort_options: list[str] = []
if self.model_info.capabilities.thinking.types.adaptive.supported:
effort_options.append("none")
if effort_capability.low.supported:
effort_options.append("low")
if effort_capability.medium.supported:
effort_options.append("medium")
if effort_capability.high.supported:
effort_options.append("high")
if effort_capability.xhigh and effort_capability.xhigh.supported:
effort_options.append("xhigh")
if effort_capability.max.supported:
effort_options.append("max")
step_schema[
vol.Optional(
CONF_THINKING_EFFORT,
@@ -415,7 +433,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
)
] = SelectSelector(
SelectSelectorConfig(
options=["none", "low", "medium", "high", "max"],
options=effort_options,
translation_key=CONF_THINKING_EFFORT,
mode=SelectSelectorMode.DROPDOWN,
)
@@ -423,43 +441,34 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
else:
self.options.pop(CONF_THINKING_EFFORT, None)
if not model.startswith(tuple(CODE_EXECUTION_UNSUPPORTED_MODELS)):
step_schema[
step_schema.update(
{
vol.Optional(
CONF_CODE_EXECUTION,
default=DEFAULT[CONF_CODE_EXECUTION],
)
] = bool
else:
self.options.pop(CONF_CODE_EXECUTION, None)
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
step_schema.update(
{
vol.Optional(
CONF_WEB_SEARCH,
default=DEFAULT[CONF_WEB_SEARCH],
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
): bool,
}
)
else:
self.options.pop(CONF_WEB_SEARCH, None)
self.options.pop(CONF_WEB_SEARCH_MAX_USES, None)
self.options.pop(CONF_WEB_SEARCH_USER_LOCATION, None)
): bool,
vol.Optional(
CONF_WEB_SEARCH,
default=DEFAULT[CONF_WEB_SEARCH],
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=DEFAULT[CONF_WEB_SEARCH_MAX_USES],
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=DEFAULT[CONF_WEB_SEARCH_USER_LOCATION],
): bool,
}
)
self.options.pop(CONF_WEB_SEARCH_CITY, None)
self.options.pop(CONF_WEB_SEARCH_REGION, None)
self.options.pop(CONF_WEB_SEARCH_COUNTRY, None)
self.options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
model = self.options[CONF_CHAT_MODEL]
if not model.startswith(tuple(TOOL_SEARCH_UNSUPPORTED_MODELS)):
step_schema[
vol.Optional(
@@ -471,9 +480,19 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
self.options.pop(CONF_TOOL_SEARCH, None)
if not step_schema:
user_input = {}
# Currently our schema is always present, but if one day it becomes empty,
# then the below line is needed to skip this step
user_input = {} # pragma: no cover
if user_input is not None:
if (
CONF_THINKING_BUDGET in user_input
and user_input[CONF_THINKING_BUDGET] >= MIN_THINKING_BUDGET
and user_input[CONF_THINKING_BUDGET]
>= user_input.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS])
):
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
if user_input.get(CONF_WEB_SEARCH, DEFAULT[CONF_WEB_SEARCH]) and not errors:
if user_input.get(
CONF_WEB_SEARCH_USER_LOCATION,

View File

@@ -15,7 +15,6 @@ CONF_CHAT_MODEL = "chat_model"
CONF_CODE_EXECUTION = "code_execution"
CONF_MAX_TOKENS = "max_tokens"
CONF_PROMPT_CACHING = "prompt_caching"
CONF_TEMPERATURE = "temperature"
CONF_THINKING_BUDGET = "thinking_budget"
CONF_THINKING_EFFORT = "thinking_effort"
CONF_TOOL_SEARCH = "tool_search"
@@ -43,7 +42,6 @@ DEFAULT = {
CONF_CODE_EXECUTION: False,
CONF_MAX_TOKENS: 3000,
CONF_PROMPT_CACHING: PromptCaching.PROMPT.value,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: MIN_THINKING_BUDGET,
CONF_THINKING_EFFORT: "low",
CONF_TOOL_SEARCH: False,
@@ -52,54 +50,6 @@ DEFAULT = {
CONF_WEB_SEARCH_MAX_USES: 5,
}
NON_THINKING_MODELS = [
"claude-3-haiku",
]
NON_ADAPTIVE_THINKING_MODELS = [
"claude-opus-4-5",
"claude-sonnet-4-5",
"claude-haiku-4-5",
"claude-opus-4-1",
"claude-opus-4-0",
"claude-opus-4-20250514",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-3-haiku",
]
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS = [
"claude-opus-4-1",
"claude-opus-4-0",
"claude-opus-4-20250514",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-3-haiku",
]
WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
CODE_EXECUTION_UNSUPPORTED_MODELS = [
"claude-3-haiku",
]
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS = [
"claude-haiku-4-5",
"claude-opus-4-1",
"claude-opus-4-0",
"claude-opus-4-20250514",
"claude-sonnet-4-0",
"claude-sonnet-4-20250514",
"claude-3-haiku",
]
TOOL_SEARCH_UNSUPPORTED_MODELS = [
"claude-3",
"claude-haiku",
]
DEPRECATED_MODELS = [
"claude-3",
]

View File

@@ -28,9 +28,7 @@ _model_short_form = re.compile(r"[^\d]-\d$")
@callback
def model_alias(model_id: str) -> str:
"""Resolve alias from versioned model name."""
if model_id == "claude-3-haiku-20240307" or model_id.endswith("-preview"):
return model_id
if model_id[-2:-1] != "-":
if model_id[-2:-1] != "-" and not model_id.endswith("-preview"):
model_id = model_id[:-9]
if _model_short_form.search(model_id):
return model_id + "-0"

View File

@@ -30,6 +30,7 @@ from anthropic.types import (
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
ModelInfo,
OutputConfigParam,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
@@ -97,7 +98,6 @@ from .const import (
CONF_CODE_EXECUTION,
CONF_MAX_TOKENS,
CONF_PROMPT_CACHING,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
CONF_THINKING_EFFORT,
CONF_TOOL_SEARCH,
@@ -112,10 +112,6 @@ from .const import (
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
NON_ADAPTIVE_THINKING_MODELS,
NON_THINKING_MODELS,
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS,
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
PromptCaching,
)
from .coordinator import AnthropicConfigEntry, AnthropicCoordinator
@@ -128,10 +124,14 @@ def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
unsupported_keys = {"oneOf", "anyOf", "allOf"}
schema = convert(tool.parameters, custom_serializer=custom_serializer)
schema = {k: v for k, v in schema.items() if k not in unsupported_keys}
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
input_schema=schema,
)
@@ -757,33 +757,43 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
):
model_args["cache_control"] = {"type": "ephemeral"}
if not model.startswith(tuple(NON_ADAPTIVE_THINKING_MODELS)):
if (
self.model_info.capabilities
and self.model_info.capabilities.thinking.types.adaptive.supported
):
thinking_effort = options.get(
CONF_THINKING_EFFORT, DEFAULT[CONF_THINKING_EFFORT]
)
if thinking_effort != "none":
model_args["thinking"] = ThinkingConfigAdaptiveParam(type="adaptive")
model_args["thinking"] = ThinkingConfigAdaptiveParam(
type="adaptive", display="summarized"
)
model_args["output_config"] = OutputConfigParam(effort=thinking_effort)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
)
else:
thinking_budget = options.get(
CONF_THINKING_BUDGET, DEFAULT[CONF_THINKING_BUDGET]
)
if (
not model.startswith(tuple(NON_THINKING_MODELS))
self.model_info.capabilities
and self.model_info.capabilities.thinking.types.enabled.supported
and thinking_budget >= MIN_THINKING_BUDGET
):
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
type="enabled", display="summarized", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, DEFAULT[CONF_TEMPERATURE]
if (
self.model_info.capabilities
and self.model_info.capabilities.effort.supported
):
model_args["output_config"] = OutputConfigParam(
effort=options.get(
CONF_THINKING_EFFORT, DEFAULT[CONF_THINKING_EFFORT]
)
)
tools: list[ToolUnionParam] = []
@@ -795,9 +805,11 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
if options.get(CONF_CODE_EXECUTION):
# The `web_search_20260209` tool automatically enables `code_execution_20260120` tool
if model.startswith(
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
) or not options.get(CONF_WEB_SEARCH):
if (
not self.model_info.capabilities
or not self.model_info.capabilities.code_execution.supported
or not options.get(CONF_WEB_SEARCH)
):
tools.append(
CodeExecutionTool20250825Param(
name="code_execution",
@@ -806,9 +818,11 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
)
if options.get(CONF_WEB_SEARCH):
if model.startswith(
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
) or not options.get(CONF_CODE_EXECUTION):
if (
not self.model_info.capabilities
or not self.model_info.capabilities.code_execution.supported
or not options.get(CONF_CODE_EXECUTION)
):
web_search: WebSearchTool20250305Param | WebSearchTool20260209Param = (
WebSearchTool20250305Param(
name="web_search",
@@ -846,12 +860,17 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
]
last_message["content"].extend( # type: ignore[union-attr]
await async_prepare_files_for_prompt(
self.hass, [(a.path, a.mime_type) for a in last_content.attachments]
self.hass,
self.model_info,
[(a.path, a.mime_type) for a in last_content.attachments],
)
)
if structure and structure_name:
if not model.startswith(tuple(UNSUPPORTED_STRUCTURED_OUTPUT_MODELS)):
if (
self.model_info.capabilities
and self.model_info.capabilities.structured_outputs.supported
):
# Native structured output for those models who support it.
structure_name = None
model_args.setdefault("output_config", OutputConfigParam())[
@@ -992,7 +1011,7 @@ class AnthropicBaseLLMEntity(CoordinatorEntity[AnthropicCoordinator]):
async def async_prepare_files_for_prompt(
hass: HomeAssistant, files: list[tuple[Path, str | None]]
hass: HomeAssistant, model_info: ModelInfo, files: list[tuple[Path, str | None]]
) -> Iterable[ImageBlockParam | DocumentBlockParam]:
"""Append files to a prompt.
@@ -1013,13 +1032,26 @@ async def async_prepare_files_for_prompt(
if mime_type is None:
mime_type = guess_file_type(file_path)[0]
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
if (
not mime_type
or not mime_type.startswith(("image/", "application/pdf"))
or not model_info.capabilities
or (
mime_type.startswith("image/")
and not model_info.capabilities.image_input.supported
)
or (
mime_type.startswith("application/pdf")
and not model_info.capabilities.pdf_input.supported
)
):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_type",
translation_placeholders={
"file_path": file_path.as_posix(),
"mime_type": mime_type or "unknown",
"model": model_info.display_name,
},
)
if mime_type == "image/jpg":

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["anthropic==0.92.0"]
"requirements": ["anthropic==0.96.0"]
}

View File

@@ -6,6 +6,7 @@ from collections.abc import Iterator
from typing import TYPE_CHECKING
import anthropic
from anthropic.resources.messages.messages import DEPRECATED_MODELS
import voluptuous as vol
from homeassistant import data_entry_flow
@@ -19,7 +20,7 @@ from homeassistant.helpers.selector import (
SelectSelectorConfig,
)
from .const import CONF_CHAT_MODEL, DEPRECATED_MODELS, DOMAIN
from .const import CONF_CHAT_MODEL, DOMAIN
from .coordinator import model_alias
if TYPE_CHECKING:
@@ -63,7 +64,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
model_list = [
model_option
for model_option in await self.get_model_list(client)
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
if model_option["value"] not in DEPRECATED_MODELS
]
self._model_list_cache[entry.entry_id] = model_list
@@ -105,6 +106,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
"model": model,
"subentry_name": subentry.title,
"subentry_type": self._format_subentry_type(subentry.subentry_type),
"retirement_date": DEPRECATED_MODELS[model],
},
)
@@ -131,7 +133,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
continue
for subentry in entry.subentries.values():
model = subentry.data.get(CONF_CHAT_MODEL)
if model and model.startswith(tuple(DEPRECATED_MODELS)):
if model and model in DEPRECATED_MODELS:
yield entry.entry_id, subentry.subentry_id
async def _async_next_target(
@@ -158,7 +160,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
continue
model = subentry.data.get(CONF_CHAT_MODEL)
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
if not model or model not in DEPRECATED_MODELS:
continue
self._current_entry_id = entry_id

View File

@@ -40,7 +40,8 @@
"entry_type": "AI task",
"error": {
"api_error": "[%key:component::anthropic::config_subentries::conversation::error::api_error%]",
"model_not_found": "[%key:component::anthropic::config_subentries::conversation::error::model_not_found%]"
"model_not_found": "[%key:component::anthropic::config_subentries::conversation::error::model_not_found%]",
"thinking_budget_too_large": "[%key:component::anthropic::config_subentries::conversation::error::thinking_budget_too_large%]"
},
"initiate_flow": {
"reconfigure": "Reconfigure AI task",
@@ -50,13 +51,11 @@
"advanced": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::max_tokens%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::prompt_caching%]",
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data::temperature%]"
},
"data_description": {
"chat_model": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::chat_model%]",
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::max_tokens%]",
"prompt_caching": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::prompt_caching%]",
"temperature": "[%key:component::anthropic::config_subentries::conversation::step::advanced::data_description::temperature%]"
},
@@ -76,6 +75,7 @@
"model": {
"data": {
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data::code_execution%]",
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::model::data::max_tokens%]",
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_budget%]",
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data::thinking_effort%]",
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data::tool_search%]",
@@ -85,6 +85,7 @@
},
"data_description": {
"code_execution": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::code_execution%]",
"max_tokens": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::max_tokens%]",
"thinking_budget": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_budget%]",
"thinking_effort": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::thinking_effort%]",
"tool_search": "[%key:component::anthropic::config_subentries::conversation::step::model::data_description::tool_search%]",
@@ -104,7 +105,8 @@
"entry_type": "Conversation agent",
"error": {
"api_error": "Unable to get model info: {message}",
"model_not_found": "Model not found"
"model_not_found": "Model not found",
"thinking_budget_too_large": "Thinking budget must be less than the Maximum tokens."
},
"initiate_flow": {
"reconfigure": "Reconfigure conversation agent",
@@ -114,13 +116,11 @@
"advanced": {
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "Maximum tokens to return in response",
"prompt_caching": "Caching strategy",
"temperature": "Temperature"
},
"data_description": {
"chat_model": "The model to serve the responses.",
"max_tokens": "Limit the number of response tokens.",
"prompt_caching": "Optimize your API cost and response times based on your usage.",
"temperature": "Control the randomness of the response, trading off between creativity and coherence."
},
@@ -144,6 +144,7 @@
"model": {
"data": {
"code_execution": "Code execution",
"max_tokens": "Maximum tokens to return in response",
"thinking_budget": "Thinking budget",
"thinking_effort": "Thinking effort",
"tool_search": "Enable tool search tool",
@@ -153,6 +154,7 @@
},
"data_description": {
"code_execution": "Allow the model to execute code in a secure sandbox environment, enabling it to analyze data and perform complex calculations.",
"max_tokens": "Limit the number of response tokens.",
"thinking_budget": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking.",
"thinking_effort": "Control how many tokens Claude uses when responding, trading off between response thoroughness and token efficiency",
"tool_search": "Enable dynamic tool discovery instead of preloading all tools into the context",
@@ -203,7 +205,7 @@
"message": "`{file_path}` does not exist."
},
"wrong_file_type": {
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
"message": "The {model} model does not support {mime_type} file types (for `{file_path}`)."
}
},
"issues": {
@@ -217,7 +219,7 @@
"data_description": {
"chat_model": "Select the new model to use."
},
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated and will reach end-of-life on {retirement_date}. Select a supported model to continue.",
"title": "Update model"
}
}
@@ -239,7 +241,8 @@
"low": "[%key:common::state::low%]",
"max": "Max",
"medium": "[%key:common::state::medium%]",
"none": "None"
"none": "None",
"xhigh": "X-High"
}
}
}

View File

@@ -945,7 +945,10 @@ class PipelineRun:
try:
# Transcribe audio stream
stt_vad: VoiceCommandSegmenter | None = None
if self.audio_settings.is_vad_enabled:
if (
self.audio_settings.is_vad_enabled
and self.stt_provider.audio_processing.requires_external_vad
):
stt_vad = VoiceCommandSegmenter(
silence_seconds=self.audio_settings.silence_seconds
)

View File

@@ -7,13 +7,17 @@ from .const import DOMAIN
from .entity import AssistSatelliteState
CONDITIONS: dict[str, type[Condition]] = {
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
"is_idle": make_entity_state_condition(
DOMAIN, AssistSatelliteState.IDLE, support_duration=True
),
"is_listening": make_entity_state_condition(
DOMAIN, AssistSatelliteState.LISTENING, support_duration=True
),
"is_processing": make_entity_state_condition(
DOMAIN, AssistSatelliteState.PROCESSING
DOMAIN, AssistSatelliteState.PROCESSING, support_duration=True
),
"is_responding": make_entity_state_condition(
DOMAIN, AssistSatelliteState.RESPONDING
DOMAIN, AssistSatelliteState.RESPONDING, support_duration=True
),
}

View File

@@ -12,6 +12,11 @@
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:
is_idle: *condition_common
is_listening: *condition_common

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is idle"
@@ -19,6 +23,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is listening"
@@ -28,6 +35,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is processing"
@@ -37,6 +47,9 @@
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
}
},
"name": "Satellite is responding"

View File

@@ -169,6 +169,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"cover",
"device_tracker",
"door",
"doorbell",
"event",
"fan",
"garage_door",

View File

@@ -1,8 +1,12 @@
"""Support for Amazon Web Services (AWS)."""
from __future__ import annotations
import asyncio
from collections import OrderedDict
from dataclasses import dataclass
import logging
from typing import Any
from aiobotocore.session import AioSession
import voluptuous as vol
@@ -30,14 +34,22 @@ from .const import (
CONF_REGION,
CONF_SECRET_ACCESS_KEY,
CONF_VALIDATE,
DATA_CONFIG,
DATA_HASS_CONFIG,
DATA_SESSIONS,
DATA_AWS,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@dataclass
class AWSData:
"""Runtime data for the AWS integration."""
hass_config: ConfigType
config: dict[str, Any]
sessions: OrderedDict[str, AioSession]
AWS_CREDENTIAL_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
@@ -88,14 +100,13 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up AWS component."""
hass.data[DATA_HASS_CONFIG] = config
if (conf := config.get(DOMAIN)) is None:
# create a default conf using default profile
conf = CONFIG_SCHEMA({ATTR_CREDENTIALS: DEFAULT_CREDENTIAL})
hass.data[DATA_CONFIG] = conf
hass.data[DATA_SESSIONS] = OrderedDict()
hass.data[DATA_AWS] = AWSData(
hass_config=config, config=conf, sessions=OrderedDict()
)
hass.async_create_task(
hass.config_entries.flow.async_init(
@@ -111,8 +122,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Validate and save sessions per aws credential.
"""
config = hass.data[DATA_HASS_CONFIG]
conf = hass.data[DATA_CONFIG]
data = hass.data[DATA_AWS]
conf = data.config
if entry.source == config_entries.SOURCE_IMPORT:
if conf is None:
@@ -143,14 +154,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
validation = False
else:
hass.data[DATA_SESSIONS][name] = result
data.sessions[name] = result
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
for notify_config in conf[CONF_NOTIFY]:
hass.async_create_task(
discovery.async_load_platform(
hass, Platform.NOTIFY, DOMAIN, notify_config, config
hass, Platform.NOTIFY, DOMAIN, notify_config, data.hass_config
)
)

View File

@@ -1,10 +1,17 @@
"""Constant for AWS component."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import AWSData
DOMAIN = "aws"
DATA_CONFIG = "aws_config"
DATA_HASS_CONFIG = "aws_hass_config"
DATA_SESSIONS = "aws_sessions"
DATA_AWS: HassKey[AWSData] = HassKey(DOMAIN)
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_CONTEXT = "context"

View File

@@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_SESSIONS
from .const import CONF_CONTEXT, CONF_CREDENTIAL_NAME, CONF_REGION, DATA_AWS
_LOGGER = logging.getLogger(__name__)
@@ -76,10 +76,12 @@ async def async_get_service(
if CONF_CONTEXT in aws_config:
del aws_config[CONF_CONTEXT]
sessions = hass.data[DATA_AWS].sessions
if not aws_config:
# no platform config, use the first aws component credential instead
if hass.data[DATA_SESSIONS]:
session = next(iter(hass.data[DATA_SESSIONS].values()))
if sessions:
session = next(iter(sessions.values()))
else:
_LOGGER.error("Missing aws credential for %s", config[CONF_NAME])
return None
@@ -87,7 +89,7 @@ async def async_get_service(
if session is None:
credential_name = aws_config.get(CONF_CREDENTIAL_NAME)
if credential_name is not None:
session = hass.data[DATA_SESSIONS].get(credential_name)
session = sessions.get(credential_name)
if session is None:
_LOGGER.warning("No available aws session for %s", credential_name)
del aws_config[CONF_CREDENTIAL_NAME]

View File

@@ -5,10 +5,7 @@ from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.backup import DATA_MANAGER as BACKUP_DATA_MANAGER
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
@@ -31,7 +28,7 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backup_manager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],

View File

@@ -34,7 +34,7 @@ def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
def get_serial_number_from_jid(jid: str) -> str:
"""Get serial number from Beolink JID."""
return jid.split(".")[2].split("@")[0]
return jid.split(".")[2].split("@", maxsplit=1)[0]
async def get_remotes(client: MozartClient) -> list[PairedRemote]:

View File

@@ -29,11 +29,17 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
}
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE

View File

@@ -13,6 +13,11 @@
options:
- all
- any
for: &condition_for
required: true
default: 00:00:00
selector:
duration:
.battery_threshold_entity: &battery_threshold_entity
- domain: input_number
@@ -39,6 +44,7 @@ is_charging:
device_class: battery_charging
fields:
behavior: *condition_behavior
for: *condition_for
is_not_charging:
target:
@@ -47,6 +53,7 @@ is_not_charging:
device_class: battery_charging
fields:
behavior: *condition_behavior
for: *condition_for
is_level:
target:

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -12,6 +13,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is charging"
@@ -33,6 +37,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is low"
@@ -42,6 +49,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is not charging"
@@ -51,6 +61,9 @@
"fields": {
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
}
},
"name": "Battery is not low"

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.0"],
"requirements": ["blebox-uniapi==2.5.1"],
"zeroconf": ["_bbxsrv._tcp.local."]
}

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==4.0.4",
"habluetooth==6.0.0"
"habluetooth==6.1.0"
]
}

View File

@@ -7,9 +7,11 @@ from typing import Final
from aiohttp import CookieJar
from pybravia import BraviaClient
from homeassistant.components import ssdp
from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
from .const import CONF_USE_SSL
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
@@ -46,6 +48,19 @@ async def async_setup_entry(
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
async def async_ssdp_callback(
discovery_info: SsdpServiceInfo, change: ssdp.SsdpChange
) -> None:
await coordinator.async_request_refresh()
config_entry.async_on_unload(
await ssdp.async_register_callback(
hass,
async_ssdp_callback,
{"nt": "urn:schemas-upnp-org:device:MediaRenderer:1", "_host": host},
)
)
return True

View File

@@ -173,6 +173,9 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
power_status = await self.client.get_power_status()
self.is_on = power_status == "active"
self.skipped_updates = 0
self.update_interval = (
timedelta(seconds=120) if power_status == "standby" else SCAN_INTERVAL
)
if not self.system_info:
self.system_info = await self.client.get_system_info()

View File

@@ -1,4 +1,5 @@
"""The Broadlink integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -34,6 +34,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink climate entities."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
if device.api.type in DOMAINS_AND_TYPES[Platform.CLIMATE]:

View File

@@ -133,6 +133,8 @@ class BroadlinkDevice[_ApiT: blk.Device = blk.Device]:
await coordinator.async_config_entry_first_refresh()
self.update_manager = update_manager
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
self.hass.data[DOMAIN].devices[config.entry_id] = self
self.reset_jobs.append(config.add_update_listener(self.async_update))

View File

@@ -32,6 +32,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink light."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
lights = []

View File

@@ -95,6 +95,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Broadlink remote."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
remote = BroadlinkRemote(
device,

View File

@@ -31,6 +31,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink select."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkDayOfWeek(device)])

View File

@@ -108,6 +108,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink sensor."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
sensor_data = device.update_manager.coordinator.data
sensors = [

View File

@@ -1,4 +1,5 @@
"""Support for Broadlink switches."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -22,6 +22,8 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Broadlink time."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkTime(device)])

View File

@@ -738,10 +738,7 @@ class CalendarEntity(Entity):
listener(None)
return
event_list: list[JsonValueType] = [
dataclasses.asdict(event, dict_factory=_list_events_dict_factory)
for event in events
]
event_list: list[JsonValueType] = [event.as_dict() for event in events]
listener(event_list)
async def async_get_events(

View File

@@ -7,7 +7,9 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
"is_event_active": make_entity_state_condition(
DOMAIN, STATE_ON, support_duration=True
),
}

View File

@@ -12,3 +12,8 @@ is_event_active:
options:
- all
- any
for:
required: true
default: 00:00:00
selector:
duration:

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if"
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least"
},
"conditions": {
"is_event_active": {
@@ -8,6 +9,9 @@
"fields": {
"behavior": {
"name": "[%key:component::calendar::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::calendar::common::condition_for_name%]"
}
},
"name": "Calendar event is active"

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==2.2.0"]
"requirements": ["PyTurboJPEG==1.8.3"]
}

View File

@@ -1,4 +1,5 @@
"""Component to embed Google Cast."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -65,6 +65,8 @@ class ChromecastInfo:
"""
cast_info = self.cast_info
if self.cast_info.cast_type is None or self.cast_info.manufacturer is None:
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
unknown_models = hass.data[DOMAIN]["unknown_models"]
if self.cast_info.model_name not in unknown_models:
# Manufacturer and cast type is not available in mDNS data,

View File

@@ -1,4 +1,5 @@
"""Provide functionality to interact with Cast devices on the network."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -50,7 +50,9 @@ ATTR_UID = "uid"
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_FREE_EBIKES = "free_ebikes"
ATTR_TIMESTAMP = "timestamp"
EXTRA_EBIKES = "ebikes"
CONF_NETWORK = "network"
CONF_STATIONS_LIST = "stations"
@@ -238,5 +240,6 @@ class CityBikesStation(SensorEntity):
ATTR_LATITUDE: station.latitude,
ATTR_LONGITUDE: station.longitude,
ATTR_EMPTY_SLOTS: station.empty_slots,
ATTR_FREE_EBIKES: station.extra.get(EXTRA_EBIKES),
ATTR_TIMESTAMP: station.timestamp,
}

View File

@@ -67,7 +67,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_hvac_mode": ClimateHVACModeCondition,
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF, support_duration=True),
"is_on": make_entity_state_condition(
DOMAIN,
{

View File

@@ -39,7 +39,16 @@
- domain: number
device_class: temperature
is_off: *condition_common
is_off:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_on: *condition_common
is_cooling: *condition_common
is_drying: *condition_common

View File

@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -8,34 +9,34 @@
},
"conditions": {
"is_cooling": {
"description": "Tests if one or more climate-control devices are cooling.",
"description": "Tests if one or more thermostats are cooling.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Climate-control device is cooling"
"name": "Thermostat is cooling"
},
"is_drying": {
"description": "Tests if one or more climate-control devices are drying.",
"description": "Tests if one or more thermostats are drying.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Climate-control device is drying"
"name": "Thermostat is drying"
},
"is_heating": {
"description": "Tests if one or more climate-control devices are heating.",
"description": "Tests if one or more thermostats are heating.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Climate-control device is heating"
"name": "Thermostat is heating"
},
"is_hvac_mode": {
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
"description": "Tests if one or more thermostats are set to a specific HVAC mode.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -45,28 +46,31 @@
"name": "Modes"
}
},
"name": "Climate-control device HVAC mode"
"name": "Thermostat HVAC mode"
},
"is_off": {
"description": "Tests if one or more climate-control devices are off.",
"description": "Tests if one or more thermostats are off.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Climate-control device is off"
"name": "Thermostat is off"
},
"is_on": {
"description": "Tests if one or more climate-control devices are on.",
"description": "Tests if one or more thermostats are on.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
}
},
"name": "Climate-control device is on"
"name": "Thermostat is on"
},
"target_humidity": {
"description": "Tests the humidity setpoint of one or more climate-control devices.",
"description": "Tests the humidity setpoint of one or more thermostats.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -75,10 +79,10 @@
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Climate-control device target humidity"
"name": "Thermostat target humidity"
},
"target_temperature": {
"description": "Tests the temperature setpoint of one or more climate-control devices.",
"description": "Tests the temperature setpoint of one or more thermostats.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
@@ -87,7 +91,7 @@
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
},
"name": "Climate-control device target temperature"
"name": "Thermostat target temperature"
}
},
"device_automation": {
@@ -284,67 +288,67 @@
},
"services": {
"set_fan_mode": {
"description": "Sets the fan mode of a climate-control device.",
"description": "Sets the fan mode of a thermostat.",
"fields": {
"fan_mode": {
"description": "Fan operation mode.",
"name": "Fan mode"
}
},
"name": "Set climate-control device fan mode"
"name": "Set thermostat fan mode"
},
"set_humidity": {
"description": "Sets the target humidity of a climate-control device.",
"description": "Sets the target humidity of a thermostat.",
"fields": {
"humidity": {
"description": "Target humidity.",
"name": "Humidity"
}
},
"name": "Set climate-control device target humidity"
"name": "Set thermostat target humidity"
},
"set_hvac_mode": {
"description": "Sets the HVAC mode of a climate-control device.",
"description": "Sets the HVAC mode of a thermostat.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
"name": "HVAC mode"
}
},
"name": "Set climate-control device HVAC mode"
"name": "Set thermostat HVAC mode"
},
"set_preset_mode": {
"description": "Sets the preset mode of a climate-control device.",
"description": "Sets the preset mode of a thermostat.",
"fields": {
"preset_mode": {
"description": "Preset mode.",
"name": "Preset mode"
}
},
"name": "Set climate-control device preset mode"
"name": "Set thermostat preset mode"
},
"set_swing_horizontal_mode": {
"description": "Sets the horizontal swing mode of a climate-control device.",
"description": "Sets the horizontal swing mode of a thermostat.",
"fields": {
"swing_horizontal_mode": {
"description": "Horizontal swing operation mode.",
"name": "Horizontal swing mode"
}
},
"name": "Set climate-control device horizontal swing mode"
"name": "Set thermostat horizontal swing mode"
},
"set_swing_mode": {
"description": "Sets the swing mode of a climate-control device.",
"description": "Sets the swing mode of a thermostat.",
"fields": {
"swing_mode": {
"description": "Swing operation mode.",
"name": "Swing mode"
}
},
"name": "Set climate-control device swing mode"
"name": "Set thermostat swing mode"
},
"set_temperature": {
"description": "Sets the target temperature of a climate-control device.",
"description": "Sets the target temperature of a thermostat.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
@@ -363,25 +367,25 @@
"name": "Target temperature"
}
},
"name": "Set climate-control device target temperature"
"name": "Set thermostat target temperature"
},
"toggle": {
"description": "Toggles a climate-control device on/off.",
"name": "Toggle climate-control device"
"description": "Toggles a thermostat on/off.",
"name": "Toggle thermostat"
},
"turn_off": {
"description": "Turns off a climate-control device.",
"name": "Turn off climate-control device"
"description": "Turns off a thermostat.",
"name": "Turn off thermostat"
},
"turn_on": {
"description": "Turns on a climate-control device.",
"name": "Turn on climate-control device"
"description": "Turns on a thermostat.",
"name": "Turn on thermostat"
}
},
"title": "Climate",
"triggers": {
"hvac_mode_changed": {
"description": "Triggers after the mode of one or more climate-control devices changes.",
"description": "Triggers after the mode of one or more thermostats changes.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -394,10 +398,10 @@
"name": "Modes"
}
},
"name": "Climate-control device mode changed"
"name": "Thermostat mode changed"
},
"started_cooling": {
"description": "Triggers after one or more climate-control devices start cooling.",
"description": "Triggers after one or more thermostats start cooling.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -406,10 +410,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started cooling"
"name": "Thermostat started cooling"
},
"started_drying": {
"description": "Triggers after one or more climate-control devices start drying.",
"description": "Triggers after one or more thermostats start drying.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -418,10 +422,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started drying"
"name": "Thermostat started drying"
},
"started_heating": {
"description": "Triggers after one or more climate-control devices start heating.",
"description": "Triggers after one or more thermostats start heating.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -430,19 +434,19 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device started heating"
"name": "Thermostat started heating"
},
"target_humidity_changed": {
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
"description": "Triggers after the humidity setpoint of one or more thermostats changes.",
"fields": {
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Climate-control device target humidity changed"
"name": "Thermostat target humidity changed"
},
"target_humidity_crossed_threshold": {
"description": "Triggers after the humidity setpoint of one or more climate-control devices crosses a threshold.",
"description": "Triggers after the humidity setpoint of one or more thermostats crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -454,19 +458,19 @@
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Climate-control device target humidity crossed threshold"
"name": "Thermostat target humidity crossed threshold"
},
"target_temperature_changed": {
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
"description": "Triggers after the temperature setpoint of one or more thermostats changes.",
"fields": {
"threshold": {
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Climate-control device target temperature changed"
"name": "Thermostat target temperature changed"
},
"target_temperature_crossed_threshold": {
"description": "Triggers after the temperature setpoint of one or more climate-control devices crosses a threshold.",
"description": "Triggers after the temperature setpoint of one or more thermostats crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -478,10 +482,10 @@
"name": "[%key:component::climate::common::trigger_threshold_name%]"
}
},
"name": "Climate-control device target temperature crossed threshold"
"name": "Thermostat target temperature crossed threshold"
},
"turned_off": {
"description": "Triggers after one or more climate-control devices turn off.",
"description": "Triggers after one or more thermostats turn off.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -490,10 +494,10 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device turned off"
"name": "Thermostat turned off"
},
"turned_on": {
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
"description": "Triggers after one or more thermostats turn on, regardless of the mode.",
"fields": {
"behavior": {
"name": "[%key:component::climate::common::trigger_behavior_name%]"
@@ -502,7 +506,7 @@
"name": "[%key:component::climate::common::trigger_for_name%]"
}
},
"name": "Climate-control device turned on"
"name": "Thermostat turned on"
}
}
}

View File

@@ -169,6 +169,8 @@ class OptionsFlowHandler(OptionsFlowWithReload):
data_schema = vol.Schema(
{
# Polling interval is user-configurable, which is no longer allowed
# pylint: disable-next=hass-config-flow-polling-field
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
SerialSelector,
SerialPortSelector,
)
from .const import DOMAIN, LOGGER
@@ -110,7 +110,7 @@ class DenonRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
translation_key="model",
)
),
vol.Required(CONF_DEVICE): SerialSelector(),
vol.Required(CONF_DEVICE): SerialPortSelector(),
}
),
user_input or {},

View File

@@ -6,6 +6,7 @@ import asyncio
from collections.abc import Callable, Coroutine, Sequence
from datetime import datetime, timedelta
import hashlib
import logging
from types import ModuleType
from typing import Any, Final, Protocol, final
@@ -82,6 +83,8 @@ from .const import (
SourceType,
)
_LOGGER = logging.getLogger(__name__)
SERVICE_SEE: Final = "see"
SOURCE_TYPES = [cls.value for cls in SourceType]
@@ -128,6 +131,8 @@ SERVICE_SEE_PAYLOAD_SCHEMA: Final[vol.Schema] = vol.Schema(
YAML_DEVICES: Final = "known_devices.yaml"
EVENT_NEW_DEVICE: Final = "device_tracker_new_device"
DATA_LEGACY_TRACKERS: Final = "device_tracker.legacy_trackers"
class SeeCallback(Protocol):
"""Protocol type for DeviceTracker.see callback."""
@@ -243,8 +248,19 @@ async def _async_setup_integration(
tracker = await get_tracker(hass, config)
tracker_future.set_result(tracker)
warned_called_see = False
async def async_see_service(call: ServiceCall) -> None:
"""Service to see a device."""
nonlocal warned_called_see
if not warned_called_see:
_LOGGER.warning(
"The %s.%s action is deprecated and will be removed in "
"Home Assistant Core 2027.5",
DOMAIN,
SERVICE_SEE,
)
warned_called_see = True
# Temp workaround for iOS, introduced in 0.65
data = dict(call.data)
data.pop("hostname", None)
@@ -327,6 +343,18 @@ class DeviceTrackerPlatform:
try:
scanner = None
setup: bool | None = None
legacy_trackers = hass.data.setdefault(DATA_LEGACY_TRACKERS, set())
if full_name not in legacy_trackers:
legacy_trackers.add(full_name)
_LOGGER.warning(
"The legacy device tracker platform %s is being set up; legacy "
"device trackers are deprecated and will be removed in Home "
"Assistant Core 2027.5, please migrate to an integration which "
"uses a modern config entry based device tracker",
full_name,
)
if hasattr(self.platform, "async_get_scanner"):
scanner = await self.platform.async_get_scanner(
hass, {DOMAIN: self.config}

View File

@@ -1,4 +1,5 @@
"""Data used by this integration."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -1,4 +1,5 @@
"""Wrapper for media_source around async_upnp_client's DmsDevice ."""
# pylint: disable=hass-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from __future__ import annotations

View File

@@ -0,0 +1,15 @@
"""Integration for doorbell triggers."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
DOMAIN = "doorbell"
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
__all__ = []
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
return True

View File

@@ -0,0 +1,7 @@
{
"triggers": {
"rang": {
"trigger": "mdi:doorbell"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"domain": "doorbell",
"name": "Doorbell",
"codeowners": ["@home-assistant/core"],
"documentation": "https://www.home-assistant.io/integrations/doorbell",
"integration_type": "system",
"quality_scale": "internal"
}

View File

@@ -0,0 +1,9 @@
{
"title": "Doorbell",
"triggers": {
"rang": {
"description": "Triggers after one or more doorbells rang.",
"name": "Doorbell rang"
}
}
}

View File

@@ -0,0 +1,50 @@
"""Provides triggers for doorbells."""
from homeassistant.components.event import (
ATTR_EVENT_TYPE,
DOMAIN as EVENT_DOMAIN,
DoorbellEventType,
EventDeviceClass,
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
)
class DoorbellRangTrigger(EntityTriggerBase):
"""Trigger for doorbell event entity when a ring event is received."""
_domain_specs = {EVENT_DOMAIN: DomainSpec(device_class=EventDeviceClass.DOORBELL)}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_state(self, state: State) -> bool:
"""Check if the entity is available and the event type is ring."""
return (
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
and state.attributes.get(ATTR_EVENT_TYPE) == DoorbellEventType.RING
)
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and different from the current state."""
# UNKNOWN is a valid from_state, otherwise the first time the event is received
# would not trigger
if from_state.state == STATE_UNAVAILABLE:
return False
return from_state.state != to_state.state
TRIGGERS: dict[str, type[Trigger]] = {
"rang": DoorbellRangTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for doorbells."""
return TRIGGERS

View File

@@ -0,0 +1,5 @@
rang:
target:
entity:
domain: event
device_class: doorbell

View File

@@ -13,6 +13,8 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN
@@ -31,6 +33,99 @@ class DucoConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1
_host: str
_box_name: str
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
await self.async_set_unique_id(format_mac(discovery_info.macaddress))
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
try:
box_name, _ = await self._validate_input(discovery_info.ip)
except DucoConnectionError:
return self.async_abort(reason="cannot_connect")
except DucoError:
_LOGGER.exception("Unexpected error discovering Duco box via DHCP")
return self.async_abort(reason="unknown")
self._host = discovery_info.ip
self._box_name = box_name
self.context["title_placeholders"] = {"name": box_name}
return await self.async_step_discovery_confirm()
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
try:
box_name, mac = await self._validate_input(discovery_info.host)
except DucoConnectionError:
return self.async_abort(reason="cannot_connect")
except DucoError:
_LOGGER.exception("Unexpected error discovering Duco box via zeroconf")
return self.async_abort(reason="unknown")
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
self._host = discovery_info.host
self._box_name = box_name
self.context["title_placeholders"] = {"name": box_name}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
return self.async_create_entry(
title=self._box_name,
data={CONF_HOST: self._host},
)
self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={"name": self._box_name},
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
try:
box_name, mac = await self._validate_input(user_input[CONF_HOST])
except DucoConnectionError:
errors["base"] = "cannot_connect"
except DucoError:
_LOGGER.exception("Unexpected error connecting to Duco box")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfigure_entry,
title=box_name,
data_updates={CONF_HOST: user_input[CONF_HOST]},
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_SCHEMA, reconfigure_entry.data
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -46,7 +141,7 @@ class DucoConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected error connecting to Duco box")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(format_mac(mac))
await self.async_set_unique_id(format_mac(mac), raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
from duco.exceptions import DucoError
import logging
from duco.exceptions import DucoError, DucoRateLimitError
from duco.models import Node, NodeType, VentilationState
from homeassistant.components.fan import FanEntity, FanEntityFeature
@@ -15,6 +17,8 @@ from .const import DOMAIN
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
# Permanent speed states ordered low → high.
@@ -60,6 +64,7 @@ async def async_setup_entry(
"""Set up Duco fan entities."""
coordinator = entry.runtime_data
# BOX is always node 1 and is never dynamically added or removed, so no listener needed.
async_add_entities(
DucoVentilationFanEntity(coordinator, node)
for node in coordinator.data.nodes.values()
@@ -118,6 +123,12 @@ class DucoVentilationFanEntity(DucoEntity, FanEntity):
await self.coordinator.client.async_set_ventilation_state(
self._node_id, state
)
except DucoRateLimitError as err:
_LOGGER.warning("Duco write rate limit exceeded for node %s", self._node_id)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="rate_limit_exceeded",
) from err
except DucoError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,

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