Compare commits

..

521 Commits

Author SHA1 Message Date
jbouwh
4bb2c0b213 Remove integration domain 2025-10-17 13:45:53 +00:00
jbouwh
0307f0c781 Remove invalid import 2025-10-17 13:45:53 +00:00
jbouwh
81c4d8582a Rework with mixin - Light only 2025-10-17 13:45:53 +00:00
jbouwh
f66a03d6cb Automatically update the entity propery when a member created, updated or deleted 2025-10-17 13:45:53 +00:00
jbouwh
ff37570035 Apply light group icon to all MQTT light schemas 2025-10-17 13:45:53 +00:00
jbouwh
4f7e82ba76 Allow an MQTT entity to show as a group 2025-10-17 13:45:53 +00:00
jbouwh
87204bbfca Fix device tracker 2025-10-17 13:45:53 +00:00
jbouwh
69785a5361 Use platform name 2025-10-17 13:13:44 +00:00
jbouwh
c566950fb1 Fix device tracker state attrs 2025-10-17 13:13:44 +00:00
jbouwh
fd8e366a2f Also implement as default in base entity 2025-10-17 13:13:44 +00:00
jbouwh
ab658e05a6 Integrate with base entity component state attributes 2025-10-17 13:13:44 +00:00
jbouwh
49542e8302 Update docstr 2025-10-17 13:13:44 +00:00
jbouwh
bc8d7fc02e Move logic into Entity class 2025-10-17 13:13:44 +00:00
jbouwh
20a494e4f8 Use platform domain attribute 2025-10-17 13:13:44 +00:00
jbouwh
64ad83b1cd Fix typo 2025-10-17 13:13:44 +00:00
jbouwh
254a4de025 Follow up on code review 2025-10-17 13:13:44 +00:00
jbouwh
ec43e01d51 Implement mixin class and add feature to maintain included entities from unique IDs 2025-10-17 13:13:44 +00:00
jbouwh
4f25518671 Add included_entities attribute to base Entity class 2025-10-17 13:13:44 +00: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
G Johansson
929d76e236 Add validation for ObjectSelector (#153081) 2025-10-09 21:46:03 +02:00
Artur Pragacz
fe1ff083de Improve comments in the core config (#154096) 2025-10-09 21:08:16 +02:00
puddly
90c68f8ad0 Prevent reloading the ZHA integration while adapter firmware is being updated (#152626) 2025-10-09 21:00:02 +02:00
Shay Levy
6b79aa7738 Use Entity Description in Shelly cover platform (#154085) 2025-10-09 21:04:06 +03:00
Joost Lekkerkerker
f6fb4c8d5a Add unique id to nederlandse spoorwegen (#154013) 2025-10-09 19:00:47 +02:00
hanwg
a6e575ecfa Add diagnostics for Telegram bot (#154016) 2025-10-09 18:20:00 +02:00
Thomas D
85392ae167 Bump dependency for Volvo integration (#154084) 2025-10-09 18:15:22 +02:00
G Johansson
9d124be491 Remove deprecated set state directly in alarmcontrolpanel (#154038) 2025-10-09 18:06:13 +02:00
G Johansson
8bca3931ab Remove deprecated cover state constants (#154037) 2025-10-09 18:05:49 +02:00
Kevin McCormack
0367a01287 Enable strict typing for GitHub integration (#154048) 2025-10-09 17:50:24 +02:00
Manu
86e2c2f361 Add jet lag prevention event support to Sleep as Android integration (#154075) 2025-10-09 17:48:44 +02:00
Daniel De Sousa
335c8e50a2 Add switchbot_cloud climate TURN_OFF, TURN_ON support. (#154017) 2025-10-09 17:47:26 +02:00
eskerda
8152a9e5da Update Citybikes component with third-party library and fields (#151009) 2025-10-09 17:27:31 +02:00
Felipe Santos
250e562caf Fix devcontainer mistakenly using Python 3.14 (#154046) 2025-10-09 17:25:59 +02:00
Maciej Bieniek
a3b641e53d Bump brother to version 5.1.1 (#154080) 2025-10-09 17:00:41 +02:00
Shay Levy
135ea4c02e Fix Shelly orphaned entity removal logic (#154031) 2025-10-09 16:21:58 +03:00
Simone Chemelli
bc980c1212 Bump aioamazondevices to 6.4.0 (#154071) 2025-10-09 15:20:25 +02:00
Shay Levy
59ca88a7e8 Update Shelly block valve platform to use entity description (#154068) 2025-10-09 12:05:19 +03:00
Erik Montnemery
d45114cd11 Improve unit handling in recorder (#153941) 2025-10-09 10:29:42 +02:00
David Rapan
2eba650064 Mark Shelly docs-troubleshooting as done (#154066) 2025-10-09 11:22:32 +03:00
Christopher Fenner
de4adb8855 Make sensor names translatable in OpenWeatherMap integration (#153872) 2025-10-09 09:47:25 +02:00
Joost Lekkerkerker
1d86c03b02 Migrate Nederlandse Spoorwegen sensor to timestamp (#154011) 2025-10-09 09:25:11 +02:00
Klaas Schoute
77fb1036cc Bump autarco to v3.2.0 (#154039) 2025-10-09 01:13:57 +03:00
Shay Levy
b15b4e4888 Fix Shelly virtual components roles migration (#153987) 2025-10-09 00:06:32 +03:00
puddly
dddf6d5f1a Add new ZBT-2 VID:PID pair for discovery (#154036) 2025-10-08 15:59:49 -05:00
Abílio Costa
66fb5f4d95 Simplify firing of trigger actions (#152772)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-08 21:40:20 +01:00
hanwg
42a9d5d4e3 Add webhook tests for Telegram bot (#153998) 2025-10-08 20:58:15 +02:00
Maciej Bieniek
93fa162913 Update IQS for IMGW-PIB integration (#153870) 2025-10-08 20:30:05 +02:00
Maciej Bieniek
c432b1c8da Add entities for Shely cury component (#153918) 2025-10-08 20:26:29 +02:00
Artur Pragacz
00955b8e6a Fix empty llm api list in chat log (#153996) 2025-10-08 10:39:56 -05:00
Erik Montnemery
045b9d7f01 Correct homeassistant.helpers.trigger._trigger_action_wrapper (#153983) 2025-10-08 17:33:44 +02:00
Aaron Bach
438c4c7871 Limit SimpliSafe websocket connection attempts during startup (#153853)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-08 16:32:17 +02:00
Thomas D
abc360460c Add diagnostics to Volvo integration (#153997) 2025-10-08 16:25:33 +02:00
HarvsG
26437bb253 Adds ConfigFlow for London Underground (#152050)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-08 16:17:34 +02:00
epenet
56d953ac1e Use contants in climate set_temperature (#154008) 2025-10-08 16:15:01 +02:00
Joost Lekkerkerker
fe4eb8766d Don't mark ZHA coordinator as via_device with itself (#154004) 2025-10-08 16:05:54 +02:00
Mark Adkins
2d9f14c401 Add 3rd maintainer to sharkiq (#153961) 2025-10-08 15:17:52 +02:00
dependabot[bot]
7b6ccb07fd Bump github/codeql-action from 3.30.6 to 4.30.7 (#153979)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 13:42:25 +02:00
Shay Levy
2ba5728060 Enable Shelly binary input sensors by default (#154001) 2025-10-08 14:41:53 +03:00
epenet
b5f163cc85 Update Tuya fixture for product ID IAYz2WK1th0cMLmL (#154000) 2025-10-08 13:28:11 +02:00
Marc Mueller
65540a3e0b Update mypy dev to 1.19.0a4 (#153995) 2025-10-08 13:24:54 +02:00
Erwin Douna
cbf1b39edb Portainer add sensor platform (#153059)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-10-08 11:02:20 +02:00
G Johansson
142daf5e49 Call async_track_template_result with template without hass now fails (#153473)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-08 10:14:51 +02:00
Erik Montnemery
8bd0ff7cca Replace has_mean with mean_type in mill external statistics (#153985) 2025-10-08 09:52:07 +02:00
Erik Montnemery
ac676e12f6 Remove has_mean from suez_water external statistics (#153986) 2025-10-08 09:51:44 +02:00
Glenn Vandeuren (aka Iondependent)
c0ac3292cd FIx brightness always 100% when toggling the light (#153765)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-08 09:48:41 +02:00
Denis Shulyaka
80fd07c128 Add GPT-5 Pro and GPT-5 Codex support (#153936) 2025-10-08 09:48:07 +02:00
Michael Davie
3701d8859a Bump env-canada to 0.11.3 (#153967) 2025-10-08 09:40:55 +02:00
Jesse Hills
6dd26bae88 Bump aioesphomeapi to 41.13.0 (#153974) 2025-10-07 18:28:56 -10:00
Dave T
1a0abe296c Remove deprecated conductivity constants (#153942) 2025-10-07 23:20:36 +01:00
G Johansson
de6c61a4ab Bump psutil 7.1.0 (#153954) 2025-10-07 23:16:49 +01:00
Glenn Vandeuren (aka Iondependent)
33c677596e Update nhc to 0.6.1 (#153962) 2025-10-07 23:16:04 +01:00
peetersch
e9b4b8e99b Modbus Fix message_wait_milliseconds is no longer applied (#153709) 2025-10-07 23:38:05 +02:00
Maciej Bieniek
0525c04c42 Fix update interval for AccuWeather hourly forecast (#153957) 2025-10-07 23:25:04 +02:00
Shay Levy
d57b502551 Migrate Shelly virtual button platfrom unique IDs to include roles (#153865) 2025-10-07 23:01:30 +03:00
G Johansson
9fb708baf4 Bump holidays to 0.82 (#153952) 2025-10-07 23:00:38 +03:00
Josef Zweck
abdf24b7a0 Bump pylamarzocco to 2.1.2 (#153950) 2025-10-07 22:07:39 +03:00
TheJulianJES
29bfbd27bb Do not auto-set up ZHA zeroconf discoveries during onboarding (#153914) 2025-10-07 15:02:02 -04:00
starkillerOG
224553f8d9 Reverse Motion Blinds tilt direction (#149777)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-10-07 18:50:39 +01:00
mbo18
7c9f6a061f Add icons for SmartThings climate presets (#153929) 2025-10-07 19:15:15 +02:00
Marc Mueller
8e115d4685 Update pydantic to 2.12.0 (#153937) 2025-10-07 17:50:40 +01:00
Denis Shulyaka
00c189844f Bump openai to 2.2.0 (#153926) 2025-10-07 17:41:52 +01:00
Ståle Storø Hauknes
4587c286bb Add new sensors for Airthings Wave Enhance (#153879) 2025-10-07 17:44:30 +02:00
Artur Pragacz
b46097a7fc Move agent functionality from http (#153917) 2025-10-07 14:49:11 +02:00
mbo18
299cb6a2ff Change smart preset name to smart saver (#153916) 2025-10-07 14:11:00 +02:00
Erik Montnemery
1b7b91b328 Remove unused test fixtures from nintendo_parental (#153894) 2025-10-07 14:03:29 +02:00
Maciej Bieniek
01a1480ebd Use aioshelly methods for switches (#153746) 2025-10-07 13:28:58 +02:00
Jordan Harvey
26b8abb118 Bump pynintendoparental to 1.1.1 (#153874) 2025-10-07 13:28:08 +02:00
FMKaiba
53d1bbb530 Add support for gas detector status to SmartThings (#153831)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-07 12:56:53 +02:00
Tom Matheussen
a3ef55274e Add missing translation string for Satel Integra subentry type (#153905) 2025-10-07 12:18:51 +02:00
Joost Lekkerkerker
2034915457 Add fixture to SmartThings (#153902) 2025-10-07 12:13:12 +02:00
Joost Lekkerkerker
9e46d7964a Update SmartThings comments (#153903) 2025-10-07 11:46:44 +02:00
Maciej Bieniek
f9828a227b Bump aioshelly to version 13.12.0 (#153899) 2025-10-07 11:43:56 +02:00
Simone Chemelli
3341fa5f33 Code optimization for Comelit SimpleHome (#153029) 2025-10-07 10:31:44 +01:00
Christopher Fenner
e38ae47e76 Add language and location selector to OpenWeatherMap config flow (#153645)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-07 11:06:04 +02:00
Christopher Fenner
934c0e3c4c fix typo in icon assignment of AccuWeather integration (#153890) 2025-10-07 10:15:01 +02:00
Simone Chemelli
994a6ae7ed Fix restore cover state for Comelit SimpleHome (#153887) 2025-10-07 09:06:55 +02:00
Christopher Fenner
cdbe93c289 Set display precision for sensors in OpenWeatherMap integration (#153858) 2025-10-07 08:58:18 +02:00
Marc Mueller
56f90e4d96 Update pytest warnings filter (#153881) 2025-10-07 09:55:50 +03:00
TheJulianJES
34977abfec Remove Z-Wave JS voltage sensor overriding suggested precision (#153882) 2025-10-07 08:44:53 +02:00
Marc Mueller
5622103eb1 Fix nintendo_parental RuntimeWarning in tests (#153884) 2025-10-07 08:44:34 +02:00
Artur Pragacz
b9a1ab4a44 Clean up core references in conversation (#153880) 2025-10-07 00:46:47 +02:00
David Rapan
18997833c4 Shelly's power sensors naming paradigm standardization (#153822)
Signed-off-by: David Rapan <david@rapan.cz>
2025-10-07 01:32:44 +03:00
David Rapan
f99b194afc Shelly's current sensors naming paradigm standardization (#153827)
Signed-off-by: David Rapan <david@rapan.cz>
2025-10-07 01:32:25 +03:00
Dave T
566a347da7 Remove deprecated alarm panel constants (#153876) 2025-10-06 23:03:29 +01:00
Shay Levy
881306f6a4 Migrate Shelly virtual component unique IDs to include roles (#153844) 2025-10-07 00:50:47 +03:00
Marc Mueller
f63504af01 Update aiohttp to 3.13.0 (#153875) 2025-10-06 15:47:33 -05:00
derytive
d140b82a70 Add plate_count for Miele KM7575 (#153868) 2025-10-06 21:53:09 +02:00
Allen Porter
681211b1a5 Add Model Context Protocol support for OAuth scopes (#153150) 2025-10-06 15:32:42 -04:00
Joost Lekkerkerker
6c8b1f3618 Catch update exception in AirGradient (#153828) 2025-10-06 21:31:55 +02:00
Abílio Costa
d341065c34 Replace inner function with lambda in Idasen Desk (#153862) 2025-10-06 21:25:10 +02:00
G Johansson
81b1346080 Handle timeout errors gracefully in Nord Pool services (#153856) 2025-10-06 22:15:38 +03:00
J. Nick Koston
5613be3980 Bump yarl to 1.22.0 (#153860) 2025-10-06 13:43:37 -05:00
Alec
fbcf0eb94c Increase connect and configuration time for rfxtrx (#153834)
Increase the allowed time for connection and configuration. Some devices take a long time to respond to configuration changes and this time is counted for both network and configuration of the device.
2025-10-06 20:25:44 +02:00
Felipe Santos
1c7b9cc354 Avoid storing entities list in ONVIF binary_sensor and sensor (#153857) 2025-10-06 19:52:24 +02:00
William Scanlon
75e900606e Update water heater max temperature (#150970) 2025-10-06 19:21:21 +02:00
Norbert Rittel
7c665c53b5 Change translation of box in number to "Input field" for consistency (#153850) 2025-10-06 19:07:48 +02:00
Jordan Harvey
f72047eb02 Add new Nintendo Parental Controls integration (#145343)
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-06 18:36:46 +02:00
Marc Mueller
ade424c074 Update attrs to 25.4.0 (#153849) 2025-10-06 17:54:19 +02:00
Joost Lekkerkerker
5ad805de3c Add motion presets to SmartThings AC (#153830) 2025-10-06 17:29:33 +02:00
Simone Chemelli
ece77cf620 Fix PIN validation for Comelit SimpleHome (#153840) 2025-10-06 17:02:50 +02:00
Simone Chemelli
7eaa559056 Bump aiocomelit to 1.1.1 (#153843) 2025-10-06 16:57:40 +02:00
Allen Porter
08a9377373 Update the MCP Server API endpoint to mcp (#153845) 2025-10-06 16:56:45 +02:00
Jan Bouwhuis
a2837e6aee Add MQTT number subentry support (#153358) 2025-10-06 16:50:42 +02:00
Abílio Costa
fa03f6194d Remove log file write check (#153842) 2025-10-06 16:49:04 +02:00
Felipe Santos
d2851ea1df Deduplicate ONVIF sensor and binary sensor entity names (#153505) 2025-10-06 16:35:48 +02:00
Pavel Tarasov
72f8ac7857 Add BME680 sensor support for Altruist Insight (#153463) 2025-10-06 15:55:37 +02:00
NANI
77a267bc2f Updated VRM client and accounted for missing forecasts (#153464) 2025-10-06 15:37:49 +02:00
Arie Catsman
ad238daadc Enphase_envoy to use alternate data source for current transformers (#153621) 2025-10-06 15:33:25 +02:00
Øyvind Matheson Wergeland
42370ba203 Synology DSM: Don't reinitialize API during configuration (#153739) 2025-10-06 15:25:10 +02:00
Christopher Fenner
d9691c2a3b Add sensor for hydraulic separator temperature in ViCare integration (#153696) 2025-10-06 15:15:51 +02:00
Michael
66cca981a9 Expose climate current temp as dedicated sensor in FRITZ!SmartHome (#153558) 2025-10-06 15:00:29 +02:00
Ståle Storø Hauknes
9640ebb593 Add support for Wave Enhance and Corentium Home 2 in Airthings BLE integration (#153780) 2025-10-06 14:59:21 +02:00
Joost Lekkerkerker
645f32fd65 Bump pySmartThings to 3.3.1 (#153826) 2025-10-06 14:57:52 +02:00
hanwg
cb6e65f972 Refactor Telegram bot entity (#153609)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-06 14:57:46 +02:00
cdnninja
425bdc0ba6 Vesync add oscillation to fan (#153297) 2025-10-06 14:50:11 +02:00
cdnninja
c36341e51f vesync correct fan set modes (#153761) 2025-10-06 14:49:05 +02:00
Robert Resch
553d896899 Add Ecovacs active map select entity (#153748)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 14:42:01 +02:00
Åke Strandberg
ac79b3072e Use customized miele device name if set (#153835) 2025-10-06 14:40:57 +02:00
Andrew Jackson
c0aa9bfd4b Update Mealie quality scale to platinum (#153810) 2025-10-06 12:14:05 +02:00
epenet
e97100028d Add new test fixture for Tuya cl category (#153800) 2025-10-06 11:51:10 +02:00
tronikos
da89617432 Google Assistant SDK: improve init tests (#153795) 2025-10-06 11:49:06 +02:00
dependabot[bot]
e6203dffd3 Bump actions/stale from 10.0.0 to 10.1.0 (#153799)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 11:45:39 +02:00
Josef Zweck
c13cfe9c37 Re-add AGENTS.md as symlink (#153804) 2025-10-06 11:45:21 +02:00
Ludovic BOUÉ
2447df9341 Add Matter speaker mute toggle (#150104)
Add Matter speaker mute toggle functionality:
- OnOff attribute == True state means volume is on, so HA should show mute switch as off
- OnOff attribute == False means volume is off (muted), so HA should show mute switch as on
2025-10-06 11:09:59 +02:00
Erik Montnemery
1c1fbe0ec1 Log when failing to remove foreign key in recorder EventIDPostMigration (#153812) 2025-10-06 10:58:47 +02:00
Erik Montnemery
4a6d2017fd Fix stale docstring in recorder (#153811) 2025-10-06 10:58:24 +02:00
Simone Chemelli
b4997a52df Remove stale entities from Alexa Devices (#153759) 2025-10-06 10:57:21 +02:00
Joris Pelgröm
464dec1dcb Update LetPot integration quality scale to silver (#153783) 2025-10-06 10:28:43 +02:00
tronikos
85506ac78a Google Assistant SDK: use setup_credentials in setup_integration (#153793) 2025-10-06 10:23:44 +02:00
Marc Mueller
6d97355b42 Update raspyrfm-client to 1.2.9 (#153789) 2025-10-06 10:20:15 +02:00
Ludovic BOUÉ
f9e75c616a Use TEMPERATURE_SCALING_FACTOR for Matter sensors (#153807) 2025-10-06 10:15:37 +02:00
tronikos
a821d02dfb Translate reauthentication error message in Google Assistant SDK (#153797)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-06 01:10:40 -07:00
epenet
e05169c7a4 Fix Tuya cover position when only control is available (#153803) 2025-10-06 08:50:38 +02:00
tronikos
1cc3431529 Fix missing google_assistant_sdk.send_text_command (#153735) 2025-10-05 23:46:03 -07:00
Maciej Bieniek
4ba765f265 Add Shelly Wall Display XL to the list of devices without firmware changelog (#153781) 2025-10-06 06:52:15 +02:00
Paulus Schoutsen
50a7af4179 Handle ESPHome discoveries with uninitialized Z-Wave antennas (#153790) 2025-10-05 23:06:10 -04:00
Allen Porter
e0a2116e88 Update MCP server to support the newer HTTP protocol (#153779)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-10-05 21:16:53 -04:00
Aidan Timson
d8e1ed5f4a Fix power device classes for system bridge (#153201) 2025-10-05 23:52:35 +02:00
Denis Shulyaka
f1b8e8a963 Ollama thinking content (#150393) 2025-10-05 17:33:45 -04:00
Marc Mueller
9a9fd44c62 Use yaml anchors in ci workflow (#152586) 2025-10-05 23:21:38 +02:00
G Johansson
bc3fe7a18e Use automatic reload options flow in min_max (#153143) 2025-10-05 23:17:37 +02:00
G Johansson
19f3559345 Remove previously deprecated template attach function (#153370) 2025-10-05 23:16:57 +02:00
Jan Bouwhuis
fad0e23797 Allow to set the manufacturer in a MQTT device subentry setup (#153747) 2025-10-05 23:15:00 +02:00
Erik Montnemery
7f931e4d70 Add device class filter to hydrawise services (#153249) 2025-10-05 23:14:12 +02:00
Erik Montnemery
a04835629b Make hassfest fail on services with device filter on targets (#152794) 2025-10-05 23:13:33 +02:00
Andrew Jackson
78cd80746d Bump aiomealie to 1.0.0, update min Mealie instance version to v2. (#153203) 2025-10-05 23:12:05 +02:00
G Johansson
9ac93920d8 Cleanup process_fds addition in systemmonitor (#153568) 2025-10-05 22:41:19 +02:00
G Johansson
1818fce1ae Validating schema outside the event loop will now fail (#153472) 2025-10-05 22:37:14 +02:00
Erik Montnemery
f524edc4b9 Add pytest command line option to drop recorder db before test (#153527) 2025-10-05 22:36:24 +02:00
Paulus Schoutsen
19f990ed31 ESPHome to set Z-Wave discovery as next_flow (#153706) 2025-10-05 22:33:12 +02:00
David Rapan
5d83c82b81 Shelly's energy sensors naming paradigm standardization (#153729) 2025-10-05 22:32:39 +02:00
Fredrik Erlandsson
d63d154457 Daikin increase timeout (#153722)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-05 22:18:31 +02:00
Josef Zweck
933b15ce36 Revert "AGENTS.md" (#153777) 2025-10-05 22:05:04 +02:00
Denis Shulyaka
6ec7b63ebe Add support for Anthropic Claude Sonnet 4.5 (#153769) 2025-10-05 21:29:24 +02:00
Denis Shulyaka
26bfbc55e9 Bump anthropic to 0.69.0 (#153764) 2025-10-05 20:59:50 +02:00
Simone Chemelli
d75ca0f5f3 Bump aioamazondevices to 6.2.9 (#153756) 2025-10-05 20:59:02 +02:00
Sander Jochems
fed8f137e9 Upgrade python-melcloud to 0.1.2 (#153742) 2025-10-05 19:49:22 +02:00
Josef Zweck
f44d65e023 Migrate tolo to entry.runtime_data (#153744) 2025-10-05 18:43:37 +02:00
Christopher Fenner
a270bd76de Add sensors for battery charge amount to ViCare integration (#153631)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-10-05 18:34:45 +02:00
Christopher Fenner
9209e419ec Change style for critical number entities in ViCare integration (#153634) 2025-10-05 16:36:42 +02:00
Ståle Storø Hauknes
98f8f15e90 Fix crash when setting up Airthings BLE device (#153510) 2025-10-05 09:18:47 -05:00
Denis Shulyaka
b2a2868afd AGENTS.md (#153680) 2025-10-05 15:51:46 +02:00
J. Nick Koston
0d4737d360 Bump aiohomekit to 3.2.20 (#153750) 2025-10-05 15:49:55 +02:00
Ståle Storø Hauknes
2b370a0eca Use full serial number when adding an Airthings device (#153499) 2025-10-05 08:23:02 -05:00
Ståle Storø Hauknes
618fe81207 Check if firmware is outdated when adding an Airthings BLE device (#153559) 2025-10-05 14:49:34 +02:00
Maciej Bieniek
c0fe4861f9 Align Shelly presencezone entity to the new API/firmware (#153737) 2025-10-05 15:36:57 +03:00
Simone Chemelli
dfd33fdab1 Fix sensors availability check for Alexa Devices (#153743) 2025-10-05 14:26:16 +02:00
Josef Zweck
cceee05c15 Fix lamarzocco brewing start time sensor availability (#153732) 2025-10-05 13:04:28 +02:00
Manu
f560d2a05e Update suggested display precision for ntfy attachment size to 2 (#153741) 2025-10-05 13:03:55 +02:00
Ville Skyttä
3601cff88e Upgrade upcloud-api to 2.9.0 (#153727) 2025-10-05 13:58:35 +03:00
Maciej Bieniek
ca5c0a759f Remove Shelly presencezone component from VIRTUAL_COMPONENTS tuple (#153740) 2025-10-05 13:46:42 +03:00
Tom
6f9e6909ce Bump airOS to 0.5.5 using formdata for v6 firmware (#153736) 2025-10-05 12:43:43 +02:00
Manu
ccf563437b Bump aiontfy to v0.6.1 (#153738) 2025-10-05 12:34:18 +02:00
Josef Zweck
78e97428fd Add debouncer to acaia (#153725) 2025-10-05 12:31:45 +02:00
Denis Shulyaka
8b4c730993 Gemini: Use default model instead of recommended where applicable (#153676) 2025-10-05 02:59:51 -07:00
Fredrik Erlandsson
0a071a13e2 Version bump pydaikin to 2.17.1 (#153726) 2025-10-05 11:12:10 +02:00
Shay Levy
ab80991eac Add Shelly support for climate entities (#153450)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-05 11:53:52 +03:00
Erwin Douna
ee7262efb4 Portainer add button platform (#153063) 2025-10-05 10:36:52 +02:00
Fredrik Erlandsson
ea5a52cdc8 Version bump pydaikin to 2.17.0 (#153718) 2025-10-05 10:49:14 +03:00
tronikos
31fe0322ab Clarify description for media player entity in Google Assistant SDK (#153715) 2025-10-05 09:47:46 +02:00
tronikos
e8e0eabb99 Double max retries in Google Drive (#153717) 2025-10-05 10:35:50 +03:00
tronikos
1629dad1a8 Bump opower to 0.15.6 (#153714) 2025-10-05 09:25:45 +02:00
Shay Levy
d9baad530a Shelly code quality and cleanup (#153692) 2025-10-05 09:14:13 +02:00
cdnninja
4a1d00e59a Bump pyvesync to 3.1.0 (#153693) 2025-10-05 09:56:25 +03:00
Daniel Hjelseth Høyer
437e4e027c Bump Mill library (#153683)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-10-05 09:55:48 +03:00
J. Nick Koston
3726f7eca9 Bump zeroconf to 0.148.0 (#153704) 2025-10-04 21:57:00 -05:00
Marc Mueller
c943cf515c Add zeroconf to hassfest version requirements (#153703) 2025-10-05 02:55:35 +02:00
Christopher Fenner
3b0c2a7e56 Fix ViCare pressure sensors missing unit of measurement (#153691) 2025-10-05 02:14:32 +02:00
Christopher Fenner
6ebaa9cd1d Bump PyViCare to 2.52.0 (#153629) 2025-10-05 02:05:53 +02:00
J. Nick Koston
f81c32f6ea Bump aioesphomeapi to 41.12.0 (#153698) 2025-10-04 18:55:36 -05:00
J. Nick Koston
c0cd7a1a62 Bump propcache to 0.4.0 (#153694) 2025-10-04 18:03:53 -05:00
1178 changed files with 70401 additions and 10774 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"

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -17,7 +17,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -57,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

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

View File

@@ -221,6 +221,7 @@ homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
homeassistant.components.github.*
homeassistant.components.glances.*
homeassistant.components.go2rtc.*
homeassistant.components.goalzero.*

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
.github/copilot-instructions.md

20
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,6 +1069,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/nilu/ @hfurubotten
/homeassistant/components/nina/ @DeerMaximum
/tests/components/nina/ @DeerMaximum
/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
@@ -1133,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
@@ -1411,8 +1419,8 @@ build.json @home-assistant/supervisor
/tests/components/sfr_box/ @epenet
/homeassistant/components/sftp_storage/ @maretodoric
/tests/components/sftp_storage/ @maretodoric
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
/tests/components/sharkiq/ @JeffResc @funkybunch
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
/tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
/homeassistant/components/shell_command/ @home-assistant/core
/tests/components/shell_command/ @home-assistant/core
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
@@ -1477,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

@@ -34,9 +34,11 @@ WORKDIR /usr/src
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN uv python install 3.13.2
USER vscode
COPY .python-version ./
RUN uv python install
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

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

@@ -635,14 +635,6 @@ async def async_enable_logging(
err_log_path = os.path.abspath(log_file)
if err_log_path:
err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
@@ -652,8 +644,6 @@ async def async_enable_logging(
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
async_activate_log_queue_handler(hass)

View File

@@ -12,11 +12,13 @@ from homeassistant.components.bluetooth import async_get_scanner
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_IS_NEW_STYLE_SCALE
SCAN_INTERVAL = timedelta(seconds=15)
UPDATE_DEBOUNCE_TIME = 0.2
_LOGGER = logging.getLogger(__name__)
@@ -38,11 +40,19 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
config_entry=entry,
)
debouncer = Debouncer(
hass=hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=self.async_update_listeners,
)
self._scale = AcaiaScale(
address_or_ble_device=entry.data[CONF_ADDRESS],
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
notify_callback=debouncer.async_schedule_call,
scanner=async_get_scanner(hass),
)

View File

@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
}
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)

View File

@@ -1,6 +1,9 @@
{
"entity": {
"sensor": {
"air_quality": {
"default": "mdi:air-filter"
},
"cloud_ceiling": {
"default": "mdi:weather-fog"
},
@@ -34,9 +37,6 @@
"thunderstorm_probability_night": {
"default": "mdi:weather-lightning"
},
"translation_key": {
"default": "mdi:air-filter"
},
"tree_pollen": {
"default": "mdi:tree-outline"
},

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

@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final, final
from typing import Any, Final, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
@@ -133,9 +133,9 @@ class AirQualityEntity(Entity):
@final
@property
def state_attributes(self) -> dict[str, str | int | float]:
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
data: dict[str, str | int | float] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
for prop, attr in PROP_TO_ATTR.items():
if (value := getattr(self, prop)) is not None:

View File

@@ -1,7 +1,9 @@
"""Airgradient Update platform."""
from datetime import timedelta
import logging
from airgradient import AirGradientConnectionError
from propcache.api import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
PARALLEL_UPDATES = 1
SCAN_INTERVAL = timedelta(hours=1)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
_server_unreachable_logged = False
def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize the entity."""
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Return the installed version of the entity."""
return self.coordinator.data.measures.firmware_version
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._attr_available
async def async_update(self) -> None:
"""Update the entity."""
try:
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
)
except AirGradientConnectionError:
self._attr_latest_version = None
self._attr_available = False
if not self._server_unreachable_logged:
_LOGGER.error(
"Unable to connect to AirGradient server to check for updates"
)
self._server_unreachable_logged = True
else:
self._server_unreachable_logged = False
self._attr_available = True

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
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.4"]
"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

@@ -6,8 +6,13 @@ import dataclasses
import logging
from typing import Any
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from airthings_ble import (
AirthingsBluetoothDeviceData,
AirthingsDevice,
UnsupportedDeviceError,
)
from bleak import BleakError
from habluetooth import BluetoothServiceInfoBleak
import voluptuous as vol
from homeassistant.components import bluetooth
@@ -27,6 +32,7 @@ SERVICE_UUIDS = [
"b42e4a8e-ade7-11e4-89d3-123b93f75cba",
"b42e1c08-ade7-11e4-89d3-123b93f75cba",
"b42e3882-ade7-11e4-89d3-123b93f75cba",
"b42e90a2-ade7-11e4-89d3-123b93f75cba",
]
@@ -37,6 +43,7 @@ class Discovery:
name: str
discovery_info: BluetoothServiceInfo
device: AirthingsDevice
data: AirthingsBluetoothDeviceData
def get_name(device: AirthingsDevice) -> str:
@@ -44,7 +51,7 @@ def get_name(device: AirthingsDevice) -> str:
name = device.friendly_name()
if identifier := device.identifier:
name += f" ({identifier})"
name += f" ({device.model.value}{identifier})"
return name
@@ -62,8 +69,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_device: Discovery | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def _get_device_data(
self, discovery_info: BluetoothServiceInfo
async def _get_device(
self, data: AirthingsBluetoothDeviceData, discovery_info: BluetoothServiceInfo
) -> AirthingsDevice:
ble_device = bluetooth.async_ble_device_from_address(
self.hass, discovery_info.address
@@ -72,10 +79,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("no ble_device in _get_device_data")
raise AirthingsDeviceUpdateError("No ble_device")
airthings = AirthingsBluetoothDeviceData(_LOGGER)
try:
data = await airthings.update_device(ble_device)
device = await data.update_device(ble_device)
except BleakError as err:
_LOGGER.error(
"Error connecting to and getting data from %s: %s",
@@ -83,12 +88,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
err,
)
raise AirthingsDeviceUpdateError("Failed getting device data") from err
except UnsupportedDeviceError:
_LOGGER.debug("Skipping unsupported device: %s", discovery_info.name)
raise
except Exception as err:
_LOGGER.error(
"Unknown error occurred from %s: %s", discovery_info.address, err
)
raise
return data
return device
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
@@ -98,17 +106,21 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data=data, discovery_info=discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
except UnsupportedDeviceError:
return self.async_abort(reason="unsupported_device")
except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self.context["title_placeholders"] = {"name": name}
self._discovered_device = Discovery(name, discovery_info, device)
self._discovered_device = Discovery(name, discovery_info, device, data=data)
return await self.async_step_bluetooth_confirm()
@@ -117,6 +129,12 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
if (
self._discovered_device is not None
and self._discovered_device.device.firmware.need_firmware_upgrade
):
return self.async_abort(reason="firmware_upgrade_required")
return self.async_create_entry(
title=self.context["title_placeholders"]["name"], data={}
)
@@ -137,6 +155,9 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
discovery = self._discovered_devices[address]
if discovery.device.firmware.need_firmware_upgrade:
return self.async_abort(reason="firmware_upgrade_required")
self.context["title_placeholders"] = {
"name": discovery.name,
}
@@ -146,26 +167,47 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=discovery.name, data={})
current_addresses = self._async_current_ids(include_ignore=False)
devices: list[BluetoothServiceInfoBleak] = []
for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
if MFCT_ID not in discovery_info.manufacturer_data:
continue
if not any(uuid in SERVICE_UUIDS for uuid in discovery_info.service_uuids):
_LOGGER.debug(
"Skipping unsupported device: %s (%s)", discovery_info.name, address
)
continue
devices.append(discovery_info)
for discovery_info in devices:
address = discovery_info.address
data = AirthingsBluetoothDeviceData(logger=_LOGGER)
try:
device = await self._get_device_data(discovery_info)
device = await self._get_device(data, discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
_LOGGER.error(
"Error connecting to and getting data from %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except UnsupportedDeviceError:
_LOGGER.debug(
"Skipping unsupported device: %s (%s)",
discovery_info.name,
discovery_info.address,
)
continue
except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown")
name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device)
_LOGGER.debug("Discovered Airthings device: %s (%s)", name, address)
self._discovered_devices[address] = Discovery(
name, discovery_info, device, data
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")

View File

@@ -17,6 +17,10 @@
{
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e90a2-ade7-11e4-89d3-123b93f75cba"
}
],
"codeowners": ["@vincegio", "@LaStrada"],

View File

@@ -16,10 +16,12 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -112,6 +114,21 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"noise": SensorEntityDescription(
key="noise",
translation_key="ambient_noise",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
}
PARALLEL_UPDATES = 0

View File

@@ -20,6 +20,8 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"unsupported_device": "Unsupported device",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
@@ -39,6 +41,9 @@
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
},
"ambient_noise": {
"name": "Ambient noise"
}
}
}

View File

@@ -2,10 +2,9 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any, Final, final
from typing import Any, Final, final
from propcache.api import cached_property
import voluptuous as vol
@@ -28,8 +27,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import make_entity_service_schema
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
@@ -149,68 +146,11 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
)
_alarm_control_panel_option_default_code: str | None = None
__alarm_legacy_state: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
# Integrations should use the 'alarm_state' property instead of
# setting the state directly.
cls.__alarm_legacy_state = True
def __setattr__(self, name: str, value: Any, /) -> None:
"""Set attribute.
Deprecation warning if setting '_attr_state' directly
unless already reported.
"""
if name == "_attr_state":
self._report_deprecated_alarm_state_handling()
return super().__setattr__(name, value)
@callback
def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)
if self.__alarm_legacy_state:
self._report_deprecated_alarm_state_handling()
@callback
def _report_deprecated_alarm_state_handling(self) -> None:
"""Report on deprecated handling of alarm state.
Integrations should implement alarm_state instead of using state directly.
"""
report_usage(
"is setting state directly."
f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'"
" property and return its state using the AlarmControlPanelState enum",
core_integration_behavior=ReportBehavior.ERROR,
custom_integration_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2025.11",
integration_domain=self.platform.platform_name if self.platform else None,
exclude_integrations={DOMAIN},
)
@final
@property
def state(self) -> str | None:
"""Return the current state."""
if (alarm_state := self.alarm_state) is not None:
return alarm_state
if self._attr_state is not None:
# Backwards compatibility for integrations that set state directly
# Should be removed in 2025.11
if TYPE_CHECKING:
assert isinstance(self._attr_state, str)
return self._attr_state
return None
return self.alarm_state
@cached_property
def alarm_state(self) -> AlarmControlPanelState | None:
@@ -361,11 +301,12 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
return {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
ATTR_CODE_ARM_REQUIRED: self.code_arm_required,
}
data: dict[str, Any] = self.generate_entity_state_attributes()
data[ATTR_CODE_FORMAT] = self.code_format
data[ATTR_CHANGED_BY] = self.changed_by
data[ATTR_CODE_ARM_REQUIRED] = self.code_arm_required
return data
async def async_internal_added_to_hass(self) -> None:
"""Call when the alarm control panel entity is added to hass."""

View File

@@ -1472,10 +1472,10 @@ class AlexaModeController(AlexaCapability):
# Return state instead of position when using ModeController.
mode = self.entity.state
if mode in (
cover.STATE_OPEN,
cover.STATE_OPENING,
cover.STATE_CLOSED,
cover.STATE_CLOSING,
cover.CoverState.OPEN,
cover.CoverState.OPENING,
cover.CoverState.CLOSED,
cover.CoverState.CLOSING,
STATE_UNKNOWN,
):
return f"{cover.ATTR_POSITION}.{mode}"
@@ -1594,11 +1594,11 @@ class AlexaModeController(AlexaCapability):
["Position", AlexaGlobalCatalog.SETTING_OPENING], False
)
self._resource.add_mode(
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}",
[AlexaGlobalCatalog.VALUE_OPEN],
)
self._resource.add_mode(
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}",
[AlexaGlobalCatalog.VALUE_CLOSE],
)
self._resource.add_mode(
@@ -1651,22 +1651,22 @@ class AlexaModeController(AlexaCapability):
raise_labels.append(AlexaSemantics.ACTION_OPEN)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}",
)
self._semantics.add_action_to_directive(
lower_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
{"mode": f"{cover.ATTR_POSITION}.{cover.CoverState.CLOSED}"},
)
self._semantics.add_action_to_directive(
raise_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
{"mode": f"{cover.ATTR_POSITION}.{cover.CoverState.OPEN}"},
)
return self._semantics.serialize_semantics()

View File

@@ -1261,9 +1261,9 @@ async def async_api_set_mode(
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
position = mode.split(".")[1]
if position == cover.STATE_CLOSED:
if position == cover.CoverState.CLOSED:
service = cover.SERVICE_CLOSE_COVER
elif position == cover.STATE_OPEN:
elif position == cover.CoverState.OPEN:
service = cover.SERVICE_OPEN_COVER
elif position == "custom":
service = cover.SERVICE_STOP_COVER

View File

@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.helpers.entity_registry as er
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import async_update_unique_id
@@ -51,11 +53,47 @@ BINARY_SENSORS: Final = (
),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_available_fn=lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
),
),
)
DEPRECATED_BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -66,6 +104,8 @@ async def async_setup_entry(
coordinator = entry.runtime_data
entity_registry = er.async_get(hass)
# Replace unique id for "detectionState" binary sensor
await async_update_unique_id(
hass,
@@ -75,6 +115,16 @@ async def async_setup_entry(
"detectionState",
)
# Clean up deprecated sensors
for sensor_desc in DEPRECATED_BINARY_SENSORS:
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{sensor_desc.key}"
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
):
_LOGGER.debug("Removing deprecated entity %s", entity_id)
entity_registry.async_remove(entity_id)
known_devices: set[str] = set()
def _check_device() -> None:

View File

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

View File

@@ -32,7 +32,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
@@ -40,9 +42,9 @@ SENSORS: Final = (
AmazonSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device, _key: (
native_unit_of_measurement_fn=lambda device, key: (
UnitOfTemperature.CELSIUS
if device.sensors[_key].scale == "CELSIUS"
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
else UnitOfTemperature.FAHRENHEIT
),
state_class=SensorStateClass.MEASUREMENT,

View File

@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call, async_update_unique_id
from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
is_on_fn: Callable[[AmazonDevice], bool]
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online and device.sensors[key].error is False
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
method: str
@@ -58,6 +64,9 @@ async def async_setup_entry(
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
)
# Remove DND switch from virtual groups
await async_remove_dnd_from_virtual_group(hass, coordinator)
known_devices: set[str] = set()
def _check_device() -> None:

View File

@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.entity_registry as er
@@ -61,3 +63,21 @@ async def async_update_unique_id(
# Update the registry with the new unique_id
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
async def async_remove_dnd_from_virtual_group(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove entity DND from virtual group."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-do_not_disturb"
entity_id = entity_registry.async_get_entity_id(
DOMAIN, SWITCH_DOMAIN, unique_id
)
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)

View File

@@ -65,6 +65,31 @@ SENSOR_DESCRIPTIONS = [
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME280"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.HUMIDITY,
key="BME680_humidity",
translation_key="humidity",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BME680_pressure",
translation_key="pressure",
native_unit_of_measurement=UnitOfPressure.PA,
suggested_unit_of_measurement=UnitOfPressure.MMHG,
suggested_display_precision=0,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key="BME680_temperature",
translation_key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=2,
translation_placeholders={"sensor_name": "BME680"},
),
AltruistSensorEntityDescription(
device_class=SensorDeviceClass.PRESSURE,
key="BMP_pressure",

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,10 +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"
THINKING_MODELS = [
"claude-3-7-sonnet",
"claude-sonnet-4-0",
"claude-opus-4-0",
"claude-opus-4-1",
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,14 +64,21 @@ 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,
NON_THINKING_MODELS,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
@@ -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):
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),
)
if not messages or messages[-1]["role"] != "user":
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:
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)
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,10 +575,10 @@ 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 (
model.startswith(tuple(THINKING_MODELS))
not model.startswith(tuple(NON_THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
):
model_args["thinking"] = ThinkingConfigEnabledParam(
@@ -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

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.62.0"]
"requirements": ["anthropic==0.69.0"]
}

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 = (
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
)
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

@@ -7,6 +7,8 @@ from typing import Any
from pyaprilaire.const import Attribute
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
FAN_AUTO,
FAN_ON,
PRESET_AWAY,
@@ -16,7 +18,12 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.const import PRECISION_HALVES, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -232,15 +239,15 @@ class AprilaireClimate(BaseAprilaireEntity, ClimateEntity):
cool_setpoint = 0
heat_setpoint = 0
if temperature := kwargs.get("temperature"):
if temperature := kwargs.get(ATTR_TEMPERATURE):
if self.coordinator.data.get(Attribute.MODE) == 3:
cool_setpoint = temperature
else:
heat_setpoint = temperature
else:
if target_temp_low := kwargs.get("target_temp_low"):
if target_temp_low := kwargs.get(ATTR_TARGET_TEMP_LOW):
heat_setpoint = target_temp_low
if target_temp_high := kwargs.get("target_temp_high"):
if target_temp_high := kwargs.get(ATTR_TARGET_TEMP_HIGH):
cool_setpoint = target_temp_high
if cool_setpoint == 0 and heat_setpoint == 0:

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,11 +53,14 @@ __all__ = (
"SAMPLE_CHANNELS",
"SAMPLE_RATE",
"SAMPLE_WIDTH",
"AssistPipelineSelect",
"AudioSettings",
"Pipeline",
"PipelineEvent",
"PipelineEventType",
"PipelineNotFound",
"VadSensitivity",
"VadSensitivitySelect",
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",

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:
@@ -219,7 +237,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 +253,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 +324,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 +454,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

@@ -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

@@ -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)
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

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco",
"iot_class": "cloud_polling",
"requirements": ["autarco==3.1.0"]
"requirements": ["autarco==3.2.0"]
}

View File

@@ -136,8 +136,8 @@ class WellKnownOAuthInfoView(HomeAssistantView):
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
url_prefix = ""
return self.json(
{
metadata = {
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
@@ -146,7 +146,12 @@ class WellKnownOAuthInfoView(HomeAssistantView):
"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

@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"requirements": ["brother==5.1.0"],
"requirements": ["brother==5.1.1"],
"zeroconf": [
{
"type": "_printer._tcp.local.",

View File

@@ -7,12 +7,14 @@ from typing import Any
from evolutionhttp import BryantEvolutionLocalClient
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
@@ -208,24 +210,24 @@ class BryantEvolutionClimate(ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if kwargs.get("target_temp_high"):
temp = int(kwargs["target_temp_high"])
if value := kwargs.get(ATTR_TARGET_TEMP_HIGH):
temp = int(value)
if not await self._client.set_cooling_setpoint(temp):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="failed_to_set_clsp"
)
self._attr_target_temperature_high = temp
if kwargs.get("target_temp_low"):
temp = int(kwargs["target_temp_low"])
if value := kwargs.get(ATTR_TARGET_TEMP_LOW):
temp = int(value)
if not await self._client.set_heating_setpoint(temp):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="failed_to_set_htsp"
)
self._attr_target_temperature_low = temp
if kwargs.get("temperature"):
temp = int(kwargs["temperature"])
if value := kwargs.get(ATTR_TEMPERATURE):
temp = int(value)
fn = (
self._client.set_heating_setpoint
if self.hvac_mode == HVACMode.HEAT

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

@@ -525,17 +525,18 @@ class CalendarEntity(Entity):
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the entity state attributes."""
if (event := self.event) is None:
return None
data: dict[str, Any] = self.generate_entity_state_attributes()
return {
"message": event.summary,
"all_day": event.all_day,
"start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT),
"end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT),
"location": event.location if event.location else "",
"description": event.description if event.description else "",
}
if (event := self.event) is None:
return data or None
data["message"] = event.summary
data["all_day"] = event.all_day
data["start_time"] = event.start_datetime_local.strftime(DATE_STR_FORMAT)
data["end_time"] = event.end_datetime_local.strftime(DATE_STR_FORMAT)
data["location"] = event.location if event.location else ""
data["description"] = event.description if event.description else ""
return data
@final
@property

View File

@@ -169,7 +169,7 @@ class CalendarEventListener:
def __init__(
self,
hass: HomeAssistant,
job: HassJob[..., Coroutine[Any, Any, None]],
job: HassJob[..., Coroutine[Any, Any, None] | Any],
trigger_data: dict[str, Any],
fetcher: QueuedEventFetcher,
) -> None:

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
@@ -661,7 +664,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, str | None]:
"""Return the camera state attributes."""
attrs = {"access_token": self.access_tokens[-1]}
attrs: dict[str, Any] = self.generate_entity_state_attributes()
attrs["access_token"] = self.access_tokens[-1]
if model := self.model:
attrs["model_name"] = model

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

@@ -4,5 +4,6 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/citybikes",
"iot_class": "cloud_polling",
"quality_scale": "legacy"
"quality_scale": "legacy",
"requirements": ["python-citybikes==0.3.3"]
}

View File

@@ -5,8 +5,11 @@ from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
import sys
import aiohttp
from citybikes import __version__ as CITYBIKES_CLIENT_VERSION
from citybikes.asyncio import Client as CitybikesClient
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -15,21 +18,18 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_ID,
ATTR_LATITUDE,
ATTR_LOCATION,
ATTR_LONGITUDE,
ATTR_NAME,
APPLICATION_NAME,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
EVENT_HOMEASSISTANT_CLOSE,
UnitOfLength,
__version__,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
@@ -40,31 +40,33 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
_LOGGER = logging.getLogger(__name__)
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_EXTRA = "extra"
ATTR_FREE_BIKES = "free_bikes"
ATTR_NETWORK = "network"
ATTR_NETWORKS_LIST = "networks"
ATTR_STATIONS_LIST = "stations"
ATTR_TIMESTAMP = "timestamp"
HA_USER_AGENT = (
f"{APPLICATION_NAME}/{__version__} "
f"python-citybikes/{CITYBIKES_CLIENT_VERSION} "
f"Python/{sys.version_info[0]}.{sys.version_info[1]}"
)
ATTR_UID = "uid"
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
ATTR_EMPTY_SLOTS = "empty_slots"
ATTR_TIMESTAMP = "timestamp"
CONF_NETWORK = "network"
CONF_STATIONS_LIST = "stations"
DEFAULT_ENDPOINT = "https://api.citybik.es/{uri}"
PLATFORM = "citybikes"
MONITORED_NETWORKS = "monitored-networks"
DATA_CLIENT = "client"
NETWORKS_URI = "v2/networks"
REQUEST_TIMEOUT = 5 # In seconds; argument to asyncio.timeout
REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=5)
SCAN_INTERVAL = timedelta(minutes=5) # Timely, and doesn't suffocate the API
STATIONS_URI = "v2/networks/{uid}?fields=network.stations"
CITYBIKES_ATTRIBUTION = (
"Information provided by the CityBikes Project (https://citybik.es/#about)"
)
@@ -87,72 +89,6 @@ PLATFORM_SCHEMA = vol.All(
),
)
NETWORK_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ID): cv.string,
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_LOCATION): vol.Schema(
{
vol.Required(ATTR_LATITUDE): cv.latitude,
vol.Required(ATTR_LONGITUDE): cv.longitude,
},
extra=vol.REMOVE_EXTRA,
),
},
extra=vol.REMOVE_EXTRA,
)
NETWORKS_RESPONSE_SCHEMA = vol.Schema(
{vol.Required(ATTR_NETWORKS_LIST): [NETWORK_SCHEMA]}
)
STATION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_FREE_BIKES): cv.positive_int,
vol.Required(ATTR_EMPTY_SLOTS): vol.Any(cv.positive_int, None),
vol.Required(ATTR_LATITUDE): cv.latitude,
vol.Required(ATTR_LONGITUDE): cv.longitude,
vol.Required(ATTR_ID): cv.string,
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_TIMESTAMP): cv.string,
vol.Optional(ATTR_EXTRA): vol.Schema(
{vol.Optional(ATTR_UID): cv.string}, extra=vol.REMOVE_EXTRA
),
},
extra=vol.REMOVE_EXTRA,
)
STATIONS_RESPONSE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_NETWORK): vol.Schema(
{vol.Required(ATTR_STATIONS_LIST): [STATION_SCHEMA]}, extra=vol.REMOVE_EXTRA
)
}
)
class CityBikesRequestError(Exception):
"""Error to indicate a CityBikes API request has failed."""
async def async_citybikes_request(hass, uri, schema):
"""Perform a request to CityBikes API endpoint, and parse the response."""
try:
session = async_get_clientsession(hass)
async with asyncio.timeout(REQUEST_TIMEOUT):
req = await session.get(DEFAULT_ENDPOINT.format(uri=uri))
json_response = await req.json()
return schema(json_response)
except (TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not connect to CityBikes API endpoint")
except ValueError:
_LOGGER.error("Received non-JSON data from CityBikes API endpoint")
except vol.Invalid as err:
_LOGGER.error("Received unexpected JSON from CityBikes API endpoint: %s", err)
raise CityBikesRequestError
async def async_setup_platform(
hass: HomeAssistant,
@@ -175,6 +111,14 @@ async def async_setup_platform(
radius, UnitOfLength.FEET, UnitOfLength.METERS
)
client = CitybikesClient(user_agent=HA_USER_AGENT, timeout=REQUEST_TIMEOUT)
hass.data[PLATFORM][DATA_CLIENT] = client
async def _async_close_client(event):
await client.close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client)
# Create a single instance of CityBikesNetworks.
networks = hass.data.setdefault(CITYBIKES_NETWORKS, CityBikesNetworks(hass))
@@ -194,10 +138,10 @@ async def async_setup_platform(
devices = []
for station in network.stations:
dist = location_util.distance(
latitude, longitude, station[ATTR_LATITUDE], station[ATTR_LONGITUDE]
latitude, longitude, station.latitude, station.longitude
)
station_id = station[ATTR_ID]
station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, ""))
station_id = station.id
station_uid = str(station.extra.get(ATTR_UID, ""))
if radius > dist or stations_list.intersection((station_id, station_uid)):
if name:
@@ -216,6 +160,7 @@ class CityBikesNetworks:
def __init__(self, hass):
"""Initialize the networks instance."""
self.hass = hass
self.client = hass.data[PLATFORM][DATA_CLIENT]
self.networks = None
self.networks_loading = asyncio.Condition()
@@ -224,24 +169,21 @@ class CityBikesNetworks:
try:
await self.networks_loading.acquire()
if self.networks is None:
networks = await async_citybikes_request(
self.hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA
)
self.networks = networks[ATTR_NETWORKS_LIST]
except CityBikesRequestError as err:
self.networks = await self.client.networks.fetch()
except aiohttp.ClientError as err:
raise PlatformNotReady from err
else:
result = None
minimum_dist = None
for network in self.networks:
network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE]
network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE]
network_latitude = network.location.latitude
network_longitude = network.location.longitude
dist = location_util.distance(
latitude, longitude, network_latitude, network_longitude
)
if minimum_dist is None or dist < minimum_dist:
minimum_dist = dist
result = network[ATTR_ID]
result = network.id
return result
finally:
@@ -257,22 +199,20 @@ class CityBikesNetwork:
self.network_id = network_id
self.stations = []
self.ready = asyncio.Event()
self.client = hass.data[PLATFORM][DATA_CLIENT]
async def async_refresh(self, now=None):
"""Refresh the state of the network."""
try:
network = await async_citybikes_request(
self.hass,
STATIONS_URI.format(uid=self.network_id),
STATIONS_RESPONSE_SCHEMA,
)
self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST]
self.ready.set()
except CityBikesRequestError as err:
if now is not None:
self.ready.clear()
else:
network = await self.client.network(uid=self.network_id).fetch()
except aiohttp.ClientError as err:
if now is None:
raise PlatformNotReady from err
self.ready.clear()
return
self.stations = network.stations
self.ready.set()
class CityBikesStation(SensorEntity):
@@ -290,16 +230,13 @@ class CityBikesStation(SensorEntity):
async def async_update(self) -> None:
"""Update station state."""
for station in self._network.stations:
if station[ATTR_ID] == self._station_id:
station_data = station
break
self._attr_name = station_data.get(ATTR_NAME)
self._attr_native_value = station_data.get(ATTR_FREE_BIKES)
station = next(s for s in self._network.stations if s.id == self._station_id)
self._attr_name = station.name
self._attr_native_value = station.free_bikes
self._attr_extra_state_attributes = {
ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID),
ATTR_LATITUDE: station_data.get(ATTR_LATITUDE),
ATTR_LONGITUDE: station_data.get(ATTR_LONGITUDE),
ATTR_EMPTY_SLOTS: station_data.get(ATTR_EMPTY_SLOTS),
ATTR_TIMESTAMP: station_data.get(ATTR_TIMESTAMP),
ATTR_UID: station.extra.get(ATTR_UID),
ATTR_LATITUDE: station.latitude,
ATTR_LONGITUDE: station.longitude,
ATTR_EMPTY_SLOTS: station.empty_slots,
ATTR_TIMESTAMP: station.timestamp,
}

View File

@@ -341,16 +341,16 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
data: dict[str, Any] = self.generate_entity_state_attributes()
supported_features = self.supported_features
temperature_unit = self.temperature_unit
precision = self.precision
hass = self.hass
data: dict[str, str | float | None] = {
ATTR_CURRENT_TEMPERATURE: show_temp(
data[ATTR_CURRENT_TEMPERATURE] = show_temp(
hass, self.current_temperature, temperature_unit, precision
),
}
)
if ClimateEntityFeature.TARGET_TEMPERATURE in supported_features:
data[ATTR_TEMPERATURE] = show_temp(

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

@@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -29,23 +30,19 @@ async def async_setup_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoBinarySensorEntity(
coordinator, device, config_entry.entry_id
)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
class ComelitVedoBinarySensorEntity(

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from asyncio.exceptions import TimeoutError
from collections.abc import Mapping
import re
from typing import Any
from aiocomelit import (
@@ -27,25 +28,20 @@ from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = "111111"
pin_regex = r"^[0-9]{4,10}$"
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
STEP_RECONFIGURE = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
}
)
@@ -55,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
api: ComelitCommonApi
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
raise InvalidPin
session = await async_client_session(hass)
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
api = ComeliteSerialBridgeApi(
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidPin:
errors["base"] = "invalid_pin"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class InvalidPin(HomeAssistantError):
"""Error to indicate an invalid pin."""

View File

@@ -161,7 +161,7 @@ class ComelitSerialBridge(
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""

View File

@@ -7,14 +7,14 @@ from typing import Any, cast
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call
from .utils import DeviceType, bridge_api_call, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -29,21 +29,19 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[COVER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
if device.index in new_devices
)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, COVER)
)
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@@ -62,7 +60,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
super().__init__(coordinator, device, config_entry_entry_id)
# Device doesn't provide a status so we assume UNKNOWN at first startup
self._last_action: int | None = None
self._last_state: str | None = None
def _current_action(self, action: str) -> bool:
"""Return the current cover action."""
@@ -98,7 +95,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@bridge_api_call
async def _cover_set_state(self, action: int, state: int) -> None:
"""Set desired cover state."""
self._last_state = self.state
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
self.coordinator.data[COVER][self._device.index].status = state
self.async_write_ha_state()
@@ -124,5 +120,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
await super().async_added_to_hass()
if last_state := await self.async_get_last_state():
self._last_state = last_state.state
if (state := await self.async_get_last_state()) is not None:
if state.state == CoverState.CLOSED:
self._last_action = STATE_COVER.index(CoverState.CLOSING)
if state.state == CoverState.OPEN:
self._last_action = STATE_COVER.index(CoverState.OPENING)
self._attr_is_closed = state.state == CoverState.CLOSED

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import bridge_api_call
from .utils import DeviceType, bridge_api_call, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -27,21 +27,19 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[LIGHT])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
if device.index in new_devices
)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, LIGHT)
)
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):

View File

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

View File

@@ -20,6 +20,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -65,24 +66,22 @@ async def async_setup_bridge_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[OTHER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[OTHER].values()
if device.index in new_devices
)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, OTHER)
)
async def async_setup_vedo_entry(
@@ -94,24 +93,22 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
if device in new_devices
]
if entities:
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):

View File

@@ -43,11 +43,13 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},

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