Compare commits

..

319 Commits

Author SHA1 Message Date
epenet
55643f0632 Remove async_setup/async_setup_entry/async_unload_entry from __all__ (#154674) 2025-10-17 16:37:16 +02:00
Magnus
36f4723f6e Component asuswrt: Improve get_bridge parameters typing in asuswrt (#154540) 2025-10-17 16:00:56 +02:00
Alistair Francis
03bc698936 husqvarna_automower_ble: Log errors if the mower isn't pairable (#151768)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2025-10-17 15:49:53 +02:00
Manu
0c1dc73422 Add snapshot tests of sensor platform to Xbox integration (#154684) 2025-10-17 15:46:51 +02:00
Anuj Soni
c31537081b Move translatable URLs out of strings.json for tautulli (#154681) 2025-10-17 15:35:05 +02:00
epenet
d13067abb3 Remove rest from _IGNORE_ROOT_IMPORT in pylint plugin (#154662) 2025-10-17 09:21:53 -04:00
epenet
64da32b5f9 Revert "Adding __all__ export to device_tracker" (#154675) 2025-10-17 09:20:18 -04:00
Paulus Schoutsen
3990fc6ab2 LLM: skip local handling of search media query (#154496) 2025-10-17 09:18:47 -04:00
Alistair Francis
e4071bd305 Bump automower-ble to 0.2.8 (#154683)
Signed-off-by: Alistair Francis <alistair@alistair23.me>
2025-10-17 15:08:11 +02:00
Magnus
8dda26c227 Component asuswrt: Type hint for aioasuswrt returns (#154594) 2025-10-17 15:04:36 +02:00
johanzander
b182d5ce87 Add additional unit tests for Growatt Server integration (#154644) 2025-10-17 14:22:16 +02:00
Thomas55555
175365bdea Add integration_type to Husqvarna Automower (#154642) 2025-10-17 14:18:32 +02:00
Bouwe Westerdijk
cbe52cbfca Bump plugwise to v1.8.1 (#154679) 2025-10-17 15:13:35 +03:00
Felipe Santos
9251dde2c6 Add OpenRGB reconfiguration flow (#154478) 2025-10-17 12:27:11 +02:00
Andrew Jackson
24d77cc453 Bump aiomealie to 1.0.1 (#154672) 2025-10-17 12:23:55 +03:00
johanzander
a1f98abe49 Add CODEOWNERS entry for Growatt Server integration (#154647) 2025-10-17 11:20:11 +03:00
cdnninja
d25dde1d11 Bump pyvesync version to 3.1.2 (#154650)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-17 10:19:48 +02:00
hanwg
8ec483b38b Fix Telegram bot bug where message is sent to wrong recipient (#154658) 2025-10-17 11:15:41 +03:00
epenet
bf14caca69 Fix behavior spelling for public facing strings (#154665) 2025-10-17 11:07:05 +03:00
Ludovic BOUÉ
e5fb6b2fb2 Remove duplicated Matter powersource cluster from Mock device fixture files (#154668) 2025-10-17 11:06:01 +03:00
epenet
7dfeb3a3f6 Improve metoffice typing (#154670) 2025-10-17 10:05:27 +02:00
epenet
9d3b1562c4 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154667) 2025-10-17 09:46:53 +02:00
epenet
e14407f066 Remove HomeAssistantRemoteScanner from __all__ in bluetooth (#154669) 2025-10-17 09:31:30 +02:00
epenet
67872e3746 Adjust onewire strings (#154664) 2025-10-17 09:28:37 +02:00
Manu
06bd1a2003 Migrate Xbox to runtime_data (#154652) 2025-10-17 09:25:49 +02:00
dependabot[bot]
37ea360304 Bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#154661)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 09:15:41 +02:00
epenet
25ce57424c Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154660) 2025-10-17 08:35:18 +02:00
Thomas55555
3d46ab549d Add serial number to IPP (#154648) 2025-10-16 23:58:57 +01:00
Thomas55555
567cc9f842 Bump colorlog to 6.10.1 (#154643) 2025-10-16 23:57:24 +01:00
Shay Levy
b5457a5abd Fix demo cover set position action (#154641) 2025-10-16 21:21:32 +03:00
Marc Mueller
e4b5e35d1d Update Pillow to 12.0.0 (#154637) 2025-10-16 18:25:36 +01:00
Ludovic BOUÉ
12023c33b5 Rename Mock Door Lock with unbolt fixture (#154627) 2025-10-16 13:01:46 -04:00
Jan Čermák
a28749937c Allow ignored rapt_ble devices to be set up from the user flow (#154606) 2025-10-16 12:54:24 -04:00
Jan Čermák
3fe37d651f Update Home Assistant base image to 2025.10.1 (#154609)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 12:53:25 -04:00
epenet
cb3424cdf0 Remove more components from _IGNORE_ROOT_IMPORT in pylint plugin (#154622) 2025-10-16 12:52:51 -04:00
Thomas D
a799f7ff91 Add service warning sensor to Volvo integration (#154613) 2025-10-16 18:52:12 +02:00
Louis Pré
34ab725b75 LLM prefix caching optimization using new GetDateTime tool (#152408)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
2025-10-16 12:47:12 -04:00
Manu
2dfc7f02ba Bump habiticalib to v0.4.6 (#154566) 2025-10-16 17:15:13 +01:00
Jan Čermák
c8919222bd Mock network calls in comfoconnect tests to fix timeouts (#154620) 2025-10-16 11:42:04 -04:00
Ludovic BOUÉ
a888264d2f Add Matter fixture for Aqara Smart Lock U200 (#154623) 2025-10-16 16:25:16 +02:00
Joost Lekkerkerker
ae84c7e15d Add subentries to WAQI (#148966)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 14:11:52 +01:00
epenet
415c8b490b Add device diagnostics to onewire (#154617)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:56:19 +02:00
Aviad Levy
6038f15406 Add support for Telegram message attachments (#153216) 2025-10-16 14:54:50 +02:00
Justus
a8758253c4 Add config flow exceptions to IOMeter (#154604)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 14:52:51 +02:00
epenet
fa4eb2e820 The 1-wire integration has now reached silver on the quality scale (#154614) 2025-10-16 14:52:11 +02:00
Ludovic BOUÉ
58f35d0614 Add Matter Eve Energy 20ECN4101 fixture (#154608) 2025-10-16 14:07:29 +02:00
epenet
f72a91ca29 Remove assist_pipeline from _IGNORE_ROOT_IMPORT in pylint plugin (#154600) 2025-10-16 13:33:19 +02:00
Thomas D
5d99da6e1f The Volvo integration has now reached platinum on the quality scale (#154015)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-16 13:07:54 +02:00
Joost Lekkerkerker
64746eb99c Add new Dryer fixture to SmartThings (#154607) 2025-10-16 12:55:30 +02:00
Maciej Bieniek
70fc6df599 Make Shelly deprecated firmware issue more general (#154539) 2025-10-16 13:50:43 +03:00
epenet
8dc33ece7b Remove sensor from _IGNORE_ROOT_IMPORT in pylint plugin (#154602) 2025-10-16 11:28:29 +01:00
Carlos Gustavo Sarmiento
3d4d8e7f20 Make Speed optional for GoToPreset ONVIF command (#149636)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-16 11:21:02 +01:00
Joakim Sørensen
c92d319e12 Bump hass-nabucasa from 1.3.0 to 1.4.0 (#154599) 2025-10-16 11:18:55 +01:00
Christopher Fenner
1bdba7906a Add new sensors for Zigbee based devices in ViCare (#154271) 2025-10-16 11:11:08 +01:00
epenet
aa8198d852 Bump epson-projector to 0.6.0 (#154596) 2025-10-16 12:06:30 +02:00
Ashus
b7f30ec17f Fix friendly names of zones with mobile_app (#149453)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-16 11:27:59 +02:00
Ludovic BOUÉ
2da1878f60 Add Matter Inovelli VTM30 fixture (#154601) 2025-10-16 11:20:29 +02:00
tstabrawa
872b33a088 Move URL out of Nuheat strings.json (#154580) 2025-10-16 10:14:22 +02:00
epenet
e0faa36157 Bump pymonoprice to 0.5 (#146936) 2025-10-16 10:07:52 +02:00
Magnus
14b270a2db Component asuswrt: handle_errors_and_zip._wrapper returns dict[str, str] (#154544) 2025-10-16 09:48:21 +02:00
Magnus
8402bead4f Component asuswrt: import of ConnectionState corrected (#154518) 2025-10-16 09:38:09 +02:00
Erik Montnemery
6bf7a4278e Fix flaky playstation_network test (#154559)
Co-authored-by: Joakim Plate <elupus@ecce.se>
2025-10-16 09:35:53 +02:00
Erik Montnemery
3de62b2b4c Improve mobile_app device_tracker tests (#154584)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-10-16 09:15:35 +02:00
Kinachi249
0d2558c030 Implement cync reauth flow (#154257) 2025-10-16 08:48:31 +02:00
Jordan Harvey
9efbcb2f82 Add model information for probe_plus devices (#154262) 2025-10-16 08:46:29 +02:00
Keith Burzinski
f210bb35ed Bump aioesphomeapi to 42.0.0 (#154577) 2025-10-16 00:21:15 -05:00
Grzegorz M
0581ceb771 Add ability for CalDAV to create calendar events (#150030) 2025-10-15 20:07:31 -07:00
J. Diego Rodríguez Royo
7ba2e60af3 Bump aiohomeconnect to version 0.22.0 (#154572) 2025-10-16 00:10:54 +01:00
epenet
75fa0ffd04 Update onewire quality scale (#154515) 2025-10-16 00:51:14 +03:00
epenet
01effb7ca6 Remove hardware from _IGNORE_ROOT_IMPORT in pylint plugin (#154532)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-15 21:59:40 +01:00
Jan Bouwhuis
88d383962c Fix lingering todoist test by fixing its test time (#154511) 2025-10-15 22:17:21 +02:00
Paulus Schoutsen
3c001bd6ed Revert "Expose the entity_id of an entity to LLMs" (#154561)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-10-15 16:07:34 -04:00
Marc Mueller
ec5c4843d1 Fix typing issue in fritz (#154497) 2025-10-15 21:27:50 +02:00
Omer Korner
e2c281549e Expose the entity_id of an entity to LLMs (#149428) 2025-10-15 21:24:32 +02:00
epenet
051e472537 Import device_tracker classes from component root (#154524) 2025-10-15 20:57:38 +02:00
Marc Mueller
1e5910215d Update pylint to 4.0.1 (#154526) 2025-10-15 20:54:15 +02:00
epenet
645089edba Bump aio-ownet to 0.0.4 (#154520)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-15 20:52:58 +02:00
Marc Mueller
7abe289681 Add support for Python 3.14 (#153939) 2025-10-15 20:50:16 +02:00
Maciej Bieniek
7829c2d03e Align Shelly entity names with device classes (#154492) 2025-10-15 20:47:47 +02:00
Erwin Douna
148a13361f Firefly refactor entities (#153292) 2025-10-15 20:33:38 +02:00
epenet
57dccd1474 Remove zha from _IGNORE_ROOT_IMPORT in pylint plugin (#154534) 2025-10-15 20:12:11 +02:00
Aarni Koskela
a3b0132299 Move template-rendering test helpers to separate module (#154366) 2025-10-15 20:11:19 +02:00
epenet
fbd8443745 Simplify onewire entity descriptions (#154513) 2025-10-15 20:09:51 +02:00
Manu
cd7015c6b7 Add integration type device to IronOS manifest (#154533) 2025-10-15 20:00:46 +02:00
Joakim Plate
1012c7bdf9 Ensure psn wait more than coordinator tick (#154549) 2025-10-15 19:54:53 +02:00
Markus Adrario
ca912906f5 Automatically removing stale devices in Homee (#152680)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-15 18:36:27 +01:00
karwosts
d0cad43a6c Recalculate derivative unit correctly when source or options change (#147527)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-15 18:17:41 +02:00
Magnus
751540e606 Component asuswrt: Update SENSORS_DEFAULT in tests (#154547) 2025-10-15 17:53:22 +02:00
hanwg
3d2ec712f1 Raise exceptions for Telegram bot when actions fail (#148140) 2025-10-15 17:49:57 +02:00
Noah Husby
e3a6c06997 Bump aiorussound to 4.9.0 (#154545) 2025-10-15 16:14:38 +01:00
Jordan Harvey
08b94e29e6 Bump pynintendoparental to 1.1.2 (#154527) 2025-10-15 16:08:06 +01:00
Simone Chemelli
79323189fb Bump aioamazondevices to 6.4.4 (#154538) 2025-10-15 18:02:56 +03:00
Magnus
7508828518 Adding __all__ export to device_tracker (#154525) 2025-10-15 15:11:16 +01:00
Magnus
f257e89b2a Adjust import of ATTR_GPS_ACCURACY in device_tracker tests (#154531) 2025-10-15 13:55:10 +02:00
Magnus
a2e469eb28 Adjust import of ATTR_GPS_ACCURACY in mobile_app.webhook (#154529) 2025-10-15 13:52:30 +02:00
Magnus
7c80491325 Adjust import of ATTR_GPS_ACCURACY in mobile_app (#154528) 2025-10-15 13:50:49 +02:00
J. Nick Koston
adedf2037a Fix improv_ble provisioning futures type (#154530) 2025-10-15 13:46:23 +02:00
G Johansson
188459e3ff Allow use of Selector in ObjectSelector fields (#147929) 2025-10-15 13:25:04 +02:00
Luke Lashley
7324a12ada Add suggested units for Roborock Durations sensors (#153607)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-15 12:05:46 +01:00
Jan Bouwhuis
fe07e9c840 Move out MQTT translation strings (#154406)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-15 11:25:21 +01:00
krahabb
afeaf2409f Add TEMPERATURE_DELTA device class to Number and Sensor entities (#147358) 2025-10-15 11:49:12 +02:00
Magnus
69f9c0a6cc Typing for WrtDevice (#154514) 2025-10-15 11:08:54 +02:00
epenet
46f52db87c Mark tempres configuration as disabled by default in onewire (#154517) 2025-10-15 11:00:49 +02:00
epenet
d877761dbb Cleanup model/model_id in onewire (#154509) 2025-10-15 10:52:57 +02:00
Fabian Weisshaar
95da65f552 System Bridge to set unavailable entry state if host is not reachable (#154177) 2025-10-15 10:43:42 +02:00
Jan Bouwhuis
6ec82d0b21 Fix MQTT siren subentry translation string (#154483) 2025-10-15 10:36:55 +02:00
Foscam-wangzhengyu
f6a16f63a4 Bump libpyfoscamcgi to 0.0.8 (#154505) 2025-10-15 10:36:20 +02:00
wollew
9ff2dab468 set integration type for velux to hub (#154510) 2025-10-15 10:35:03 +02:00
epenet
9422703288 Add support for DS2401 (#154506) 2025-10-15 08:47:28 +02:00
Anuj Soni
d91eccb209 Move translatable URLs out of strings.json for vera (#154475) 2025-10-15 07:51:52 +02:00
J. Nick Koston
939cbc8644 Bump uiprotect to 7.22.0 (#154494) 2025-10-15 01:07:27 +02:00
J. Nick Koston
0f1d2a77cb Add flow chaining from Improv BLE to integration config flows (#154415)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 12:00:55 -10:00
J. Nick Koston
385fc5b3d0 Add next_flow parameter to async_abort for flow chaining (#154416)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 11:45:21 -10:00
Felipe Santos
18c63e3b8f Introduce the OpenRGB integration (#153373)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-14 19:03:58 +02:00
Paul Bottein
cf477186aa Set assumed state to group if at least one child has assumed state (#154163) 2025-10-14 18:53:51 +02:00
Jan Bouwhuis
0eef44be91 Fix inconsistent use of StrEnum as index in MQTT subentry flow globals (#154210) 2025-10-14 18:47:20 +02:00
Denis Shulyaka
e7ac56c59f Revisit list of OpenAI models for tool support (#154399)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-14 18:37:18 +02:00
Tom
3cc4091f31 Update airOS IQS (completing silver) (#153675) 2025-10-14 18:36:55 +02:00
Anuj Soni
00025c8f42 Move translatable URLs out of strings.json for isy994 (#154464) 2025-10-14 18:31:01 +02:00
Ludovic BOUÉ
db48f8cb28 Add Matter Zemismart Roller Motor fixture (#154458)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-10-14 18:19:25 +02:00
TheJulianJES
4fdbe82df2 Bump pydantic to 2.12.2 (#154461) 2025-10-14 17:46:41 +02:00
Lennart Coopmans
742f1b2157 PushSafer: Handle empty data section properly (#154109) 2025-10-14 17:29:34 +02:00
Sebastian Schneider
681eb6b594 Add LED control for supported UniFi network devices (#152649) 2025-10-14 17:20:47 +02:00
epenet
1d6c6628f4 Migrate onewire to async library (#154439) 2025-10-14 17:18:25 +02:00
Marc Mueller
b6337c07d6 Update intellifire4py to 4.2.1 (#154454) 2025-10-14 16:52:12 +02:00
Jan Bouwhuis
8b6fb05ee4 Add subentry support for MQTT siren device (#154220) 2025-10-14 16:45:48 +02:00
MoonDevLT
28405e2b04 Add model name to Lunatone devices (#154432) 2025-10-14 16:40:48 +02:00
ollo69
31857a03d6 Remove Asuwrt device tracker last_time_reachable extra attribute (#154219) 2025-10-14 16:35:48 +02:00
Kelyan PEGEOT SELME
97a0a4ea17 Add tyre pressure to Renault integration (#154377) 2025-10-14 16:33:43 +02:00
Abílio Costa
b494074ee0 Fix device registry arg docstring (#154453) 2025-10-14 15:32:31 +01:00
Manu
6aff1287dd Fix capitalization of RADIUS in Uptime Kuma (#154456) 2025-10-14 17:29:50 +03:00
puddly
655de3dfd2 Use async_schedule_reload instead of async_reload for ZHA (#154397) 2025-10-14 16:26:40 +02:00
cdnninja
11ee7d63be Remove vesync unused extra attributes, refine enums (#153171) 2025-10-14 16:23:29 +02:00
Simone Chemelli
080a7dcfa7 Allow more device types for Vodafone Station (#153990) 2025-10-14 16:18:16 +02:00
Sid
3e20c506f4 Add gallons per hour as volume flow rate unit (#154246)
Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
2025-10-14 16:16:48 +02:00
Abílio Costa
2abc197dcd Add extract_from_target websocket command (#150124)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-10-14 16:16:00 +02:00
karwosts
a3dec46d59 Add derivative tests exhibiting unit issues (#153051) 2025-10-14 15:58:14 +02:00
Samuel Xiao
7a3630e647 Add sensor description for switchbot cloud's device(plug) small changes (#148551)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-14 15:35:11 +02:00
Heindrich Paul
2812d7c712 Add the coordinator pattern to the NS integration (#154149)
Signed-off-by: Heindrich Paul <heindrich.paul@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-14 15:21:09 +02:00
Anuj Soni
c0fc7b66f0 Move translatable URLs out of strings.json for huawei lte (#154368)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-14 15:11:47 +02:00
David Recordon
c6e334ca60 Skip adding Control4 rooms with no audio/video sources as media player devices (#154348)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-14 15:09:04 +02:00
Tom
416f6b922c Add reconfigure flow to airOS (#154447) 2025-10-14 15:05:10 +02:00
DannyS95
d2af875d63 Move igloohome API access URL into constant placeholders (#154430) 2025-10-14 15:01:39 +02:00
Mateusz
1237010b4a auth: add required issuer to OAuth (#152385) 2025-10-14 14:50:38 +02:00
Jan-Philipp Benecke
26fec2fdcc Move Electricity Maps url out of strings.json (#154284) 2025-10-14 14:50:28 +02:00
Oliver Gründel
13e828038d Move developer url out of strings.json for coinbase setup flow (#154339) 2025-10-14 14:50:12 +02:00
Oliver Gründel
b517774be0 Move Ecobee authorization URL out of strings.json (#154332) 2025-10-14 14:49:45 +02:00
Andrew Jackson
6e515d4829 Move URL out of Mealie strings.json (#154230) 2025-10-14 14:48:36 +02:00
Manu
7f5128eb15 Add description placeholders to pyLoad config flow (#154254) 2025-10-14 14:48:11 +02:00
Shai Ungar
7ddfcd350b Move URLs out of SABnzbd strings.json (#154333)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-14 14:47:50 +02:00
epenet
a92e73ff17 Move URL out of sfr_box strings.json (#154364)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-14 14:47:32 +02:00
Shay Levy
ae3d32073c Move URL out of Switcher strings.json (#154240) 2025-10-14 14:47:22 +02:00
Christopher Fenner
38d0299951 Remove URL from ViCare strings.json (#154243) 2025-10-14 14:47:12 +02:00
Stefan Agner
8dba1edbe5 Machine container: Remove codenotary configuration (#153855)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-14 14:39:38 +02:00
Jamin
f3c4288026 Use contact header for outgoing call transport (#151847) 2025-10-14 14:36:31 +02:00
peteS-UK
8db6505a97 Set initial integration_hub in manifest for Squeezebox (#154438) 2025-10-14 14:35:12 +02:00
Kamil Breguła
61a9094d5f Update WLED Select Options after update (#154205)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-10-14 14:23:41 +02:00
Joakim Plate
d140eb4c76 Protect internal coordinator state (#153685) 2025-10-14 14:14:37 +02:00
Arie Catsman
21f24c2f6a Get Enphase_envoy collar grid status from admin_state_str rather then from grid_state (#153766)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-14 13:10:14 +01:00
Aarni Koskela
85b26479de Shut down core event loop on unrecoverable errors (#144806) 2025-10-14 14:09:29 +02:00
Artur Pragacz
bddbf9c73c Simplify current ids callback in config entries (#154082) 2025-10-14 14:04:57 +02:00
Tom
64f48564ff Change device identifier and binary_sensor unique_id for airOS (#153085)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-10-14 14:02:22 +02:00
Yvan13120
06e4922021 Fix state class for Overkiz water consumption (#154164) 2025-10-14 12:49:32 +01:00
G Johansson
cdc6c44a49 Fix reconfigure flow in esphome uses create_entry (#154107) 2025-10-14 13:46:53 +02:00
mmstano
106a74c954 Prevent AttributeError in luci device tracker (#148357)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-14 13:39:56 +02:00
Domochip
8464dad8e0 Add milliPascal (mPa) as unit of measurement for Pressure (#153087) 2025-10-14 12:38:14 +01:00
Joakim Plate
c3e2f0e19b Always run install of packages with same python as script (#154253) 2025-10-14 13:35:00 +02:00
Erik Montnemery
fbf875b5af Deprecate has_mean in favor of mean_type in recorder statistic API (#154093) 2025-10-14 13:34:25 +02:00
epenet
fcea5e0da6 Simplify DPType lookup in Tuya (#150117) 2025-10-14 13:23:50 +02:00
nasWebio
81fd9e1c5a Move state conversion from library to nasweb integration code (#153208) 2025-10-14 13:21:19 +02:00
Shay Levy
d108d5f106 Use Shelly RPC cover methods from upstream and fix cover status update (#154345)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 14:07:37 +03:00
Magnus
487940872e Dependency update py-melissa-climate to 3.0.2 (#154285) 2025-10-14 12:37:37 +02:00
Paul Bottein
aaf58075c6 Rename security panel to safety panel (#154435) 2025-10-14 12:08:41 +02:00
Michel van de Wetering
a23bed6f4d Add missinglong_press entry for trigger_type in strings.json for Hue (#154437) 2025-10-14 11:23:26 +02:00
Maciej Bieniek
02e05643f1 Add boost switches for Shelly cury component (#154387) 2025-10-14 11:21:55 +02:00
Samuel Xiao
5f9b098c19 Add K11+ vacuum support to Switchbot Cloud (#154363) 2025-10-14 10:52:40 +02:00
Simone Chemelli
143f7df7fd Use aioshelly methods for climate platform (#154384) 2025-10-14 10:44:39 +02:00
Marc Mueller
9a28ee5378 Update pydantic to 2.12.1 (#154424) 2025-10-14 10:37:16 +02:00
J. Nick Koston
82f33fbc39 Bump aioesphomeapi to 41.16.0 (#154427) 2025-10-14 09:00:27 +02:00
Shay Levy
6a632a71b6 Bump aioshelly to 13.14.0 (#154421) 2025-10-14 09:28:26 +03:00
PaulCavill
ae8678b2af Bump pyiCloud to 2.1.0 (#154365) 2025-10-13 23:36:40 +01:00
Tucker Kern
b52ee6915a Make Snapcast snapshot action async (#153132) 2025-10-13 23:32:33 +01:00
Åke Strandberg
b0e1b00598 Set integration_type explicitly in miele manifest (#154375) 2025-10-13 23:29:28 +01:00
Jamin
fd902af23b VOIP Integration Type (#154418) 2025-10-13 23:21:27 +01:00
Matthias Alphart
07d6ebef4c Restore KNX sensor entity states (#154318) 2025-10-13 23:18:41 +01:00
tronikos
c9b9f05f4b Google Assistant SDK: improve config flow tests (#153794) 2025-10-13 17:28:33 -04:00
J. Nick Koston
90a0262217 Bump aioesphomeapi to 41.15.0 (#154407) 2025-10-13 10:56:24 -10:00
J. Nick Koston
324aa09ebe Update Improv BLE discovery notification when device name changes (#154352)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-10-13 16:14:51 -04:00
Dave T
663431fc80 Allow following of 302 redirects in generic camera (#154308) 2025-10-13 16:11:34 -04:00
J. Nick Koston
610183c11b Fix Improv BLE factory reset rediscovery (#154354) 2025-10-13 16:03:26 -04:00
Simone Chemelli
b7718f6f0f Bump aiocomelit to 1.1.2 (#154393) 2025-10-13 16:01:46 -04:00
Jan Bouwhuis
5708f61964 Prepare to move out URL's from MQTT translation strings (#154391) 2025-10-13 21:48:01 +02:00
G Johansson
4fb3c9fed2 Add async_update_and_abort method to config flow (#153146)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-13 21:39:04 +02:00
Marc Mueller
1e5f5f4ad3 Enable pylint consider-math-not-float check (#154338) 2025-10-13 21:14:57 +02:00
TheJulianJES
82c536a4e9 Migrate Matter descriptions to be kw_only (#154398) 2025-10-13 20:18:05 +02:00
Matthias Alphart
97afec1912 Record last_reported for KNX BinarySensor entitiy states (#154392) 2025-10-13 20:12:13 +02:00
G Johansson
0bfdd70730 async_config_entry_first_refresh in update coordinator requires a config entry (#154114) 2025-10-13 19:47:07 +02:00
johanzander
01dee6507b Add 14 additional sensor entities for Growatt TLX/MIN inverters (#153964)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 19:17:14 +02:00
Kurt
04f83bc067 Add actron_air climate integration (#134740)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-13 19:10:35 +02:00
Paulus Schoutsen
f0756af52d Add Python version file (#154267) 2025-10-13 19:02:49 +02:00
johanzander
dd6bc715d8 Add switch platform and grid charge enable for Growatt Server integration (#153960)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-13 17:54:30 +02:00
Jordan Harvey
1452aec47f Add switch platform to Nintendo Parental controls integration (#154179) 2025-10-13 17:52:06 +02:00
Simone Chemelli
6f8439de5b Fix switch platform for Comelit SimpleHome (#154227) 2025-10-13 17:48:50 +02:00
Tom
f649717372 Add model_id support to airOS (#154388) 2025-10-13 17:47:53 +02:00
Tom
bf273ef407 Add integration_type to airOS (#154390) 2025-10-13 17:47:44 +02:00
J. Nick Koston
94d015e00a Fix Bluetooth discovery for devices with alternating advertisement names (#154347) 2025-10-13 11:44:16 -04:00
Thomas D
f185ffddf1 Set model_id on device for Volvo integration (#154385) 2025-10-13 17:29:15 +02:00
Foscam-wangzhengyu
2d0b4dd7e9 New Foscam switch (#152732) 2025-10-13 17:23:27 +02:00
J. Nick Koston
eab1205823 Add config flow title placeholder update infrastructure (#154353) 2025-10-13 11:15:28 -04:00
J. Nick Koston
a991dcbe6a Add Bluetooth API to clear address from match history (#154355) 2025-10-13 16:56:39 +02:00
Yevhenii Vaskivskyi
6f79a65762 AsusWRT: Pass only online clients to the device list from the API (#154322) 2025-10-13 16:55:28 +02:00
Matthias Alphart
ce1fdc6b75 Update xknx to 3.10.0 (#154361) 2025-10-13 16:53:32 +02:00
Tom
d7aa0834c7 Bump airOS preparing for model_id matching (#154370) 2025-10-13 16:48:32 +02:00
Tom Matheussen
3151384867 Set integration type for Satel Integra to device (#154372) 2025-10-13 16:47:57 +02:00
Ravaka Razafimanantsoa
8aa5e7de91 Bump momonga to 0.2.0 (#154371) 2025-10-13 16:43:44 +02:00
Erik Montnemery
cca5c807ad Store nmap tracker options as lists (#154378) 2025-10-13 16:39:04 +02:00
Krisjanis Lejejs
89433219dd Bump hass-nabucasa from 1.2.0 to 1.3.0 (#154376) 2025-10-13 16:37:48 +02:00
Renat Sibgatulin
694b169c79 Bump aioairq to 0.4.7 (#154386) 2025-10-13 16:37:31 +02:00
puddly
f1e0954c61 Automatically setup hardware integrations when firmware info is published by an integration (#154030) 2025-10-13 16:26:01 +02:00
Erik Montnemery
3c3b4ef14a Fix stale docstring in nmap_tracker (#154380) 2025-10-13 15:45:33 +02:00
Ted van den Brink
54ff49115c Implement MAC address exclude list in nmap_tracker (#142724)
Co-authored-by: Erik <erik@montnemery.com>
2025-10-13 15:01:47 +02:00
Åke Strandberg
2512dad843 Set model_id in miele integration (#154367) 2025-10-13 14:31:22 +02:00
Erik Montnemery
a3b67d5f28 Add support to sensor statistics for changing unit_class (#154130) 2025-10-13 12:35:10 +01:00
epenet
76a0b2d616 Bump renault-api to 0.4.4 (#154137)
Thanks!
2025-10-13 11:23:33 +02:00
dependabot[bot]
1182082c1f Bump actions/dependency-review-action from 4.8.0 to 4.8.1 (#154356)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 11:12:30 +02:00
wollew
e0811558cb update pysqueezebox lib to 0.13.0 (#154358) 2025-10-13 11:05:33 +02:00
Christopher Fenner
d389405218 Bump PyViCare to 2.54.0 (#154336) 2025-10-13 11:04:25 +02:00
Magnus
3a71087c9c Bump aioasuswrt to 1.5.1 (#153209) 2025-10-13 10:59:22 +02:00
starkillerOG
c7d7cfa7ad Add Reolink IO input binary sensor (#154133) 2025-10-13 10:54:30 +02:00
wittypluck
e4ea79866d Add support for μg/m³ for Carbon Monoxide (#153158) 2025-10-13 10:08:55 +02:00
tronikos
ddfa6f33d2 Bump opower to 0.15.7 (#154351) 2025-10-13 00:07:20 -07:00
dependabot[bot]
15e99650aa Bump github/codeql-action from 4.30.7 to 4.30.8 (#154357) 2025-10-13 08:32:24 +02:00
Christopher Fenner
58bacbb84e Fix identifier generation for sub devices in ViCare (#154330) 2025-10-13 08:31:03 +02:00
Marc Mueller
82758f7671 Update pyheos to 1.0.6 (#154346) 2025-10-13 01:39:36 +02:00
David Recordon
7739cdc626 Update pyControl4 to v1.5.0 (#154341) 2025-10-12 23:28:08 +02:00
Michael Davie
4ca1ae61aa Environment Canada station selector (#154307)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-12 22:34:16 +02:00
Dave T
3d130a9bdf Simplify generic camera tests (#154313) 2025-10-12 22:06:13 +02:00
Shay Levy
2b38f33d50 Bump aioshelly to 13.13.0 (#154337) 2025-10-12 23:01:22 +03:00
Glenn Vandeuren (aka Iondependent)
19dedb038e Update nhc requirement to version 0.7.0 (#154250) 2025-10-12 21:58:01 +02:00
Dan Schafer
59781422f7 Update Snoo strings.json to include weaning_baseline (#154268) 2025-10-12 21:57:47 +02:00
Thomas55555
083277d1ff Add model_id to Husqvarna Automower (#154335) 2025-10-12 21:45:01 +02:00
Marcus Gustavsson
9b9c55b37b Updated prowlpy to 1.1.1 and changed the usage to do asynchronous calls (#154193) 2025-10-12 21:17:43 +02:00
J. Nick Koston
c9d67d596b Fix August integration to handle unavailable OAuth implementation at startup (#154244) 2025-10-12 09:16:22 -10:00
J. Nick Koston
7948b35265 Fix Yale integration to handle unavailable OAuth implementation at startup (#154245) 2025-10-12 09:16:02 -10:00
Ernst Klamer
be843970fd bump tilt-ble to 1.0.1 (#154320) 2025-10-12 21:38:27 +03:00
Michael Davie
53b65b2fb4 Bump env-canada to v0.12.1 (#154303)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-12 20:31:02 +02:00
Simone Chemelli
ac7be97245 Bump aioamazondevices to 6.4.3 (#154293) 2025-10-12 19:25:53 +02:00
Jan Bouwhuis
09e539bf0e Fix home wiziard total increasing sensors returning 0 (#154264) 2025-10-12 12:51:50 -04:00
J. Nick Koston
6ef1b3bad3 Bump aioesphomeapi to 41.14.0 (#154275) 2025-10-12 12:51:05 -04:00
Bouwe Westerdijk
38e46f7a53 Bump plugwise to v1.8.0 - add initial support for Emma (#154277) 2025-10-12 12:50:46 -04:00
Michael Davie
ef60d16659 Fix Environment Canada camera entity initialization (#154302)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-12 12:47:22 -04:00
Marc Mueller
bf4f8b48a3 Update pylint to 4.0.0 + astroid to 4.0.1 (#154311) 2025-10-12 12:46:04 -04:00
Denis Shulyaka
3c1496d2bb Add gpt-image-1-mini support (#154316) 2025-10-12 12:44:38 -04:00
Mick Vleeshouwer
d457787639 Move URL out of Overkiz Config Flow descriptions (#154315) 2025-10-12 18:23:24 +02:00
Mick Vleeshouwer
de4bfd6f05 Bump pyOverkiz to 1.19.0 in Overkiz (#154310) 2025-10-12 18:07:19 +02:00
Shay Levy
34c5748132 Align Shelly async_setup_entry in platforms (#154142)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2025-10-12 18:41:54 +03:00
Michael Hansen
5bfd9620db Handle Wyoming config entries with missing info (#154186) 2025-10-12 10:23:09 -05:00
Michael Davie
6f8766e4bd Update config flow strings for Environment Canada (#154242) 2025-10-12 11:49:29 +02:00
Jordan Harvey
d3b519846b Bump pyprobeplus to 1.1.0 (#154265) 2025-10-12 10:06:00 +02:00
Joakim Plate
36d952800b Move url like strings to placeholders for nibe (#154249) 2025-10-12 00:11:23 +02:00
Andrew Jackson
b832561e53 Move URL out of Mastodon strings.json (#154231) 2025-10-12 00:07:40 +02:00
Manu
c59d295bf2 Add description placeholders in Uptime Kuma config flow (#154252)
Signed-off-by: tr4nt0r <4445816+tr4nt0r@users.noreply.github.com>
2025-10-12 00:06:29 +02:00
Oliver Gründel
6e28e3aed1 Move developer registration url out of strings.json file (#154261) 2025-10-12 00:04:59 +02:00
Magnus
6d8944d379 Fix multiple definition of DEFAULT_PORT and DEFAULT_RETAIN constants (#154255) 2025-10-11 23:47:18 +02:00
Joost Lekkerkerker
762fd6d241 Move URL out of Aemet strings.json (#154225) 2025-10-11 20:52:51 +03:00
Simone Chemelli
4c6500e7a4 Bump aioamazondevices to 6.4.1 (#154228) 2025-10-11 18:58:14 +02:00
Jan Bouwhuis
cdc224715f Fix inconsistent naming of MQTT test config globals (#154221) 2025-10-11 16:29:13 +02:00
Paul Bottein
648b250fc8 Bump frontend 20251001.4 (#154218) 2025-10-11 09:33:06 -04:00
Ernst Klamer
ba61562300 Bump kegtron-ble to 1.0.2 (#154207) 2025-10-11 16:27:25 +03:00
Marc Mueller
8d67182e0e [ci] No longer install setuptools + wheel by default (#154212) 2025-10-11 15:26:04 +02:00
Shay Levy
3ce1ef4c3f Use Entity Description in Shelly light platform (#154102) 2025-10-11 16:15:48 +03:00
Ludovic BOUÉ
bde4eb5011 Rename Matter SolarPower fixture to Solar inverter (#154201)
The goal is to facilitate understanding for the introduction of Matter namespaces and tags:
 - https://github.com/home-assistant/core/pull/152754
2025-10-11 12:16:37 +02:00
srirams
a58a7065b6 Remove redudant state write in Smart Meter Texas (#154126) 2025-10-11 10:32:10 +02:00
Marc Mueller
0c9b72bf1d Update pylint to 3.3.9 (#154194) 2025-10-11 09:31:36 +02:00
Matthias Alphart
541d94d8c6 Record last_reported for KNX sensor entitiy states (#154169) 2025-10-11 08:21:51 +02:00
Abílio Costa
c370c86a4f Use custom string for Oral-B no-devices-found message (#154183) 2025-10-10 22:08:38 +01:00
Paul Bottein
bc6accf4ae Add missing entity category and icons for smlight integration (#154131) 2025-10-10 21:30:32 +02:00
G Johansson
d40eeee422 Remove deprecated ConfigSource from core (#154112) 2025-10-10 18:23:13 +02:00
Erik Montnemery
c9d9730c4a Change domain and name of Nintendo Switch parental controls integration (#153893) 2025-10-10 17:11:10 +02:00
ehendrix23
d3a8f3191b Add Speech-to-Text (stt) to elevenlabs (#147838)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-10 17:01:22 +02:00
Thomas D
cb3829ddee Add buttons to Volvo integration (#153272)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2025-10-10 16:51:35 +02:00
tronikos
73383e6c26 Add reconfigure flow in Google Assistant SDK (#153802) 2025-10-10 16:24:46 +02:00
Matthias Alphart
217894ee8b Update knx-frontend to 2025.10.9.185845 (#154103) 2025-10-10 16:24:14 +02:00
Thomas D
c7321a337e Add device_tracker platform to Volvo integration (#153437) 2025-10-10 16:23:07 +02:00
Denis Shulyaka
517124dfbe Anthropic web search support (#153753) 2025-10-10 16:21:21 +02:00
hanwg
f49299b009 Add edit message media feature for Telegram bot (#151034) 2025-10-10 15:50:54 +02:00
Shay Levy
1001da08f6 Fix Shelly RPC cover update when the device is not initialized (#154159) 2025-10-10 16:50:45 +03:00
Lars
0da019404c Remove deprecated extra attributes from fritzbox climate (#154152) 2025-10-10 15:48:22 +02:00
Jan Čermák
9a4280d0de Add attachments support to OpenRouter AI task (#154161) 2025-10-10 15:44:33 +02:00
Lukas
c28e105df5 Pooldose update api (#153497)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-10 15:43:47 +02:00
Matthias Alphart
68787248f6 Update xknx to 3.9.1 (#154146) 2025-10-10 15:42:38 +02:00
Tom Matheussen
36be6b6187 Add configured number to Satel Integra subentry titles (#154155) 2025-10-10 15:27:23 +02:00
Jordan Harvey
42dea92c51 Add time platform to nintendo_parental integration (#153866)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-10 15:20:59 +02:00
Justus
4b828d4753 IOmeter bump version v0.2.0 (#154150) 2025-10-10 15:15:58 +02:00
Robert Resch
8e79c38f34 Bump deebot-client to 15.1.0 (#154154) 2025-10-10 16:07:45 +03:00
jvmahon
c92107b8d4 Inherit MatterEntityDescription in Matter entities (#154083) 2025-10-10 15:04:01 +02:00
epenet
b25622f40e Use SI constants in CO unit converter (#153187) 2025-10-10 14:59:20 +02:00
Petro31
e887d5e6ad Fix delay_on and auto_off with multiple triggers (#153839) 2025-10-10 14:21:11 +02:00
TheJulianJES
1f19e40cfe Adjust OTBR config entry name for ZBT-2 (#153940) 2025-10-10 14:19:08 +02:00
Bram Kragten
3d2d2271d3 Update frontend to 20251001.2 (#154143) 2025-10-10 14:08:17 +02:00
Jack Thomasson
d1dd5eecd6 use a consistent python version for uv (#154022) 2025-10-10 13:59:45 +02:00
Jan Bouwhuis
cdec29ffb7 Add MQTT select subentry support (#153637) 2025-10-10 13:46:21 +02:00
peteS-UK
07f3e00f18 Fix for multiple Lyrion Music Server on a single Home Assistant server for Squeezebox (#154081) 2025-10-10 13:36:46 +02:00
starkillerOG
084d029168 Add Reolink survaillance rule switch entities (#154132)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 13:35:07 +02:00
tronikos
17e997ee18 Add module-level statistics to SolarEdge (#152581) 2025-10-10 13:07:39 +02:00
J. Diego Rodríguez Royo
16d4c6c95a Add Spotless series features to Home Connect integration (#153016) 2025-10-10 13:00:17 +02:00
epenet
0205a636ef Filter out invalid Renault vehicles (#154070)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 12:52:21 +02:00
hanwg
4707fd2f94 Update quality scale for Telegram bot (#154122) 2025-10-10 12:47:55 +02:00
J. Nick Koston
ad3cadab83 Bump propcache to 0.4.1 (#154033) 2025-10-10 11:45:58 +01:00
Jordan Harvey
3fce815415 Add reauthentication to Nintendo Switch Parental controls integration (#154077) 2025-10-10 12:31:46 +02:00
Erik Montnemery
ee67619cb1 Add mg/m³ as a valid UOM for sensor/number Carbon Monoxide device class (#154074)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-10 11:31:31 +01:00
TheJulianJES
1a744a2c91 Fix HA hardware configuration message for Thread without HAOS (#153933) 2025-10-10 11:37:43 +02:00
Erik Montnemery
951978e483 Include unit class in units_changed statistics issue (#154069) 2025-10-10 10:50:40 +03:00
Marcus Gustavsson
54d30377d3 Add ConfigFlow to Prowl integration (#133771)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-10 09:41:45 +02:00
Shay Levy
eb04dda197 Use Entity Description in Shelly BLU TRV button (#154118) 2025-10-10 10:24:08 +03:00
Fabien Kleinbourg
1e192aadfa sharkiq dependency bump to 1.4.2 (#153931) 2025-10-10 08:40:20 +02:00
Erwin Douna
6f680f3d03 Portainer fix offline endpoint (#154101) 2025-10-10 08:14:49 +02:00
starkillerOG
f0663dc275 Bump reolink-aio to 0.16.2 (#154117) 2025-10-10 02:20:11 +03:00
Paulus Schoutsen
96bb67bef9 Z-Wave: ESPHome discovery to update all options (#154113) 2025-10-09 17:14:53 -04:00
879 changed files with 60380 additions and 6973 deletions

View File

@@ -326,7 +326,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.2.3"

View File

@@ -37,12 +37,12 @@ on:
type: boolean
env:
CACHE_VERSION: 8
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.11"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -525,7 +525,7 @@ jobs:
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
uv pip install -U "pip>=21.3.1" setuptools wheel
uv pip install -U "pip>=25.2"
uv pip install -r requirements.txt
python -m script.gen_requirements_all ci
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
@@ -625,7 +625,7 @@ jobs:
steps:
- *checkout
- name: Dependency review
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
with:
license-check: false # We use our own license audit checks

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
category: "/language:python"

1
.gitignore vendored
View File

@@ -79,7 +79,6 @@ junit.xml
.project
.pydevproject
.python-version
.tool-versions
# emacs auto backups

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

18
CODEOWNERS generated
View File

@@ -46,6 +46,8 @@ build.json @home-assistant/supervisor
/tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray
/tests/components/acmeda/ @atmurray
/homeassistant/components/actron_air/ @kclif9 @JagadishDhanamjayam
/tests/components/actron_air/ @kclif9 @JagadishDhanamjayam
/homeassistant/components/adax/ @danielhiversen @lazytarget
/tests/components/adax/ @danielhiversen @lazytarget
/homeassistant/components/adguard/ @frenck
@@ -617,6 +619,8 @@ build.json @home-assistant/supervisor
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
/tests/components/group/ @home-assistant/core
/homeassistant/components/growatt_server/ @johanzander
/tests/components/growatt_server/ @johanzander
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @tr4nt0r
@@ -762,8 +766,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/intesishome/ @jnimmo
/homeassistant/components/iometer/ @MaestroOnICe
/tests/components/iometer/ @MaestroOnICe
/homeassistant/components/iometer/ @jukrebs
/tests/components/iometer/ @jukrebs
/homeassistant/components/ios/ @robbiet480
/tests/components/ios/ @robbiet480
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
@@ -1065,8 +1069,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/nilu/ @hfurubotten
/homeassistant/components/nina/ @DeerMaximum
/tests/components/nina/ @DeerMaximum
/homeassistant/components/nintendo_parental/ @pantherale0
/tests/components/nintendo_parental/ @pantherale0
/homeassistant/components/nintendo_parental_controls/ @pantherale0
/tests/components/nintendo_parental_controls/ @pantherale0
/homeassistant/components/nissan_leaf/ @filcole
/homeassistant/components/noaa_tides/ @jdelaney72
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
@@ -1135,6 +1139,8 @@ build.json @home-assistant/supervisor
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/openrgb/ @felipecrs
/tests/components/openrgb/ @felipecrs
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
@@ -1479,8 +1485,8 @@ build.json @home-assistant/supervisor
/tests/components/snoo/ @Lash-L
/homeassistant/components/snooz/ @AustinBrunkhorst
/tests/components/snooz/ @AustinBrunkhorst
/homeassistant/components/solaredge/ @frenck @bdraco
/tests/components/solaredge/ @frenck @bdraco
/homeassistant/components/solaredge/ @frenck @bdraco @tronikos
/tests/components/solaredge/ @frenck @bdraco @tronikos
/homeassistant/components/solaredge_local/ @drobtravels @scheric
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
/tests/components/solarlog/ @Ernst79 @dontinelli

View File

@@ -36,7 +36,8 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
USER vscode
RUN uv python install 3.13.2
COPY .python-version ./
RUN uv python install
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
RUN uv venv $VIRTUAL_ENV

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -0,0 +1,57 @@
"""The Actron Air integration."""
from actron_neo_api import (
ActronAirNeoACSystem,
ActronNeoAPI,
ActronNeoAPIError,
ActronNeoAuthError,
)
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from .const import _LOGGER
from .coordinator import (
ActronAirConfigEntry,
ActronAirRuntimeData,
ActronAirSystemCoordinator,
)
PLATFORM = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Set up Actron Air integration from a config entry."""
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirNeoACSystem] = []
try:
systems = await api.get_ac_systems()
await api.update_status()
except ActronNeoAuthError:
_LOGGER.error("Authentication error while setting up Actron Air integration")
raise
except ActronNeoAPIError as err:
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
raise
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems:
coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
_LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
await coordinator.async_config_entry_first_refresh()
system_coordinators[system["serial"]] = coordinator
entry.runtime_data = ActronAirRuntimeData(
api=api,
system_coordinators=system_coordinators,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)

View File

@@ -0,0 +1,259 @@
"""Climate platform for Actron Air integration."""
from typing import Any
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
PARALLEL_UPDATES = 0
FAN_MODE_MAPPING_ACTRONAIR_TO_HA = {
"AUTO": FAN_AUTO,
"LOW": FAN_LOW,
"MED": FAN_MEDIUM,
"HIGH": FAN_HIGH,
}
FAN_MODE_MAPPING_HA_TO_ACTRONAIR = {
v: k for k, v in FAN_MODE_MAPPING_ACTRONAIR_TO_HA.items()
}
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = {
"COOL": HVACMode.COOL,
"HEAT": HVACMode.HEAT,
"FAN": HVACMode.FAN_ONLY,
"AUTO": HVACMode.AUTO,
"OFF": HVACMode.OFF,
}
HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = {
v: k for k, v in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.items()
}
async def async_setup_entry(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Actron Air climate entities."""
system_coordinators = entry.runtime_data.system_coordinators
entities: list[ClimateEntity] = []
for coordinator in system_coordinators.values():
status = coordinator.data
name = status.ac_system.system_name
entities.append(ActronSystemClimate(coordinator, name))
entities.extend(
ActronZoneClimate(coordinator, zone)
for zone in status.remote_zone_info
if zone.exists
)
async_add_entities(entities)
class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
"""Base class for Actron Air climate entities."""
_attr_has_entity_name = True
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
_attr_name = None
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator)
self._serial_number = coordinator.serial_number
class ActronSystemClimate(BaseClimateEntity):
"""Representation of the Actron Air system."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, name)
serial_number = coordinator.serial_number
self._attr_unique_id = serial_number
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=self._status.ac_system.system_name,
manufacturer="Actron Air",
model_id=self._status.ac_system.master_wc_model,
sw_version=self._status.ac_system.master_wc_firmware_version,
serial_number=serial_number,
)
@property
def min_temp(self) -> float:
"""Return the minimum temperature that can be set."""
return self._status.min_temp
@property
def max_temp(self) -> float:
"""Return the maximum temperature that can be set."""
return self._status.max_temp
@property
def _status(self) -> ActronAirNeoStatus:
"""Get the current status from the coordinator."""
return self.coordinator.data
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
if not self._status.user_aircon_settings.is_on:
return HVACMode.OFF
mode = self._status.user_aircon_settings.mode
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self._status.user_aircon_settings.fan_mode
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
@property
def current_humidity(self) -> float:
"""Return the current humidity."""
return self._status.master_info.live_humidity_pc
@property
def current_temperature(self) -> float:
"""Return the current temperature."""
return self._status.master_info.live_temp_c
@property
def target_temperature(self) -> float:
"""Return the target temperature."""
return self._status.user_aircon_settings.temperature_setpoint_cool_c
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.lower())
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
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)
await self._status.ac_system.set_system_mode(ac_mode)
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)
class ActronZoneClimate(BaseClimateEntity):
"""Representation of a zone within the Actron Air system."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
zone: ActronAirNeoZone,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, zone.title)
serial_number = coordinator.serial_number
self._zone_id: int = zone.zone_id
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
self._attr_device_info: DeviceInfo = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=zone.title,
manufacturer="Actron Air",
model="Zone",
suggested_area=zone.title,
via_device=(DOMAIN, serial_number),
)
@property
def min_temp(self) -> float:
"""Return the minimum temperature that can be set."""
return self._zone.min_temp
@property
def max_temp(self) -> float:
"""Return the maximum temperature that can be set."""
return self._zone.max_temp
@property
def _zone(self) -> ActronAirNeoZone:
"""Get the current zone data from the coordinator."""
status = self.coordinator.data
return status.zones[self._zone_id]
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
if self._zone.is_active:
mode = self._zone.hvac_mode
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
return HVACMode.OFF
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
return self._zone.humidity
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._zone.live_temp_c
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
return self._zone.temperature_setpoint_cool_c
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
is_enabled = hvac_mode != HVACMode.OFF
await self._zone.enable(is_enabled)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
await self._zone.set_temperature(temperature=kwargs["temperature"])

View File

@@ -0,0 +1,132 @@
"""Setup config flow for Actron Air integration."""
import asyncio
from typing import Any
from actron_neo_api import ActronNeoAPI, ActronNeoAuthError
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN
from homeassistant.exceptions import HomeAssistantError
from .const import _LOGGER, DOMAIN
class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Actron Air."""
def __init__(self) -> None:
"""Initialize the config flow."""
self._api: ActronNeoAPI | None = None
self._device_code: str | None = None
self._user_code: str = ""
self._verification_uri: str = ""
self._expires_minutes: str = "30"
self.login_task: asyncio.Task | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if self._api is None:
_LOGGER.debug("Initiating device authorization")
self._api = ActronNeoAPI()
try:
device_code_response = await self._api.request_device_code()
except ActronNeoAuthError as err:
_LOGGER.error("OAuth2 flow failed: %s", err)
return self.async_abort(reason="oauth2_error")
self._device_code = device_code_response["device_code"]
self._user_code = device_code_response["user_code"]
self._verification_uri = device_code_response["verification_uri_complete"]
self._expires_minutes = str(device_code_response["expires_in"] // 60)
async def _wait_for_authorization() -> None:
"""Wait for the user to authorize the device."""
assert self._api is not None
assert self._device_code is not None
_LOGGER.debug("Waiting for device authorization")
try:
await self._api.poll_for_token(self._device_code)
_LOGGER.debug("Authorization successful")
except ActronNeoAuthError as ex:
_LOGGER.exception("Error while waiting for device authorization")
raise CannotConnect from ex
_LOGGER.debug("Checking login task")
if self.login_task is None:
_LOGGER.debug("Creating task for device authorization")
self.login_task = self.hass.async_create_task(_wait_for_authorization())
if self.login_task.done():
_LOGGER.debug("Login task is done, checking results")
if exception := self.login_task.exception():
if isinstance(exception, CannotConnect):
return self.async_show_progress_done(
next_step_id="connection_error"
)
return self.async_show_progress_done(next_step_id="timeout")
return self.async_show_progress_done(next_step_id="finish_login")
return self.async_show_progress(
step_id="user",
progress_action="wait_for_authorization",
description_placeholders={
"user_code": self._user_code,
"verification_uri": self._verification_uri,
"expires_minutes": self._expires_minutes,
},
progress_task=self.login_task,
)
async def async_step_finish_login(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the finalization of login."""
_LOGGER.debug("Finalizing authorization")
assert self._api is not None
try:
user_data = await self._api.get_user_info()
except ActronNeoAuthError as err:
_LOGGER.error("Error getting user info: %s", err)
return self.async_abort(reason="oauth2_error")
unique_id = str(user_data["id"])
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_data["email"],
data={CONF_API_TOKEN: self._api.refresh_token_value},
)
async def async_step_timeout(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle issues that need transition await from progress step."""
if user_input is None:
return self.async_show_form(
step_id="timeout",
)
del self.login_task
return await self.async_step_user()
async def async_step_connection_error(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle connection error from progress step."""
if user_input is None:
return self.async_show_form(step_id="connection_error")
# Reset state and try again
self._api = None
self._device_code = None
self.login_task = None
return await self.async_step_user()
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@@ -0,0 +1,6 @@
"""Constants used by Actron Air integration."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "actron_air"

View File

@@ -0,0 +1,69 @@
"""Coordinator for Actron Air integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from actron_neo_api import ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from .const import _LOGGER
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
ERROR_UNKNOWN = "unknown_error"
@dataclass
class ActronAirRuntimeData:
"""Runtime data for the Actron Air integration."""
api: ActronNeoAPI
system_coordinators: dict[str, ActronAirSystemCoordinator]
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
AUTH_ERROR_THRESHOLD = 3
SCAN_INTERVAL = timedelta(seconds=30)
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
"""System coordinator for Actron Air integration."""
def __init__(
self,
hass: HomeAssistant,
entry: ActronAirConfigEntry,
api: ActronNeoAPI,
system: ActronAirNeoACSystem,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name="Actron Air Status",
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
self.system = system
self.serial_number = system["serial"]
self.api = api
self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow()
async def _async_update_data(self) -> ActronAirNeoStatus:
"""Fetch updates and merge incremental changes into the full state."""
await self.api.update_status()
self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow()
return self.status
def is_device_stale(self) -> bool:
"""Check if a device is stale (not seen for a while)."""
return (dt_util.utcnow() - self.last_seen) > STALE_DEVICE_TIMEOUT

View File

@@ -0,0 +1,16 @@
{
"domain": "actron_air",
"name": "Actron Air",
"codeowners": ["@kclif9", "@JagadishDhanamjayam"],
"config_flow": true,
"dhcp": [
{
"hostname": "neo-*",
"macaddress": "FC0FE7*"
}
],
"documentation": "https://www.home-assistant.io/integrations/actron_air",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["actron-neo-api==0.1.84"]
}

View File

@@ -0,0 +1,78 @@
rules:
# Bronze
action-setup:
status: exempt
comment: This integration does not have custom service actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: This integration does not have custom service actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: This integration does not subscribe to external events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No options flow
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update.
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
entity-category:
status: exempt
comment: This integration does not use entity categories.
entity-device-class:
status: exempt
comment: This integration does not use entity device classes.
entity-disabled-by-default:
status: exempt
comment: Not required for this integration at this stage.
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: This integration does not have any known issues that require repair.
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: todo

View File

@@ -0,0 +1,29 @@
{
"config": {
"step": {
"user": {
"title": "Actron Air OAuth2 Authorization"
},
"timeout": {
"title": "Authorization timeout",
"description": "The authorization process timed out. Please try again.",
"data": {}
},
"connection_error": {
"title": "Connection error",
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
"data": {}
}
},
"progress": {
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
},
"error": {
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
},
"abort": {
"oauth2_error": "Failed to start OAuth2 flow",
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@@ -71,7 +71,14 @@ class AemetConfigFlow(ConfigFlow, domain=DOMAIN):
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors,
description_placeholders={
"api_key_url": "https://opendata.aemet.es/centrodedescargas/altaUsuario"
},
)
@staticmethod
@callback

View File

@@ -14,7 +14,7 @@
"longitude": "[%key:common::config_flow::data::longitude%]",
"name": "Name of the integration"
},
"description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario"
"description": "To generate API key go to {api_key_url}"
}
}
},

View File

@@ -53,9 +53,6 @@ __all__ = [
"GenImageTaskResult",
"async_generate_data",
"async_generate_image",
"async_setup",
"async_setup_entry",
"async_unload_entry",
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -18,6 +18,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS
DESCRIPTION_PLACEHOLDERS = {
"developer_registration_url": "https://developer.airly.eu/register",
}
class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Airly."""
@@ -85,6 +89,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
}
),
errors=errors,
description_placeholders=DESCRIPTION_PLACEHOLDERS,
)

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "To generate API key go to https://developer.airly.eu/register",
"description": "To generate API key go to {developer_registration_url}",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
import logging
from airos.airos8 import AirOS8
from homeassistant.const import (
@@ -12,10 +14,11 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [
@@ -23,6 +26,8 @@ _PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Set up Ubiquiti airOS from a config entry."""
@@ -54,11 +59,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Migrate old config entry."""
if entry.version > 1:
# This means the user has downgraded from a future version
# This means the user has downgraded from a future version
if entry.version > 2:
return False
# 1.1 Migrate config_entry to add advanced ssl settings
if entry.version == 1 and entry.minor_version == 1:
new_minor_version = 2
new_data = {**entry.data}
advanced_data = {
CONF_SSL: DEFAULT_SSL,
@@ -69,7 +76,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b
hass.config_entries.async_update_entry(
entry,
data=new_data,
minor_version=2,
minor_version=new_minor_version,
)
# 2.1 Migrate binary_sensor entity unique_id from device_id to mac_address
# Step 1 - migrate binary_sensor entity unique_id
# Step 2 - migrate device entity identifier
if entry.version == 1:
new_version = 2
new_minor_version = 1
mac_adress = dr.format_mac(entry.unique_id)
device_registry = dr.async_get(hass)
if device_entry := device_registry.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)}
):
old_device_id = next(
(
device_id
for domain, device_id in device_entry.identifiers
if domain == DOMAIN
),
)
@callback
def update_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, str] | None:
"""Update unique id from device_id to mac address."""
if old_device_id and entity_entry.unique_id.startswith(old_device_id):
suffix = entity_entry.unique_id.removeprefix(old_device_id)
new_unique_id = f"{mac_adress}{suffix}"
return {"new_unique_id": new_unique_id}
return None
await er.async_migrate_entries(hass, entry.entry_id, update_unique_id)
new_identifiers = device_entry.identifiers.copy()
new_identifiers.discard((DOMAIN, old_device_id))
new_identifiers.add((DOMAIN, mac_adress))
device_registry.async_update_device(
device_entry.id, new_identifiers=new_identifiers
)
hass.config_entries.async_update_entry(
entry, version=new_version, minor_version=new_minor_version
)
return True

View File

@@ -98,7 +98,7 @@ class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
@property
def is_on(self) -> bool:

View File

@@ -15,7 +15,12 @@ from airos.exceptions import (
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -57,8 +62,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ubiquiti airOS."""
VERSION = 1
MINOR_VERSION = 2
VERSION = 2
MINOR_VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -119,7 +124,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(airos_data.derived.mac)
if self.source == SOURCE_REAUTH:
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
@@ -164,3 +169,54 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=self.errors,
)
async def async_step_reconfigure(
self,
user_input: Mapping[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of airOS."""
self.errors = {}
entry = self._get_reconfigure_entry()
current_data = entry.data
if user_input is not None:
validate_data = {**current_data, **user_input}
if await self._validate_and_get_device_info(config_data=validate_data):
return self.async_update_reload_and_abort(
entry,
data_updates=validate_data,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(
CONF_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_SSL
],
): bool,
vol.Required(
CONF_VERIFY_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_VERIFY_SSL
],
): bool,
}
),
{"collapsed": True},
),
}
),
errors=self.errors,
)

View File

@@ -33,9 +33,14 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
configuration_url=configuration_url,
identifiers={(DOMAIN, str(airos_data.host.device_id))},
identifiers={(DOMAIN, airos_data.derived.mac)},
manufacturer=MANUFACTURER,
model=airos_data.host.devmodel,
model_id=(
sku
if (sku := airos_data.derived.sku) not in ["UNKNOWN", "AMBIGUOUS"]
else None
),
name=airos_data.host.hostname,
sw_version=airos_data.host.fwversion,
)

View File

@@ -4,7 +4,8 @@
"codeowners": ["@CoMPaTech"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airos",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.5.5"]
"quality_scale": "silver",
"requirements": ["airos==0.5.6"]
}

View File

@@ -32,11 +32,11 @@ rules:
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
@@ -48,9 +48,9 @@ rules:
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: todo
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
docs-use-cases: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
@@ -60,7 +60,7 @@ rules:
icon-translations:
status: exempt
comment: no (custom) icons used or envisioned
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices: todo

View File

@@ -10,6 +10,27 @@
"password": "[%key:component::airos::config::step::user::data_description::password%]"
}
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::airos::config::step::user::data_description::password%]"
},
"sections": {
"advanced_settings": {
"name": "[%key:component::airos::config::step::user::sections::advanced_settings::name%]",
"data": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::ssl%]",
"verify_ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::verify_ssl%]"
}
}
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -23,6 +44,7 @@
},
"sections": {
"advanced_settings": {
"name": "Advanced settings",
"data": {
"ssl": "Use HTTPS",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
@@ -44,6 +66,7 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
}
},

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.6"]
"requirements": ["aioairq==0.4.7"]
}

View File

@@ -29,7 +29,7 @@
},
"data_description": {
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0"
}
}
}

View File

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

View File

@@ -4,12 +4,15 @@ from __future__ import annotations
from collections.abc import Mapping
from functools import partial
import json
import logging
from typing import Any, cast
import anthropic
import voluptuous as vol
from voluptuous_openapi import convert
from homeassistant.components.zone import ENTITY_ID_HOME
from homeassistant.config_entries import (
ConfigEntry,
ConfigEntryState,
@@ -18,7 +21,13 @@ from homeassistant.config_entries import (
ConfigSubentryFlow,
SubentryFlowResult,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_API_KEY,
CONF_LLM_HASS_API,
CONF_NAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
@@ -37,12 +46,23 @@ from .const import (
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_MAX_USES,
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_TIMEZONE,
CONF_WEB_SEARCH_USER_LOCATION,
DEFAULT_CONVERSATION_NAME,
DOMAIN,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
RECOMMENDED_WEB_SEARCH,
RECOMMENDED_WEB_SEARCH_MAX_USES,
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
WEB_SEARCH_UNSUPPORTED_MODELS,
)
_LOGGER = logging.getLogger(__name__)
@@ -168,6 +188,14 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
if user_input.get(CONF_WEB_SEARCH, RECOMMENDED_WEB_SEARCH):
model = user_input.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
if model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
errors[CONF_WEB_SEARCH] = "web_search_unsupported_model"
elif user_input.get(
CONF_WEB_SEARCH_USER_LOCATION, RECOMMENDED_WEB_SEARCH_USER_LOCATION
):
user_input.update(await self._get_location_data())
if not errors:
if self._is_new:
@@ -215,6 +243,68 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
errors=errors or None,
)
async def _get_location_data(self) -> dict[str, str]:
"""Get approximate location data of the user."""
location_data: dict[str, str] = {}
zone_home = self.hass.states.get(ENTITY_ID_HOME)
if zone_home is not None:
client = await self.hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=self._get_entry().data[CONF_API_KEY],
)
)
location_schema = vol.Schema(
{
vol.Optional(
CONF_WEB_SEARCH_CITY,
description="Free text input for the city, e.g. `San Francisco`",
): str,
vol.Optional(
CONF_WEB_SEARCH_REGION,
description="Free text input for the region, e.g. `California`",
): str,
}
)
response = await client.messages.create(
model=RECOMMENDED_CHAT_MODEL,
messages=[
{
"role": "user",
"content": "Where are the following coordinates located: "
f"({zone_home.attributes[ATTR_LATITUDE]},"
f" {zone_home.attributes[ATTR_LONGITUDE]})? Please respond "
"only with a JSON object using the following schema:\n"
f"{convert(location_schema)}",
},
{
"role": "assistant",
"content": "{", # hints the model to skip any preamble
},
],
max_tokens=RECOMMENDED_MAX_TOKENS,
)
_LOGGER.debug("Model response: %s", response.content)
location_data = location_schema(
json.loads(
"{"
+ "".join(
block.text
for block in response.content
if isinstance(block, anthropic.types.TextBlock)
)
)
or {}
)
if self.hass.config.country:
location_data[CONF_WEB_SEARCH_COUNTRY] = self.hass.config.country
location_data[CONF_WEB_SEARCH_TIMEZONE] = self.hass.config.time_zone
_LOGGER.debug("Location data: %s", location_data)
return location_data
async_step_user = async_step_set_options
async_step_reconfigure = async_step_set_options
@@ -273,6 +363,18 @@ def anthropic_config_option_schema(
CONF_THINKING_BUDGET,
default=RECOMMENDED_THINKING_BUDGET,
): int,
vol.Optional(
CONF_WEB_SEARCH,
default=RECOMMENDED_WEB_SEARCH,
): bool,
vol.Optional(
CONF_WEB_SEARCH_MAX_USES,
default=RECOMMENDED_WEB_SEARCH_MAX_USES,
): int,
vol.Optional(
CONF_WEB_SEARCH_USER_LOCATION,
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
): bool,
}
)
return schema

View File

@@ -18,9 +18,26 @@ RECOMMENDED_TEMPERATURE = 1.0
CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
CONF_WEB_SEARCH = "web_search"
RECOMMENDED_WEB_SEARCH = False
CONF_WEB_SEARCH_USER_LOCATION = "user_location"
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False
CONF_WEB_SEARCH_MAX_USES = "web_search_max_uses"
RECOMMENDED_WEB_SEARCH_MAX_USES = 5
CONF_WEB_SEARCH_CITY = "city"
CONF_WEB_SEARCH_REGION = "region"
CONF_WEB_SEARCH_COUNTRY = "country"
CONF_WEB_SEARCH_TIMEZONE = "timezone"
NON_THINKING_MODELS = [
"claude-3-5", # Both sonnet and haiku
"claude-3-opus",
"claude-3-haiku",
]
WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-haiku",
"claude-3-opus",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
]

View File

@@ -1,12 +1,17 @@
"""Base entity for Anthropic."""
from collections.abc import AsyncGenerator, Callable, Iterable
from dataclasses import dataclass, field
import json
from typing import Any
import anthropic
from anthropic import AsyncStream
from anthropic.types import (
CitationsDelta,
CitationsWebSearchResultLocation,
CitationWebSearchResultLocationParam,
ContentBlockParam,
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
@@ -16,11 +21,16 @@ from anthropic.types import (
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
ServerToolUseBlock,
ServerToolUseBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextCitation,
TextCitationParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
@@ -29,9 +39,15 @@ from anthropic.types import (
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUnionParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
WebSearchTool20250305Param,
WebSearchToolRequestErrorParam,
WebSearchToolResultBlock,
WebSearchToolResultBlockParam,
WebSearchToolResultError,
)
from anthropic.types.message_create_params import MessageCreateParamsStreaming
from voluptuous_openapi import convert
@@ -48,6 +64,13 @@ from .const import (
CONF_MAX_TOKENS,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
CONF_WEB_SEARCH,
CONF_WEB_SEARCH_CITY,
CONF_WEB_SEARCH_COUNTRY,
CONF_WEB_SEARCH_MAX_USES,
CONF_WEB_SEARCH_REGION,
CONF_WEB_SEARCH_TIMEZONE,
CONF_WEB_SEARCH_USER_LOCATION,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
@@ -73,6 +96,69 @@ def _format_tool(
)
@dataclass(slots=True)
class CitationDetails:
"""Citation details for a content part."""
index: int = 0
"""Start position of the text."""
length: int = 0
"""Length of the relevant data."""
citations: list[TextCitationParam] = field(default_factory=list)
"""Citations for the content part."""
@dataclass(slots=True)
class ContentDetails:
"""Native data for AssistantContent."""
citation_details: list[CitationDetails] = field(default_factory=list)
def has_content(self) -> bool:
"""Check if there is any content."""
return any(detail.length > 0 for detail in self.citation_details)
def has_citations(self) -> bool:
"""Check if there are any citations."""
return any(detail.citations for detail in self.citation_details)
def add_citation_detail(self) -> None:
"""Add a new citation detail."""
if not self.citation_details or self.citation_details[-1].length > 0:
self.citation_details.append(
CitationDetails(
index=self.citation_details[-1].index
+ self.citation_details[-1].length
if self.citation_details
else 0
)
)
def add_citation(self, citation: TextCitation) -> None:
"""Add a citation to the current detail."""
if not self.citation_details:
self.citation_details.append(CitationDetails())
citation_param: TextCitationParam | None = None
if isinstance(citation, CitationsWebSearchResultLocation):
citation_param = CitationWebSearchResultLocationParam(
type="web_search_result_location",
title=citation.title,
url=citation.url,
cited_text=citation.cited_text,
encrypted_index=citation.encrypted_index,
)
if citation_param:
self.citation_details[-1].citations.append(citation_param)
def delete_empty(self) -> None:
"""Delete empty citation details."""
self.citation_details = [
detail for detail in self.citation_details if detail.citations
]
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
@@ -81,15 +167,31 @@ def _convert_content(
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
if content.tool_name == "web_search":
tool_result_block: ContentBlockParam = WebSearchToolResultBlockParam(
type="web_search_tool_result",
tool_use_id=content.tool_call_id,
content=content.tool_result["content"]
if "content" in content.tool_result
else WebSearchToolRequestErrorParam(
type="web_search_tool_result_error",
error_code=content.tool_result.get("error_code", "unavailable"), # type: ignore[typeddict-item]
),
)
external_tool = True
else:
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
external_tool = False
if not messages or messages[-1]["role"] != (
"assistant" if external_tool else "user"
):
messages.append(
MessageParam(
role="user",
role="assistant" if external_tool else "user",
content=[tool_result_block],
)
)
@@ -151,13 +253,56 @@ def _convert_content(
redacted_thinking_block
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
current_index = 0
for detail in (
content.native.citation_details
if isinstance(content.native, ContentDetails)
else [CitationDetails(length=len(content.content))]
):
if detail.index > current_index:
# Add text block for any text without citations
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(
type="text",
text=content.content[current_index : detail.index],
)
)
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(
type="text",
text=content.content[
detail.index : detail.index + detail.length
],
citations=detail.citations,
)
if detail.citations
else TextBlockParam(
type="text",
text=content.content[
detail.index : detail.index + detail.length
],
)
)
current_index = detail.index + detail.length
if current_index < len(content.content):
# Add text block for any remaining text without citations
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(
type="text",
text=content.content[current_index:],
)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
ServerToolUseBlockParam(
type="server_tool_use",
id=tool_call.id,
name="web_search",
input=tool_call.tool_args,
)
if tool_call.external and tool_call.tool_name == "web_search"
else ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
@@ -173,10 +318,12 @@ def _convert_content(
return messages
async def _transform_stream(
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
stream: AsyncStream[MessageStreamEvent],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
) -> AsyncGenerator[
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
@@ -209,11 +356,13 @@ async def _transform_stream(
if stream is None:
raise TypeError("Expected a stream of messages")
current_tool_block: ToolUseBlockParam | None = None
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
current_tool_args: str
content_details = ContentDetails()
content_details.add_citation_detail()
input_usage: Usage | None = None
has_content = False
has_native = False
first_block: bool
async for response in stream:
LOGGER.debug("Received response: %s", response)
@@ -222,6 +371,7 @@ async def _transform_stream(
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
input_usage = response.message.usage
first_block = True
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_tool_block = ToolUseBlockParam(
@@ -232,17 +382,37 @@ async def _transform_stream(
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
if has_content:
if ( # Do not start a new assistant content just for citations, concatenate consecutive blocks with citations instead.
first_block
or (
not content_details.has_citations()
and response.content_block.citations is None
and content_details.has_content()
)
):
if content_details.has_citations():
content_details.delete_empty()
yield {"native": content_details}
content_details = ContentDetails()
yield {"role": "assistant"}
has_native = False
has_content = True
first_block = False
content_details.add_citation_detail()
if response.content_block.text:
content_details.citation_details[-1].length += len(
response.content_block.text
)
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
if has_native:
if first_block or has_native:
if content_details.has_citations():
content_details.delete_empty()
yield {"native": content_details}
content_details = ContentDetails()
content_details.add_citation_detail()
yield {"role": "assistant"}
has_native = False
has_content = False
first_block = False
elif isinstance(response.content_block, RedactedThinkingBlock):
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
@@ -250,15 +420,60 @@ async def _transform_stream(
"responses"
)
if has_native:
if content_details.has_citations():
content_details.delete_empty()
yield {"native": content_details}
content_details = ContentDetails()
content_details.add_citation_detail()
yield {"role": "assistant"}
has_native = False
has_content = False
first_block = False
yield {"native": response.content_block}
has_native = True
elif isinstance(response.content_block, ServerToolUseBlock):
current_tool_block = ServerToolUseBlockParam(
type="server_tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
)
current_tool_args = ""
elif isinstance(response.content_block, WebSearchToolResultBlock):
if content_details.has_citations():
content_details.delete_empty()
yield {"native": content_details}
content_details = ContentDetails()
content_details.add_citation_detail()
yield {
"role": "tool_result",
"tool_call_id": response.content_block.tool_use_id,
"tool_name": "web_search",
"tool_result": {
"type": "web_search_tool_result_error",
"error_code": response.content_block.content.error_code,
}
if isinstance(
response.content_block.content, WebSearchToolResultError
)
else {
"content": [
{
"type": "web_search_result",
"encrypted_content": block.encrypted_content,
"page_age": block.page_age,
"title": block.title,
"url": block.url,
}
for block in response.content_block.content
]
},
}
first_block = True
elif isinstance(response, RawContentBlockDeltaEvent):
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
content_details.citation_details[-1].length += len(response.delta.text)
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
yield {"thinking_content": response.delta.thinking}
@@ -271,6 +486,8 @@ async def _transform_stream(
)
}
has_native = True
elif isinstance(response.delta, CitationsDelta):
content_details.add_citation(response.delta.citation)
elif isinstance(response, RawContentBlockStopEvent):
if current_tool_block is not None:
tool_args = json.loads(current_tool_args) if current_tool_args else {}
@@ -281,6 +498,7 @@ async def _transform_stream(
id=current_tool_block["id"],
tool_name=current_tool_block["name"],
tool_args=tool_args,
external=current_tool_block["type"] == "server_tool_use",
)
]
}
@@ -290,6 +508,12 @@ async def _transform_stream(
chat_log.async_trace(_create_token_stats(input_usage, usage))
if response.delta.stop_reason == "refusal":
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if content_details.has_citations():
content_details.delete_empty()
yield {"native": content_details}
content_details = ContentDetails()
content_details.add_citation_detail()
def _create_token_stats(
@@ -337,21 +561,11 @@ class AnthropicBaseLLMEntity(Entity):
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: list[ToolParam] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model_args = MessageCreateParamsStreaming(
@@ -361,8 +575,8 @@ class AnthropicBaseLLMEntity(Entity):
system=system.content,
stream=True,
)
if tools:
model_args["tools"] = tools
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
if (
not model.startswith(tuple(NON_THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
@@ -376,6 +590,34 @@ class AnthropicBaseLLMEntity(Entity):
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
tools: list[ToolUnionParam] = []
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
for tool in chat_log.llm_api.tools
]
if options.get(CONF_WEB_SEARCH):
web_search = WebSearchTool20250305Param(
name="web_search",
type="web_search_20250305",
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
)
if options.get(CONF_WEB_SEARCH_USER_LOCATION):
web_search["user_location"] = {
"type": "approximate",
"city": options.get(CONF_WEB_SEARCH_CITY, ""),
"region": options.get(CONF_WEB_SEARCH_REGION, ""),
"country": options.get(CONF_WEB_SEARCH_COUNTRY, ""),
"timezone": options.get(CONF_WEB_SEARCH_TIMEZONE, ""),
}
tools.append(web_search)
if tools:
model_args["tools"] = tools
client = self.entry.runtime_data
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
try:

View File

@@ -35,11 +35,17 @@
"temperature": "Temperature",
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
"recommended": "Recommended model settings",
"thinking_budget_tokens": "Thinking budget"
"thinking_budget": "Thinking budget",
"web_search": "Enable web search",
"web_search_max_uses": "Maximum web searches",
"user_location": "Include home location"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template.",
"thinking_budget_tokens": "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_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.",
"web_search": "The web search tool gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff",
"web_search_max_uses": "Limit the number of searches performed per response",
"user_location": "Localize search results based on home location"
}
}
},
@@ -48,7 +54,8 @@
"entry_not_loaded": "Cannot add things while the configuration is disabled."
},
"error": {
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget."
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget.",
"web_search_unsupported_model": "Web search is not supported by the selected model. Please choose a compatible model or disable web search."
}
}
}

View File

@@ -5,14 +5,9 @@ from __future__ import annotations
import asyncio
import logging
from random import randrange
import sys
from typing import Any, cast
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -29,7 +24,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -43,6 +42,18 @@ from .const import (
SIGNAL_DISCONNECTED,
)
if sys.version_info < (3, 14):
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
else:
class DeviceListener:
"""Dummy class."""
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME_TV = "Apple TV"
@@ -53,31 +64,41 @@ BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
else:
AUTH_EXCEPTIONS = ()
CONNECTION_TIMEOUT_EXCEPTIONS = ()
DEVICE_EXCEPTIONS = ()
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Apple TV is not supported on Python 3.14. Please use Python 3.13."
)
manager = AppleTVManager(hass, entry)
if manager.is_on:

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -41,6 +41,8 @@ from .pipeline import (
async_setup_pipeline_store,
async_update_pipeline,
)
from .select import AssistPipelineSelect, VadSensitivitySelect
from .vad import VadSensitivity
from .websocket_api import async_register_websocket_api
__all__ = (
@@ -51,16 +53,18 @@ __all__ = (
"SAMPLE_CHANNELS",
"SAMPLE_RATE",
"SAMPLE_WIDTH",
"AssistPipelineSelect",
"AudioSettings",
"Pipeline",
"PipelineEvent",
"PipelineEventType",
"PipelineNotFound",
"VadSensitivity",
"VadSensitivitySelect",
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",
"async_pipeline_from_audio_stream",
"async_setup",
"async_update_pipeline",
)

View File

@@ -19,7 +19,14 @@ import wave
import hass_nabucasa
import voluptuous as vol
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
from homeassistant.components import (
conversation,
media_player,
stt,
tts,
wake_word,
websocket_api,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -130,7 +137,10 @@ SAVE_DELAY = 10
@callback
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
"""Filter out intents that are not local fallback."""
return result.intent.name in (intent.INTENT_GET_STATE)
return result.intent.name in (
intent.INTENT_GET_STATE,
media_player.INTENT_MEDIA_SEARCH_AND_PLAY,
)
@callback

View File

@@ -3,17 +3,17 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import namedtuple
from collections.abc import Awaitable, Callable, Coroutine
import functools
import logging
from typing import Any, cast
from typing import Any, NamedTuple
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aiohttp import ClientSession
from asusrouter import AsusRouter, AsusRouterError
from asusrouter.config import ARConfigKey
from asusrouter.modules.client import AsusClient
from asusrouter.modules.connection import ConnectionState
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
from asusrouter.tools.connection import get_cookie_jar
@@ -61,11 +61,27 @@ SENSORS_TYPE_RATES = "sensors_rates"
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
SENSORS_TYPE_UPTIME = "sensors_uptime"
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
class WrtDevice(NamedTuple):
"""WrtDevice structure."""
ip: str | None
name: str | None
conneted_to: str | None
_LOGGER = logging.getLogger(__name__)
type _FuncType[_T] = Callable[[_T], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]]
type _FuncType[_T] = Callable[
[_T],
Awaitable[
list[str]
| tuple[float | None, float | None]
| list[float]
| dict[str, float | str | None]
| dict[str, float]
],
]
type _ReturnFuncType[_T] = Callable[[_T], Coroutine[Any, Any, dict[str, Any]]]
@@ -80,7 +96,9 @@ def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
"""Run library methods and zip results or manage exceptions."""
@functools.wraps(func)
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]:
async def _wrapper(
self: _AsusWrtBridgeT,
) -> dict[str, float | str | None] | dict[str, float]:
try:
data = await func(self)
except exceptions as exc:
@@ -107,7 +125,9 @@ class AsusWrtBridge(ABC):
@staticmethod
def get_bridge(
hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
hass: HomeAssistant,
conf: dict[str, str | int],
options: dict[str, str | bool | int] | None = None,
) -> AsusWrtBridge:
"""Get Bridge instance."""
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
@@ -219,7 +239,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
@property
def is_connected(self) -> bool:
"""Get connected status."""
return cast(bool, self._api.is_connected)
return self._api.is_connected
async def async_connect(self) -> None:
"""Connect to the device."""
@@ -235,8 +255,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
async def async_disconnect(self) -> None:
"""Disconnect to the device."""
if self._api is not None and self._protocol == PROTOCOL_TELNET:
self._api.connection.disconnect()
await self._api.async_disconnect()
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
"""Get list of connected devices."""
@@ -307,22 +326,22 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
async def _get_bytes(self) -> Any:
async def _get_bytes(self) -> tuple[float | None, float | None]:
"""Fetch byte information from the router."""
return await self._api.async_get_bytes_total()
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_RATES)
async def _get_rates(self) -> Any:
async def _get_rates(self) -> tuple[float, float]:
"""Fetch rates information from the router."""
return await self._api.async_get_current_transfer_rates()
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_LOAD_AVG)
async def _get_load_avg(self) -> Any:
async def _get_load_avg(self) -> list[float]:
"""Fetch load average information from the router."""
return await self._api.async_get_loadavg()
@handle_errors_and_zip((OSError, ValueError), None)
async def _get_temperatures(self) -> Any:
async def _get_temperatures(self) -> dict[str, float]:
"""Fetch temperatures information from the router."""
return await self._api.async_get_temperature()
@@ -437,6 +456,7 @@ class AsusWrtHttpBridge(AsusWrtBridge):
if dev.connection is not None
and dev.description is not None
and dev.connection.ip_address is not None
and dev.state is ConnectionState.CONNECTED
}
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:

View File

@@ -175,12 +175,12 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def _async_check_connection(
self, user_input: dict[str, Any]
self, user_input: dict[str, str | int]
) -> tuple[str, str | None]:
"""Attempt to connect the AsusWrt router."""
api: AsusWrtBridge
host: str = user_input[CONF_HOST]
host = user_input[CONF_HOST]
protocol = user_input[CONF_PROTOCOL]
error: str | None = None

View File

@@ -10,8 +10,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AsusWrtConfigEntry
from .router import AsusWrtDevInfo, AsusWrtRouter
ATTR_LAST_TIME_REACHABLE = "last_time_reachable"
DEFAULT_DEVICE_NAME = "Unknown device"
@@ -58,8 +56,6 @@ def add_entities(
class AsusWrtDevice(ScannerEntity):
"""Representation of a AsusWrt device."""
_unrecorded_attributes = frozenset({ATTR_LAST_TIME_REACHABLE})
_attr_should_poll = False
def __init__(self, router: AsusWrtRouter, device: AsusWrtDevInfo) -> None:
@@ -97,11 +93,6 @@ class AsusWrtDevice(ScannerEntity):
def async_on_demand_update(self) -> None:
"""Update state."""
self._device = self._router.devices[self._device.mac]
self._attr_extra_state_attributes = {}
if self._device.last_activity:
self._attr_extra_state_attributes[ATTR_LAST_TIME_REACHABLE] = (
self._device.last_activity.isoformat(timespec="seconds")
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.21.0"]
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
}

View File

@@ -176,7 +176,7 @@ class AsusWrtRouter:
self._on_close: list[Callable] = []
self._options: dict[str, Any] = {
self._options: dict[str, str | bool | int] = {
CONF_DNSMASQ: DEFAULT_DNSMASQ,
CONF_INTERFACE: DEFAULT_INTERFACE,
CONF_REQUIRE_IP: True,
@@ -299,12 +299,10 @@ class AsusWrtRouter:
_LOGGER.warning("Reconnected to ASUS router %s", self.host)
self._connected_devices = len(wrt_devices)
consider_home: int = self._options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
)
track_unknown: bool = self._options.get(
CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN
consider_home = int(
self._options.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds())
)
track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN)
for device_mac, device in self._devices.items():
dev_info = wrt_devices.pop(device_mac, None)

View File

@@ -36,11 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
raise ConfigEntryAuthFailed("Migration to OAuth required")
session = async_create_august_clientsession(hass)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except ValueError as err:
raise ConfigEntryNotReady("OAuth implementation not available") from err
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:

View File

@@ -136,17 +136,22 @@ class WellKnownOAuthInfoView(HomeAssistantView):
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
url_prefix = ""
return self.json(
{
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
),
}
)
metadata = {
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
),
}
# Add issuer only when we have a valid base URL (RFC 8414 compliance)
if url_prefix:
metadata["issuer"] = url_prefix
return self.json(metadata)
class AuthProvidersView(HomeAssistantView):

View File

@@ -146,7 +146,7 @@
},
"state": {
"title": "Add a Bayesian sensor",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.",
"data": {
"name": "[%key:common::config_flow::data::name%]",

View File

@@ -57,6 +57,7 @@ from .api import (
_get_manager,
async_address_present,
async_ble_device_from_address,
async_clear_address_from_match_history,
async_current_scanners,
async_discovered_service_info,
async_get_advertisement_callback,
@@ -112,9 +113,9 @@ __all__ = [
"BluetoothServiceInfo",
"BluetoothServiceInfoBleak",
"HaBluetoothConnector",
"HomeAssistantRemoteScanner",
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",
"async_current_scanners",
"async_discovered_service_info",
"async_get_advertisement_callback",

View File

@@ -193,6 +193,20 @@ def async_rediscover_address(hass: HomeAssistant, address: str) -> None:
_get_manager(hass).async_rediscover_address(address)
@hass_callback
def async_clear_address_from_match_history(hass: HomeAssistant, address: str) -> None:
"""Clear an address from the integration matcher history.
This allows future advertisements from this address to trigger discovery
even if the advertisement content has changed but the service data UUIDs
remain the same.
Unlike async_rediscover_address, this does not immediately re-trigger
discovery with the current advertisement in history.
"""
_get_manager(hass).async_clear_address_from_match_history(address)
@hass_callback
def async_register_scanner(
hass: HomeAssistant,

View File

@@ -120,6 +120,19 @@ class HomeAssistantBluetoothManager(BluetoothManager):
if service_info := self._all_history.get(address):
self._async_trigger_matching_discovery(service_info)
@hass_callback
def async_clear_address_from_match_history(self, address: str) -> None:
"""Clear an address from the integration matcher history.
This allows future advertisements from this address to trigger discovery
even if the advertisement content has changed but the service data UUIDs
remain the same.
Unlike async_rediscover_address, this does not immediately re-trigger
discovery with the current advertisement in history.
"""
self._integration_matcher.async_clear_address(address)
def _discover_service_info(self, service_info: BluetoothServiceInfoBleak) -> None:
matched_domains = self._integration_matcher.match_domains(service_info)
if self._debug:

View File

@@ -68,12 +68,17 @@ class IntegrationMatchHistory:
manufacturer_data: bool
service_data: set[str]
service_uuids: set[str]
name: str
def seen_all_fields(
previous_match: IntegrationMatchHistory, advertisement_data: AdvertisementData
previous_match: IntegrationMatchHistory,
advertisement_data: AdvertisementData,
name: str,
) -> bool:
"""Return if we have seen all fields."""
if previous_match.name != name:
return False
if not previous_match.manufacturer_data and advertisement_data.manufacturer_data:
return False
if advertisement_data.service_data and (
@@ -122,10 +127,11 @@ class IntegrationMatcher:
device = service_info.device
advertisement_data = service_info.advertisement
connectable = service_info.connectable
name = service_info.name
matched = self._matched_connectable if connectable else self._matched
matched_domains: set[str] = set()
if (previous_match := matched.get(device.address)) and seen_all_fields(
previous_match, advertisement_data
previous_match, advertisement_data, name
):
# We have seen all fields so we can skip the rest of the matchers
return matched_domains
@@ -140,11 +146,13 @@ class IntegrationMatcher:
)
previous_match.service_data |= set(advertisement_data.service_data)
previous_match.service_uuids |= set(advertisement_data.service_uuids)
previous_match.name = name
else:
matched[device.address] = IntegrationMatchHistory(
manufacturer_data=bool(advertisement_data.manufacturer_data),
service_data=set(advertisement_data.service_data),
service_uuids=set(advertisement_data.service_uuids),
name=name,
)
return matched_domains

View File

@@ -3,15 +3,20 @@
from __future__ import annotations
from datetime import datetime
from functools import partial
import logging
from typing import Any
import caldav
from caldav.lib.error import DAVError
import requests
import voluptuous as vol
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA,
CalendarEntity,
CalendarEntityFeature,
CalendarEvent,
is_offset_reached,
)
@@ -23,6 +28,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import (
@@ -175,6 +181,8 @@ async def async_setup_entry(
class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarEntity):
"""A device for getting the next Task from a WebDav Calendar."""
_attr_supported_features = CalendarEntityFeature.CREATE_EVENT
def __init__(
self,
name: str | None,
@@ -203,6 +211,31 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
"""Get all events in a specific time frame."""
return await self.coordinator.async_get_events(hass, start_date, end_date)
async def async_create_event(self, **kwargs: Any) -> None:
"""Create a new event in the calendar."""
_LOGGER.debug("Event: %s", kwargs)
item_data: dict[str, Any] = {
"summary": kwargs["summary"],
"dtstart": kwargs["dtstart"],
"dtend": kwargs["dtend"],
}
if description := kwargs.get("description"):
item_data["description"] = description
if location := kwargs.get("location"):
item_data["location"] = location
if rrule := kwargs.get("rrule"):
item_data["rrule"] = rrule
_LOGGER.debug("ICS data %s", item_data)
try:
await self.hass.async_add_executor_job(
partial(self.coordinator.calendar.add_event, **item_data),
)
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@callback
def _handle_coordinator_update(self) -> None:
"""Update event data."""

View File

@@ -74,7 +74,10 @@ from .const import (
StreamType,
)
from .helper import get_camera_from_entity_id
from .img_util import scale_jpeg_camera_image
from .img_util import (
TurboJPEGSingleton, # noqa: F401
scale_jpeg_camera_image,
)
from .prefs import (
CameraPreferences,
DynamicStreamSettings, # noqa: F401

View File

@@ -31,7 +31,7 @@ async def async_setup_entry(
for location_id, location in coordinator.data["locations"].items()
]
async_add_entities(alarms, True)
async_add_entities(alarms)
class CanaryAlarm(

View File

@@ -68,8 +68,7 @@ async def async_setup_entry(
for location_id, location in coordinator.data["locations"].items()
for device in location.devices
if device.is_online
),
True,
)
)

View File

@@ -80,7 +80,7 @@ async def async_setup_entry(
if device_type.get("name") in sensor_type[4]
)
async_add_entities(sensors, True)
async_add_entities(sensors)
class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity):

View File

@@ -19,7 +19,7 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback

View File

@@ -12,7 +12,9 @@ from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.google_assistant.helpers import ( # pylint: disable=hass-component-root-import
AbstractConfig,
)
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.2.0"],
"requirements": ["hass-nabucasa==1.4.0"],
"single_config_entry": true
}

View File

@@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
from homeassistant.components import webhook
from homeassistant.components.google_assistant.http import (
from homeassistant.components.google_assistant.http import ( # pylint: disable=hass-component-root-import
async_get_users as async_get_google_assistant_users,
)
from homeassistant.core import HomeAssistant, callback

View File

@@ -38,6 +38,10 @@ TYPE_SPECIFY_COUNTRY = "specify_country_code"
_LOGGER = logging.getLogger(__name__)
DESCRIPTION_PLACEHOLDER = {
"register_link": "https://electricitymaps.com/free-tier",
}
class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
@@ -70,6 +74,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
data_schema=data_schema,
description_placeholders=DESCRIPTION_PLACEHOLDER,
)
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
@@ -179,4 +184,5 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=step_id,
data_schema=data_schema,
errors=errors,
description_placeholders=DESCRIPTION_PLACEHOLDER,
)

View File

@@ -18,7 +18,6 @@ rules:
status: todo
comment: |
The config flow misses data descriptions.
Remove URLs from data descriptions, they should be replaced with placeholders.
Make use of Electricity Maps zone keys in country code as dropdown.
Make use of location selector for coordinates.
dependency-transparency: done

View File

@@ -6,7 +6,7 @@
"location": "[%key:common::config_flow::data::location%]",
"api_key": "[%key:common::config_flow::data::access_token%]"
},
"description": "Visit https://electricitymaps.com/free-tier to request a token."
"description": "Visit the [Electricity Maps page]({register_link}) to request a token."
},
"coordinates": {
"data": {

View File

@@ -166,6 +166,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"account_name": self.reauth_entry.title,
"developer_url": "https://www.coinbase.com/developer-platform",
},
errors=errors,
)
@@ -195,6 +196,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"account_name": self.reauth_entry.title,
"developer_url": "https://www.coinbase.com/developer-platform",
},
errors=errors,
)

View File

@@ -11,7 +11,7 @@
},
"reauth_confirm": {
"title": "Update Coinbase API credentials",
"description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit https://www.coinbase.com/developer-platform to create new credentials for {account_name}.",
"description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit the [Developer Platform]({developer_url}) to create new credentials for {account_name}.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"api_token": "API secret"

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==1.1.1"]
"requirements": ["aiocomelit==1.1.2"]
}

View File

@@ -138,7 +138,7 @@ def new_device_listener(
data_type: str,
) -> Callable[[], None]:
"""Subscribe to coordinator updates to check for new devices."""
known_devices: set[int] = set()
known_devices: dict[str, list[int]] = {}
def _check_devices() -> None:
"""Check for new devices and call callback with any new monitors."""
@@ -147,8 +147,8 @@ def new_device_listener(
new_devices: list[DeviceType] = []
for _id in coordinator.data[data_type]:
if _id not in known_devices:
known_devices.add(_id)
if _id not in (id_list := known_devices.get(data_type, [])):
known_devices.update({data_type: [*id_list, _id]})
new_devices.append(coordinator.data[data_type][_id])
if new_devices:

View File

@@ -6,7 +6,9 @@ from typing import Any
import uuid
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.automation.config import async_validate_config_item
from homeassistant.components.automation.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.script.config import async_validate_config_item
from homeassistant.components.script.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.config import SCRIPT_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/control4",
"iot_class": "local_polling",
"loggers": ["pyControl4"],
"requirements": ["pyControl4==1.2.0"],
"requirements": ["pyControl4==1.5.0"],
"ssdp": [
{
"st": "c4:director"

View File

@@ -148,6 +148,15 @@ async def async_setup_entry(
source_type={dev_type}, idx=dev_id, name=name
)
# Skip rooms with no audio/video sources
if not sources:
_LOGGER.debug(
"Skipping room '%s' (ID: %s) - no audio/video sources found",
room.get("name"),
room_id,
)
continue
try:
hidden = room["roomHidden"]
entity_list.append(

View File

@@ -87,7 +87,6 @@ __all__ = [
"async_get_chat_log",
"async_get_result_from_chat_log",
"async_set_agent",
"async_setup",
"async_unset_agent",
]

View File

@@ -569,14 +569,17 @@ class ChatLog:
if llm_api:
prompt_parts.append(llm_api.api_prompt)
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.BASE_PROMPT,
llm_context.language,
user_name,
# Append current date and time to the prompt if the corresponding tool is not provided
llm_tools: list[llm.Tool] = llm_api.tools if llm_api else []
if not any(tool.name.endswith("GetDateTime") for tool in llm_tools):
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.DATE_TIME_PROMPT,
llm_context.language,
user_name,
)
)
)
if extra_system_prompt := (
# Take new system prompt if one was given

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
@@ -9,7 +10,7 @@ from pycync import Auth
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -39,7 +40,7 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
cync_auth: Auth
cync_auth: Auth = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -47,29 +48,14 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Attempt login with user credentials."""
errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login()
except AuthFailedError:
errors["base"] = "invalid_auth"
except TwoFactorRequiredError:
return await self.async_step_two_factor()
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
if not errors:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -81,12 +67,65 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Attempt login with the two factor auth code sent to the user."""
errors: dict[str, str] = {}
if user_input is None:
if user_input:
errors = await self._validate_credentials(user_input)
if not errors:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required and prompts for their Cync credentials."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()
if not errors:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_EMAIL: reauth_entry.title},
)
async def _validate_credentials(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Attempt to log in with user email and password, and return the error dict."""
errors: dict[str, str] = {}
if not self.cync_auth:
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
await self.cync_auth.login(user_input.get(CONF_TWO_FACTOR_CODE))
except TwoFactorRequiredError:
raise
except AuthFailedError:
errors["base"] = "invalid_auth"
except CyncError:
@@ -94,25 +133,29 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
return errors
async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
"""Create the Cync config entry using input user data."""
cync_user = self.cync_auth.user
await self.async_set_unique_id(str(cync_user.user_id))
self._abort_if_unique_id_configured()
config = {
config_data = {
CONF_USER_ID: cync_user.user_id,
CONF_AUTHORIZE_STRING: cync_user.authorize,
CONF_EXPIRES_AT: cync_user.expires_at,
CONF_ACCESS_TOKEN: cync_user.access_token,
CONF_REFRESH_TOKEN: cync_user.refresh_token,
}
return self.async_create_entry(title=user_email, data=config)
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
entry=self._get_reauth_entry(), title=user_email, data=config_data
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_email, data=config_data)

View File

@@ -37,7 +37,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: todo
# Gold

View File

@@ -18,6 +18,18 @@
"data_description": {
"two_factor_code": "The two-factor code sent to your Cync account's email"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Cync integration needs to re-authenticate for {email}",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::cync::config::step::user::data_description::email%]",
"password": "[%key:component::cync::config::step::user::data_description::password%]"
}
}
},
"error": {
@@ -26,7 +38,9 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "An incorrect user was provided by Cync for your email address, please consult your Cync app"
}
}
}

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
import datetime
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import
ManualAlarm,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
from homeassistant.core import HomeAssistant

View File

@@ -139,6 +139,7 @@ class DemoCover(CoverEntity):
self.async_write_ha_state()
return
self._is_opening = False
self._is_closing = True
self._listen_cover()
self._requested_closing = True
@@ -162,6 +163,7 @@ class DemoCover(CoverEntity):
return
self._is_opening = True
self._is_closing = False
self._listen_cover()
self._requested_closing = False
self.async_write_ha_state()
@@ -181,10 +183,14 @@ class DemoCover(CoverEntity):
if self._position == position:
return
self._is_closing = position < (self._position or 0)
self._is_opening = not self._is_closing
self._listen_cover()
self._requested_closing = (
self._position is not None and position < self._position
)
self.async_write_ha_state()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover til to a specific position."""

View File

@@ -196,7 +196,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._attr_name = name if name is not None else f"{source_entity} derivative"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
self._unit_template: str | None = None
if unit_of_measurement is None:
final_unit_prefix = "" if unit_prefix is None else unit_prefix
self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}"
@@ -217,6 +217,23 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
lambda *args: None
)
def _derive_and_set_attributes_from_state(self, source_state: State | None) -> None:
if self._unit_template and source_state:
original_unit = self._attr_native_unit_of_measurement
source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if source_unit is None else source_unit
)
if original_unit != self._attr_native_unit_of_measurement:
_LOGGER.debug(
"%s: Derivative sensor switched UoM from %s to %s, resetting state to 0",
self.entity_id,
original_unit,
self._attr_native_unit_of_measurement,
)
self._state_list = []
self._attr_native_value = round(Decimal(0), self._round_digits)
def _calc_derivative_from_state_list(self, current_time: datetime) -> Decimal:
def calculate_weight(start: datetime, end: datetime, now: datetime) -> float:
window_start = now - timedelta(seconds=self._time_window)
@@ -285,6 +302,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
except (InvalidOperation, TypeError):
self._attr_native_value = None
source_state = self.hass.states.get(self._sensor_source_id)
self._derive_and_set_attributes_from_state(source_state)
def schedule_max_sub_interval_exceeded(source_state: State | None) -> None:
"""Schedule calculation using the source state and max_sub_interval.
@@ -358,10 +378,18 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
_LOGGER.debug("%s: New state changed event: %s", self.entity_id, event.data)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
return
assert new_state
original_unit = self._attr_native_unit_of_measurement
self._derive_and_set_attributes_from_state(new_state)
if original_unit != self._attr_native_unit_of_measurement:
self.async_write_ha_state()
return
schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
if old_state is not None:
@@ -391,12 +419,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self.async_write_ha_state()
return
if self.native_unit_of_measurement is None:
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if unit is None else unit
)
self._prune_state_list(new_timestamp)
try:

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
"requirements": ["pydoods==1.0.2", "Pillow==12.0.0"]
}

View File

@@ -61,5 +61,8 @@ class EcobeeFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="authorize",
errors=errors,
description_placeholders={"pin": self._ecobee.pin},
description_placeholders={
"pin": self._ecobee.pin,
"auth_url": "https://www.ecobee.com/consumerportal/index.html",
},
)

View File

@@ -8,7 +8,7 @@
}
},
"authorize": {
"description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, select **Submit**."
"description": "Please authorize this app at {auth_url} with PIN code:\n\n{pin}\n\nThen, select **Submit**."
}
},
"error": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]
}

View File

@@ -8,8 +8,11 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from elevenlabs import AsyncElevenLabs, Model
from elevenlabs.core import ApiError
@@ -18,9 +19,14 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers.httpx_client import get_async_client
from .const import CONF_MODEL
from .const import CONF_MODEL, CONF_STT_MODEL
PLATFORMS: list[Platform] = [Platform.TTS]
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.STT,
Platform.TTS,
]
async def get_model_by_id(client: AsyncElevenLabs, model_id: str) -> Model | None:
@@ -39,6 +45,7 @@ class ElevenLabsData:
client: AsyncElevenLabs
model: Model
stt_model: str
type ElevenLabsConfigEntry = ConfigEntry[ElevenLabsData]
@@ -62,7 +69,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ElevenLabsConfigEntry) -
if model is None or (not model.languages):
raise ConfigEntryError("Model could not be resolved")
entry.runtime_data = ElevenLabsData(client=client, model=model)
entry.runtime_data = ElevenLabsData(
client=client, model=model, stt_model=entry.options[CONF_STT_MODEL]
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -78,3 +87,44 @@ async def update_listener(
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: ElevenLabsConfigEntry
) -> bool:
"""Migrate old config entry to new format."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
new_options = {**config_entry.options}
if config_entry.minor_version < 2:
# Add defaults only if theyre not already present
if "stt_auto_language" not in new_options:
new_options["stt_auto_language"] = False
if "stt_model" not in new_options:
new_options["stt_model"] = "scribe_v1"
hass.config_entries.async_update_entry(
config_entry,
options=new_options,
minor_version=2,
version=1,
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True # already up to date

View File

@@ -25,15 +25,20 @@ from .const import (
CONF_MODEL,
CONF_SIMILARITY,
CONF_STABILITY,
CONF_STT_AUTO_LANGUAGE,
CONF_STT_MODEL,
CONF_STYLE,
CONF_USE_SPEAKER_BOOST,
CONF_VOICE,
DEFAULT_MODEL,
DEFAULT_SIMILARITY,
DEFAULT_STABILITY,
DEFAULT_STT_AUTO_LANGUAGE,
DEFAULT_STT_MODEL,
DEFAULT_STYLE,
DEFAULT_TTS_MODEL,
DEFAULT_USE_SPEAKER_BOOST,
DOMAIN,
STT_MODELS,
)
USER_STEP_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
@@ -68,6 +73,7 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ElevenLabs text-to-speech."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -88,7 +94,12 @@ class ElevenLabsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title="ElevenLabs",
data=user_input,
options={CONF_MODEL: DEFAULT_MODEL, CONF_VOICE: list(voices)[0]},
options={
CONF_MODEL: DEFAULT_TTS_MODEL,
CONF_VOICE: list(voices)[0],
CONF_STT_MODEL: DEFAULT_STT_MODEL,
CONF_STT_AUTO_LANGUAGE: False,
},
)
return self.async_show_form(
step_id="user", data_schema=USER_STEP_SCHEMA, errors=errors
@@ -113,6 +124,9 @@ class ElevenLabsOptionsFlow(OptionsFlow):
self.models: dict[str, str] = {}
self.model: str | None = None
self.voice: str | None = None
self.stt_models: dict[str, str] = STT_MODELS
self.stt_model: str | None = None
self.auto_language: bool | None = None
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -126,6 +140,8 @@ class ElevenLabsOptionsFlow(OptionsFlow):
if user_input is not None:
self.model = user_input[CONF_MODEL]
self.voice = user_input[CONF_VOICE]
self.stt_model = user_input[CONF_STT_MODEL]
self.auto_language = user_input[CONF_STT_AUTO_LANGUAGE]
configure_voice = user_input.pop(CONF_CONFIGURE_VOICE)
if configure_voice:
return await self.async_step_voice_settings()
@@ -165,6 +181,22 @@ class ElevenLabsOptionsFlow(OptionsFlow):
]
)
),
vol.Required(
CONF_STT_MODEL,
): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(label=model_name, value=model_id)
for model_id, model_name in self.stt_models.items()
]
)
),
vol.Required(
CONF_STT_AUTO_LANGUAGE,
default=self.config_entry.options.get(
CONF_STT_AUTO_LANGUAGE, DEFAULT_STT_AUTO_LANGUAGE
),
): bool,
vol.Required(CONF_CONFIGURE_VOICE, default=False): bool,
}
),
@@ -179,6 +211,8 @@ class ElevenLabsOptionsFlow(OptionsFlow):
if user_input is not None:
user_input[CONF_MODEL] = self.model
user_input[CONF_VOICE] = self.voice
user_input[CONF_STT_MODEL] = self.stt_model
user_input[CONF_STT_AUTO_LANGUAGE] = self.auto_language
return self.async_create_entry(
title="ElevenLabs",
data=user_input,

View File

@@ -7,12 +7,123 @@ CONF_MODEL = "model"
CONF_CONFIGURE_VOICE = "configure_voice"
CONF_STABILITY = "stability"
CONF_SIMILARITY = "similarity"
CONF_STT_AUTO_LANGUAGE = "stt_auto_language"
CONF_STT_MODEL = "stt_model"
CONF_STYLE = "style"
CONF_USE_SPEAKER_BOOST = "use_speaker_boost"
DOMAIN = "elevenlabs"
DEFAULT_MODEL = "eleven_multilingual_v2"
DEFAULT_TTS_MODEL = "eleven_multilingual_v2"
DEFAULT_STABILITY = 0.5
DEFAULT_SIMILARITY = 0.75
DEFAULT_STT_AUTO_LANGUAGE = False
DEFAULT_STT_MODEL = "scribe_v1"
DEFAULT_STYLE = 0
DEFAULT_USE_SPEAKER_BOOST = True
STT_LANGUAGES = [
"af-ZA", # Afrikaans
"am-ET", # Amharic
"ar-SA", # Arabic
"hy-AM", # Armenian
"as-IN", # Assamese
"ast-ES", # Asturian
"az-AZ", # Azerbaijani
"be-BY", # Belarusian
"bn-IN", # Bengali
"bs-BA", # Bosnian
"bg-BG", # Bulgarian
"my-MM", # Burmese
"yue-HK", # Cantonese
"ca-ES", # Catalan
"ceb-PH", # Cebuano
"ny-MW", # Chichewa
"hr-HR", # Croatian
"cs-CZ", # Czech
"da-DK", # Danish
"nl-NL", # Dutch
"en-US", # English
"et-EE", # Estonian
"fil-PH", # Filipino
"fi-FI", # Finnish
"fr-FR", # French
"ff-SN", # Fulah
"gl-ES", # Galician
"lg-UG", # Ganda
"ka-GE", # Georgian
"de-DE", # German
"el-GR", # Greek
"gu-IN", # Gujarati
"ha-NG", # Hausa
"he-IL", # Hebrew
"hi-IN", # Hindi
"hu-HU", # Hungarian
"is-IS", # Icelandic
"ig-NG", # Igbo
"id-ID", # Indonesian
"ga-IE", # Irish
"it-IT", # Italian
"ja-JP", # Japanese
"jv-ID", # Javanese
"kea-CV", # Kabuverdianu
"kn-IN", # Kannada
"kk-KZ", # Kazakh
"km-KH", # Khmer
"ko-KR", # Korean
"ku-TR", # Kurdish
"ky-KG", # Kyrgyz
"lo-LA", # Lao
"lv-LV", # Latvian
"ln-CD", # Lingala
"lt-LT", # Lithuanian
"luo-KE", # Luo
"lb-LU", # Luxembourgish
"mk-MK", # Macedonian
"ms-MY", # Malay
"ml-IN", # Malayalam
"mt-MT", # Maltese
"zh-CN", # Mandarin Chinese
"mi-NZ", # Māori
"mr-IN", # Marathi
"mn-MN", # Mongolian
"ne-NP", # Nepali
"nso-ZA", # Northern Sotho
"no-NO", # Norwegian
"oc-FR", # Occitan
"or-IN", # Odia
"ps-AF", # Pashto
"fa-IR", # Persian
"pl-PL", # Polish
"pt-PT", # Portuguese
"pa-IN", # Punjabi
"ro-RO", # Romanian
"ru-RU", # Russian
"sr-RS", # Serbian
"sn-ZW", # Shona
"sd-PK", # Sindhi
"sk-SK", # Slovak
"sl-SI", # Slovenian
"so-SO", # Somali
"es-ES", # Spanish
"sw-KE", # Swahili
"sv-SE", # Swedish
"ta-IN", # Tamil
"tg-TJ", # Tajik
"te-IN", # Telugu
"th-TH", # Thai
"tr-TR", # Turkish
"uk-UA", # Ukrainian
"umb-AO", # Umbundu
"ur-PK", # Urdu
"uz-UZ", # Uzbek
"vi-VN", # Vietnamese
"cy-GB", # Welsh
"wo-SN", # Wolof
"xh-ZA", # Xhosa
"zu-ZA", # Zulu
]
STT_MODELS = {
"scribe_v1": "Scribe v1",
"scribe_v1_experimental": "Scribe v1 Experimental",
}

View File

@@ -21,11 +21,15 @@
"data": {
"voice": "Voice",
"model": "Model",
"stt_model": "Speech-to-Text Model",
"stt_auto_language": "Auto-detect language",
"configure_voice": "Configure advanced voice settings"
},
"data_description": {
"voice": "Voice to use for the TTS.",
"voice": "Voice to use for text-to-speech.",
"model": "ElevenLabs model to use. Please note that not all models support all languages equally well.",
"stt_model": "Speech-to-Text model to use.",
"stt_auto_language": "Automatically detect the spoken language for speech-to-text.",
"configure_voice": "Configure advanced voice settings. Find more information in the ElevenLabs documentation."
}
},
@@ -44,5 +48,17 @@
}
}
}
},
"entity": {
"tts": {
"elevenlabs_tts": {
"name": "Text-to-Speech"
}
},
"stt": {
"elevenlabs_stt": {
"name": "Speech-to-Text"
}
}
}
}

View File

@@ -0,0 +1,207 @@
"""Support for the ElevenLabs speech-to-text service."""
from __future__ import annotations
from collections.abc import AsyncIterable
from io import BytesIO
import logging
from elevenlabs import AsyncElevenLabs
from elevenlabs.core import ApiError
from elevenlabs.types import Model
from homeassistant.components import stt
from homeassistant.components.stt import (
AudioBitRates,
AudioChannels,
AudioCodecs,
AudioFormats,
AudioSampleRates,
SpeechMetadata,
SpeechResultState,
SpeechToTextEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ElevenLabsConfigEntry
from .const import (
CONF_STT_AUTO_LANGUAGE,
DEFAULT_STT_AUTO_LANGUAGE,
DOMAIN,
STT_LANGUAGES,
)
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 10
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ElevenLabsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ElevenLabs stt platform via config entry."""
client = config_entry.runtime_data.client
auto_detect = config_entry.options.get(
CONF_STT_AUTO_LANGUAGE, DEFAULT_STT_AUTO_LANGUAGE
)
async_add_entities(
[
ElevenLabsSTTEntity(
client,
config_entry.runtime_data.model,
config_entry.runtime_data.stt_model,
config_entry.entry_id,
auto_detect_language=auto_detect,
)
]
)
class ElevenLabsSTTEntity(SpeechToTextEntity):
"""The ElevenLabs STT API entity."""
_attr_has_entity_name = True
_attr_translation_key = "elevenlabs_stt"
def __init__(
self,
client: AsyncElevenLabs,
model: Model,
stt_model: str,
entry_id: str,
auto_detect_language: bool = False,
) -> None:
"""Init ElevenLabs TTS service."""
self._client = client
self._auto_detect_language = auto_detect_language
self._stt_model = stt_model
# Entity attributes
self._attr_unique_id = entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry_id)},
manufacturer="ElevenLabs",
model=model.name,
name="ElevenLabs",
entry_type=DeviceEntryType.SERVICE,
)
@property
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return STT_LANGUAGES
@property
def supported_formats(self) -> list[AudioFormats]:
"""Return a list of supported formats."""
return [AudioFormats.WAV, AudioFormats.OGG]
@property
def supported_codecs(self) -> list[AudioCodecs]:
"""Return a list of supported codecs."""
return [AudioCodecs.PCM, AudioCodecs.OPUS]
@property
def supported_bit_rates(self) -> list[AudioBitRates]:
"""Return a list of supported bit rates."""
return [AudioBitRates.BITRATE_16]
@property
def supported_sample_rates(self) -> list[AudioSampleRates]:
"""Return a list of supported sample rates."""
return [AudioSampleRates.SAMPLERATE_16000]
@property
def supported_channels(self) -> list[AudioChannels]:
"""Return a list of supported channels."""
return [
AudioChannels.CHANNEL_MONO,
AudioChannels.CHANNEL_STEREO,
]
async def async_process_audio_stream(
self, metadata: SpeechMetadata, stream: AsyncIterable[bytes]
) -> stt.SpeechResult:
"""Process an audio stream to STT service."""
_LOGGER.debug(
"Processing audio stream for STT: model=%s, language=%s, format=%s, codec=%s, sample_rate=%s, channels=%s, bit_rate=%s",
self._stt_model,
metadata.language,
metadata.format,
metadata.codec,
metadata.sample_rate,
metadata.channel,
metadata.bit_rate,
)
if self._auto_detect_language:
lang_code = None
else:
language = metadata.language
if language.lower() not in [lang.lower() for lang in STT_LANGUAGES]:
_LOGGER.warning("Unsupported language: %s", language)
return stt.SpeechResult(None, SpeechResultState.ERROR)
lang_code = language.split("-")[0]
raw_pcm_compatible = (
metadata.codec == AudioCodecs.PCM
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
and metadata.channel == AudioChannels.CHANNEL_MONO
and metadata.bit_rate == AudioBitRates.BITRATE_16
)
if raw_pcm_compatible:
file_format = "pcm_s16le_16"
elif metadata.codec == AudioCodecs.PCM:
_LOGGER.warning("PCM input does not meet expected raw format requirements")
return stt.SpeechResult(None, SpeechResultState.ERROR)
else:
file_format = "other"
audio = b""
async for chunk in stream:
audio += chunk
_LOGGER.debug("Finished reading audio stream, total size: %d bytes", len(audio))
if not audio:
_LOGGER.warning("No audio received in stream")
return stt.SpeechResult(None, SpeechResultState.ERROR)
lang_display = lang_code if lang_code else "auto-detected"
_LOGGER.debug(
"Transcribing audio (%s), format: %s, size: %d bytes",
lang_display,
file_format,
len(audio),
)
try:
response = await self._client.speech_to_text.convert(
file=BytesIO(audio),
file_format=file_format,
model_id=self._stt_model,
language_code=lang_code,
tag_audio_events=False,
num_speakers=1,
diarize=False,
)
except ApiError as exc:
_LOGGER.error("Error during processing of STT request: %s", exc)
return stt.SpeechResult(None, SpeechResultState.ERROR)
text = response.text or ""
detected_lang_code = response.language_code or "?"
detected_lang_prob = response.language_probability or "?"
_LOGGER.debug(
"Transcribed text is in language %s (probability %s): %s",
detected_lang_code,
detected_lang_prob,
text,
)
return stt.SpeechResult(text, SpeechResultState.SUCCESS)

View File

@@ -71,7 +71,6 @@ async def async_setup_entry(
voices,
default_voice_id,
config_entry.entry_id,
config_entry.title,
voice_settings,
)
]
@@ -83,6 +82,8 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
_attr_supported_options = [ATTR_VOICE, ATTR_MODEL]
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
_attr_translation_key = "elevenlabs_tts"
def __init__(
self,
@@ -91,7 +92,6 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
voices: list[ElevenLabsVoice],
default_voice_id: str,
entry_id: str,
title: str,
voice_settings: VoiceSettings,
) -> None:
"""Init ElevenLabs TTS service."""
@@ -112,11 +112,11 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
# Entity attributes
self._attr_unique_id = entry_id
self._attr_name = title
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry_id)},
manufacturer="ElevenLabs",
model=model.name,
name="ElevenLabs",
entry_type=DeviceEntryType.SERVICE,
)
self._attr_supported_languages = [

View File

@@ -16,7 +16,9 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import reset_detected
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
reset_detected,
)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
from homeassistant.core import (
HomeAssistant,

View File

@@ -38,6 +38,25 @@
},
"available_energy": {
"default": "mdi:battery-50"
},
"grid_status": {
"default": "mdi:transmission-tower",
"state": {
"off_grid": "mdi:transmission-tower-off",
"synchronizing": "mdi:sync-alert"
}
},
"mid_state": {
"default": "mdi:electric-switch-closed",
"state": {
"open": "mdi:electric-switch"
}
},
"admin_state": {
"default": "mdi:transmission-tower",
"state": {
"off_grid": "mdi:transmission-tower-off"
}
}
},
"switch": {

View File

@@ -824,6 +824,12 @@ class EnvoyCollarSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[EnvoyCollar], datetime.datetime | int | float | str]
# translations don't accept uppercase
ADMIN_STATE_MAP = {
"ENCMN_MDE_ON_GRID": "on_grid",
"ENCMN_MDE_OFF_GRID": "off_grid",
}
COLLAR_SENSORS = (
EnvoyCollarSensorEntityDescription(
key="temperature",
@@ -838,11 +844,21 @@ COLLAR_SENSORS = (
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda collar: dt_util.utc_from_timestamp(collar.last_report_date),
),
# grid_state does not seem to change when off-grid, but rather admin_state_str
EnvoyCollarSensorEntityDescription(
key="grid_state",
translation_key="grid_status",
value_fn=lambda collar: collar.grid_state,
),
# grid_status off-grid shows in admin_state rather than in grid_state
# map values as translations don't accept uppercase which these are
EnvoyCollarSensorEntityDescription(
key="admin_state_str",
translation_key="admin_state",
value_fn=lambda collar: ADMIN_STATE_MAP.get(
collar.admin_state_str, collar.admin_state_str
),
),
EnvoyCollarSensorEntityDescription(
key="mid_state",
translation_key="mid_state",

View File

@@ -409,10 +409,26 @@
"name": "Last report duration"
},
"grid_status": {
"name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]"
"name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]",
"state": {
"on_grid": "On grid",
"off_grid": "Off grid",
"synchronizing": "Synchronizing to grid"
}
},
"mid_state": {
"name": "MID state"
"name": "MID state",
"state": {
"open": "[%key:common::state::open%]",
"close": "[%key:common::state::closed%]"
}
},
"admin_state": {
"name": "Admin state",
"state": {
"on_grid": "[%key:component::enphase_envoy::entity::sensor::grid_status::state::on_grid%]",
"off_grid": "[%key:component::enphase_envoy::entity::sensor::grid_status::state::off_grid%]"
}
}
},
"switch": {

View File

@@ -47,11 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) ->
radar_coordinator = ECDataUpdateCoordinator(
hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL
)
try:
await radar_coordinator.async_config_entry_first_refresh()
except ConfigEntryNotReady:
errors = errors + 1
_LOGGER.warning("Unable to retrieve Environment Canada radar")
# Skip initial refresh for radar since the camera entity is disabled by default.
# The coordinator will fetch data when the entity is enabled.
aqhi_data = ECAirQuality(coordinates=(lat, lon))
aqhi_coordinator = ECDataUpdateCoordinator(
@@ -63,7 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) ->
errors = errors + 1
_LOGGER.warning("Unable to retrieve Environment Canada AQHI")
if errors == 3:
# Require at least one coordinator to succeed (weather or AQHI)
# Radar is optional since the camera entity is disabled by default
if errors >= 2:
raise ConfigEntryNotReady
config_entry.runtime_data = ECRuntimeData(

View File

@@ -59,6 +59,14 @@ class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECRadar]], Camera
self.content_type = "image/gif"
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# Trigger coordinator refresh when entity is enabled
# since radar coordinator skips initial refresh during setup
if not self.coordinator.last_update_success:
await self.coordinator.async_request_refresh()
def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:

View File

@@ -6,11 +6,18 @@ import xml.etree.ElementTree as ET
import aiohttp
from env_canada import ECWeather, ec_exc
from env_canada.ec_weather import get_ec_sites_list
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_STATION, CONF_TITLE, DOMAIN
@@ -25,14 +32,16 @@ async def validate_input(data):
lang = data.get(CONF_LANGUAGE).lower()
if station:
# When station is provided, use it and get the coordinates from ECWeather
weather_data = ECWeather(station_id=station, language=lang)
else:
weather_data = ECWeather(coordinates=(lat, lon), language=lang)
await weather_data.update()
if lat is None or lon is None:
await weather_data.update()
# Always use the station's coordinates, not the user-provided ones
lat = weather_data.lat
lon = weather_data.lon
else:
# When no station is provided, use coordinates to find nearest station
weather_data = ECWeather(coordinates=(lat, lon), language=lang)
await weather_data.update()
return {
CONF_TITLE: weather_data.metadata.location,
@@ -46,6 +55,13 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Environment Canada weather."""
VERSION = 1
_station_codes: list[dict[str, str]] | None = None
async def _get_station_codes(self) -> list[dict[str, str]]:
"""Get station codes, cached after first call."""
if self._station_codes is None:
self._station_codes = await get_ec_sites_list()
return self._station_codes
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -80,9 +96,21 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)
station_codes = await self._get_station_codes()
data_schema = vol.Schema(
{
vol.Optional(CONF_STATION): str,
vol.Optional(CONF_STATION): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=station["value"], label=station["label"]
)
for station in station_codes
],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.11.3"]
"requirements": ["env-canada==0.12.1"]
}

View File

@@ -3,11 +3,11 @@
"step": {
"user": {
"title": "Environment Canada: weather location and language",
"description": "Either a station ID or latitude/longitude must be specified. The default latitude/longitude used are the values configured in your Home Assistant installation. The closest weather station to the coordinates will be used if specifying coordinates. If a station code is used it must follow the format: PP/code, where PP is the two-letter province and code is the station ID. The list of station IDs can be found here: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weather information can be retrieved in either English or French.",
"description": "Select a weather station from the dropdown, or specify coordinates to use the closest station. The default coordinates are from your Home Assistant installation. Weather information can be retrieved in English or French.",
"data": {
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"station": "Weather station ID",
"station": "Weather station",
"language": "Weather information language"
}
}
@@ -16,7 +16,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"bad_station_id": "Station ID is invalid, missing, or not found in the station ID database",
"bad_station_id": "Station code is invalid, missing, or not found in the station code database",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"error_response": "Response from Environment Canada in error",
"too_many_attempts": "Connections to Environment Canada are rate limited; Try again in 60 seconds",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/epson",
"iot_class": "local_polling",
"loggers": ["epson_projector"],
"requirements": ["epson-projector==0.5.1"]
"requirements": ["epson-projector==0.6.0"]
}

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