Compare commits

...

530 Commits

Author SHA1 Message Date
Franck Nijhof
bb98ed6633 2025.10.3 (#154718) 2025-10-17 23:14:01 +02:00
Franck Nijhof
59dace572a Bump version to 2025.10.3 2025-10-17 20:35:30 +00:00
cdnninja
735cf36a5b Bump pyvesync version to 3.1.2 (#154650)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-17 20:34:48 +00:00
tstabrawa
90b0f50b8f Move URL out of Nuheat strings.json (#154580) 2025-10-17 20:34:47 +00:00
Simone Chemelli
e731c07b77 Bump aioamazondevices to 6.4.4 (#154538) 2025-10-17 20:34:46 +00:00
Whitney Young
2c75635e95 OpenUV: Fix update by skipping when protection window is null (#154487) 2025-10-17 20:34:45 +00:00
Anuj Soni
1f031695c2 Move translatable URLs out of strings.json for isy994 (#154464) 2025-10-17 20:34:43 +00:00
Michel van de Wetering
fb279212a9 Add missinglong_press entry for trigger_type in strings.json for Hue (#154437) 2025-10-17 20:34:42 +00:00
DannyS95
45869523d0 Move igloohome API access URL into constant placeholders (#154430) 2025-10-17 20:34:41 +00:00
puddly
a753926f22 Use async_schedule_reload instead of async_reload for ZHA (#154397) 2025-10-17 20:34:40 +00:00
Simone Chemelli
dc874ff53a Bump aiocomelit to 1.1.2 (#154393) 2025-10-17 20:34:38 +00:00
Renat Sibgatulin
3ef6865708 Bump aioairq to 0.4.7 (#154386) 2025-10-17 20:34:37 +00:00
Anuj Soni
7f1989f9f2 Move translatable URLs out of strings.json for huawei lte (#154368)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-17 20:34:36 +00:00
epenet
97e338c760 Move URL out of sfr_box strings.json (#154364)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-17 20:34:35 +00:00
wollew
101679c17d update pysqueezebox lib to 0.13.0 (#154358) 2025-10-17 20:34:33 +00:00
tronikos
bc784c356e Bump opower to 0.15.7 (#154351) 2025-10-17 20:34:32 +00:00
J. Nick Koston
556cc57d8b Fix Bluetooth discovery for devices with alternating advertisement names (#154347) 2025-10-17 20:34:31 +00:00
Oliver Gründel
eef6e96a93 Move developer url out of strings.json for coinbase setup flow (#154339) 2025-10-17 20:34:30 +00:00
Shai Ungar
56d237af7f Move URLs out of SABnzbd strings.json (#154333)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-17 20:34:29 +00:00
Oliver Gründel
e5d1902d2a Move Ecobee authorization URL out of strings.json (#154332) 2025-10-17 20:34:27 +00:00
Yevhenii Vaskivskyi
a9a203678e AsusWRT: Pass only online clients to the device list from the API (#154322) 2025-10-17 20:34:26 +00:00
Mick Vleeshouwer
7f6237cc63 Move URL out of Overkiz Config Flow descriptions (#154315) 2025-10-17 20:34:25 +00:00
Simone Chemelli
5468e691ca Bump aioamazondevices to 6.4.3 (#154293) 2025-10-17 20:34:23 +00:00
Jan-Philipp Benecke
67cbbc3522 Move Electricity Maps url out of strings.json (#154284) 2025-10-17 20:33:17 +00:00
Dan Schafer
504da54c11 Update Snoo strings.json to include weaning_baseline (#154268) 2025-10-17 20:31:13 +00:00
Jordan Harvey
cdda2ef5c8 Bump pyprobeplus to 1.1.0 (#154265) 2025-10-17 20:31:12 +00:00
Jan Bouwhuis
f405f9eb4b Fix home wiziard total increasing sensors returning 0 (#154264) 2025-10-17 20:31:10 +00:00
Manu
634f71835a Add description placeholders to pyLoad config flow (#154254) 2025-10-17 20:31:09 +00:00
Manu
49bfb01fac Add description placeholders in Uptime Kuma config flow (#154252)
Signed-off-by: tr4nt0r <4445816+tr4nt0r@users.noreply.github.com>
2025-10-17 20:31:08 +00:00
Joakim Plate
ad8f7fdcab Move url like strings to placeholders for nibe (#154249) 2025-10-17 20:31:07 +00:00
J. Nick Koston
f82ec81062 Fix Yale integration to handle unavailable OAuth implementation at startup (#154245) 2025-10-17 20:31:05 +00:00
J. Nick Koston
03b0842a01 Fix August integration to handle unavailable OAuth implementation at startup (#154244) 2025-10-17 20:31:04 +00:00
Christopher Fenner
13e5cb5cc8 Remove URL from ViCare strings.json (#154243) 2025-10-17 20:31:03 +00:00
Shay Levy
f18cdaf4d8 Move URL out of Switcher strings.json (#154240) 2025-10-17 20:31:02 +00:00
Andrew Jackson
5b3bca1426 Move URL out of Mastodon strings.json (#154231) 2025-10-17 20:31:01 +00:00
Andrew Jackson
d812e9d43c Move URL out of Mealie strings.json (#154230) 2025-10-17 20:30:59 +00:00
Simone Chemelli
fa1071b221 Bump aioamazondevices to 6.4.1 (#154228) 2025-10-17 20:30:58 +00:00
Paul Bottein
e48c2c6c0b Bump frontend 20251001.4 (#154218) 2025-10-17 20:23:01 +00:00
Yvan13120
bddd4100c0 Fix state class for Overkiz water consumption (#154164) 2025-10-17 20:23:00 +00:00
srirams
70d8df2e95 Remove redudant state write in Smart Meter Texas (#154126) 2025-10-17 20:22:58 +00:00
Lennart Coopmans
08b3dd0173 PushSafer: Handle empty data section properly (#154109) 2025-10-17 20:22:57 +00:00
Magnus
6723a7c4e1 Bump aioasuswrt to 1.5.1 (#153209) 2025-10-17 20:22:55 +00:00
Franck Nijhof
40d7f2a89e 2025.10.2 (#154181) 2025-10-10 23:19:19 +02:00
Shay Levy
13b717e2da Fix shelly remove orphaned entities (#154182)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-10 22:46:30 +02:00
Franck Nijhof
5fcfd3ad84 Bump version to 2025.10.2 2025-10-10 20:29:17 +00:00
Shay Levy
324a7b5443 Fix Shelly RPC cover update when the device is not initialized (#154159) 2025-10-10 20:27:30 +00:00
Robert Resch
491ae8f72c Bump deebot-client to 15.1.0 (#154154) 2025-10-10 20:23:10 +00:00
Justus
259247892f IOmeter bump version v0.2.0 (#154150) 2025-10-10 20:23:09 +00:00
Bram Kragten
caeda0ef64 Update frontend to 20251001.2 (#154143) 2025-10-10 20:23:08 +00:00
Paul Bottein
df35c535e4 Add missing entity category and icons for smlight integration (#154131) 2025-10-10 20:23:07 +00:00
Paulus Schoutsen
f93b9e0ed0 Z-Wave: ESPHome discovery to update all options (#154113) 2025-10-10 20:23:05 +00:00
peteS-UK
48a3372cf2 Fix for multiple Lyrion Music Server on a single Home Assistant server for Squeezebox (#154081) 2025-10-10 20:23:04 +00:00
Maciej Bieniek
d84fd72428 Bump brother to version 5.1.1 (#154080) 2025-10-10 20:23:03 +00:00
Simone Chemelli
e8cb386962 Bump aioamazondevices to 6.4.0 (#154071) 2025-10-10 20:23:02 +00:00
epenet
5ac726703c Filter out invalid Renault vehicles (#154070)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 20:23:00 +00:00
Joost Lekkerkerker
688649a799 Don't mark ZHA coordinator as via_device with itself (#154004) 2025-10-10 20:17:07 +00:00
Artur Pragacz
c5359ade3e Fix empty llm api list in chat log (#153996) 2025-10-10 20:17:05 +00:00
Michael Davie
4e60dedc1b Bump env-canada to 0.11.3 (#153967) 2025-10-10 20:17:04 +00:00
Maciej Bieniek
221d74f83a Fix update interval for AccuWeather hourly forecast (#153957) 2025-10-10 20:17:02 +00:00
G Johansson
fbbb3d6415 Bump holidays to 0.82 (#153952) 2025-10-10 20:17:01 +00:00
Josef Zweck
8297019011 Bump pylamarzocco to 2.1.2 (#153950) 2025-10-10 20:16:59 +00:00
TheJulianJES
61715dcff3 Adjust OTBR config entry name for ZBT-2 (#153940) 2025-10-10 20:16:58 +00:00
TheJulianJES
32b822ee99 Fix HA hardware configuration message for Thread without HAOS (#153933) 2025-10-10 20:16:56 +00:00
Fabien Kleinbourg
e6c2e0ad80 sharkiq dependency bump to 1.4.2 (#153931) 2025-10-10 20:16:55 +00:00
TheJulianJES
1314427dc5 Do not auto-set up ZHA zeroconf discoveries during onboarding (#153914) 2025-10-10 20:16:53 +00:00
Tom Matheussen
bf499a45f7 Add missing translation string for Satel Integra subentry type (#153905) 2025-10-10 20:16:52 +00:00
Christopher Fenner
b955e22628 fix typo in icon assignment of AccuWeather integration (#153890) 2025-10-10 20:16:50 +00:00
Simone Chemelli
1b222ff5fd Fix restore cover state for Comelit SimpleHome (#153887) 2025-10-10 20:16:49 +00:00
derytive
f0510e703f Add plate_count for Miele KM7575 (#153868) 2025-10-10 19:36:03 +00:00
G Johansson
cbe3956e15 Handle timeout errors gracefully in Nord Pool services (#153856) 2025-10-10 19:36:01 +00:00
Aaron Bach
4588e9da8d Limit SimpliSafe websocket connection attempts during startup (#153853)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-10 19:36:00 +00:00
Simone Chemelli
5445890fdf Bump aiocomelit to 1.1.1 (#153843) 2025-10-10 19:35:59 +00:00
Simone Chemelli
9b49f77f86 Fix PIN validation for Comelit SimpleHome (#153840) 2025-10-10 19:35:57 +00:00
Petro31
566c8fb786 Fix delay_on and auto_off with multiple triggers (#153839) 2025-10-10 19:35:56 +00:00
Joost Lekkerkerker
b36150c213 Add motion presets to SmartThings AC (#153830) 2025-10-10 19:35:54 +00:00
Joost Lekkerkerker
809070d2ad Catch update exception in AirGradient (#153828) 2025-10-10 19:35:53 +00:00
Joost Lekkerkerker
f4339dc031 Bump pySmartThings to 3.3.1 (#153826) 2025-10-10 19:35:51 +00:00
epenet
f3b37d24b0 Fix Tuya cover position when only control is available (#153803) 2025-10-10 19:35:50 +00:00
Paulus Schoutsen
4c8348caa7 Handle ESPHome discoveries with uninitialized Z-Wave antennas (#153790) 2025-10-10 19:35:49 +00:00
cdnninja
b9e7c102ea vesync correct fan set modes (#153761) 2025-10-10 19:35:47 +00:00
Simone Chemelli
69d9fa89b7 Remove stale entities from Alexa Devices (#153759) 2025-10-10 19:35:46 +00:00
Simone Chemelli
6f3f5a5ec1 Bump aioamazondevices to 6.2.9 (#153756) 2025-10-10 19:35:44 +00:00
Simone Chemelli
5ecfeca90a Fix sensors availability check for Alexa Devices (#153743) 2025-10-10 19:35:43 +00:00
Sander Jochems
00e0570fd4 Upgrade python-melcloud to 0.1.2 (#153742) 2025-10-10 19:35:41 +00:00
Øyvind Matheson Wergeland
5a5b94f3af Synology DSM: Don't reinitialize API during configuration (#153739) 2025-10-10 19:35:40 +00:00
Maciej Bieniek
34f00d9b33 Align Shelly presencezone entity to the new API/firmware (#153737) 2025-10-10 19:35:39 +00:00
Tom
4cabc5b368 Bump airOS to 0.5.5 using formdata for v6 firmware (#153736) 2025-10-10 19:35:37 +00:00
tronikos
4045125422 Fix missing google_assistant_sdk.send_text_command (#153735) 2025-10-10 19:35:36 +00:00
Fredrik Erlandsson
d7393af76f Version bump pydaikin to 2.17.1 (#153726) 2025-10-10 19:35:34 +00:00
Fredrik Erlandsson
ad41386b27 Version bump pydaikin to 2.17.0 (#153718) 2025-10-10 19:35:33 +00:00
tronikos
62d17ea20c Bump opower to 0.15.6 (#153714) 2025-10-10 19:35:31 +00:00
peetersch
c4954731d0 Modbus Fix message_wait_milliseconds is no longer applied (#153709) 2025-10-10 19:35:30 +00:00
cdnninja
647723d3f0 Bump pyvesync to 3.1.0 (#153693) 2025-10-10 19:35:28 +00:00
Christopher Fenner
51c500e22c Fix ViCare pressure sensors missing unit of measurement (#153691) 2025-10-10 19:35:26 +00:00
Denis Shulyaka
f6fc13c1f2 Gemini: Use default model instead of recommended where applicable (#153676) 2025-10-10 19:35:25 +00:00
Jan Bouwhuis
0009a7a042 Fix MQTT Lock state reset to unknown when a reset payload is received (#153647) 2025-10-10 19:35:24 +00:00
Luke Lashley
a3d1aa28e7 Switch Roborock to v4 of the code login api (#153593) 2025-10-10 19:35:22 +00:00
Simone Chemelli
9f53eb9b76 Bump aioamazondevices to 6.2.8 (#153592) 2025-10-10 19:35:21 +00:00
Luke Lashley
f53a205ff3 Bump python-roborock to 2.50.2 (#153561) 2025-10-10 19:35:19 +00:00
NANI
d08517c3df Updated VRM client and accounted for missing forecasts (#153464) 2025-10-10 19:35:18 +00:00
Kinachi249
d7398a44a1 Bump PyCync to 0.4.1 (#153401) 2025-10-10 19:35:17 +00:00
Aidan Timson
9acfc0cb88 Fix power device classes for system bridge (#153201) 2025-10-10 19:35:15 +00:00
Hessel
1b3d21523a Wallbox fix Rate Limit issue for multiple chargers (#153074) 2025-10-10 19:35:14 +00:00
puddly
1d407d1326 Prevent reloading the ZHA integration while adapter firmware is being updated (#152626) 2025-10-10 19:35:12 +00:00
Franck Nijhof
013346cead 2025.10.1 (#153582) 2025-10-03 20:08:44 +02:00
Franck Nijhof
5abaabc9da Bump version to 2025.10.1 2025-10-03 17:26:37 +00:00
Paulus Schoutsen
32481312c3 When discovering a Z-Wave adapter, always configure add-on in config flow (#153575) 2025-10-03 17:26:16 +00:00
Paulus Schoutsen
bdc9eb37d3 Z-Wave to support migrating from USB to socket with same home ID (#153522) 2025-10-03 17:26:15 +00:00
Abílio Costa
e0afcbc02b Debounce updates in Idasen Desk (#153503) 2025-10-03 17:26:13 +00:00
puddly
cd56a6a98d Bump universal-silabs-flasher to 0.0.35 (#153500) 2025-10-03 17:26:11 +00:00
cdnninja
9d85893bbb Fix VeSync zero fan speed handling (#153493)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-03 17:26:10 +00:00
starkillerOG
9e8a70225f Bump reolink-aio to 0.16.1 (#153489) 2025-10-03 17:26:08 +00:00
Daniel Hjelseth Høyer
96ec795d5e Bump pyTibber to 0.32.2 (#153484) 2025-10-03 17:26:07 +00:00
Josef Zweck
65b796070d Fix missing parameter pass in onedrive (#153478) 2025-10-03 17:26:05 +00:00
Aidan Timson
32994812e5 Update OVOEnergy to 3.0.1 (#153476) 2025-10-03 17:26:04 +00:00
G Johansson
66ff9d63a3 Fix next event in workday calendar (#153465) 2025-10-03 17:26:02 +00:00
Joost Lekkerkerker
b2a63d4996 Add translation for turbo fan mode in SmartThings (#153445)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 17:26:00 +00:00
puddly
f9f37b7f2a Disable baudrate bootloader reset for ZBT-2 (#153443) 2025-10-03 17:25:59 +00:00
Stefan Agner
7bdd9dd38a Update Home Assistant base image to 2025.10.0 (#153441) 2025-10-03 17:25:58 +00:00
Joost Lekkerkerker
1e8aae0a89 Fix missing powerconsumptionreport in Smartthings (#153438) 2025-10-03 17:25:56 +00:00
Aidan Timson
cf668e9dc2 Add missing translation for media browser default title (#153430)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-10-03 17:25:55 +00:00
Norbert Rittel
2e91c8700f Fix sentence-casing in user-facing strings of slack (#153427) 2025-10-03 17:25:53 +00:00
J. Nick Koston
9d14627daa Bump aiohomekit to 3.2.19 (#153423) 2025-10-03 17:25:52 +00:00
TheJulianJES
73b8283748 Fix Z-Wave RGB light turn on causing rare ZeroDivisionError (#153422) 2025-10-03 17:25:50 +00:00
Manu
edeaaa2e63 Update markdown field description in ntfy integration (#153421) 2025-10-03 17:25:49 +00:00
Tom Matheussen
d26dd8fc39 Fix Satel Integra creating new binary sensors on YAML import (#153419) 2025-10-03 17:25:47 +00:00
Denis Shulyaka
34640ea735 Disable thinking for unsupported gemini models (#153415) 2025-10-03 17:25:46 +00:00
Erwin Douna
46a2e21ef0 Bump pyportainer 1.0.3 (#153413) 2025-10-03 17:25:45 +00:00
Erwin Douna
508af53e72 Bump pyportainer 1.0.2 (#153326) 2025-10-03 17:25:43 +00:00
Josef Zweck
5f7440608c Increase onedrive upload chunk size (#153406) 2025-10-03 17:22:10 +00:00
Michael J. Kidd
0d1aa38a26 Pushover: Handle empty data section properly (#153397) 2025-10-03 17:22:08 +00:00
Luke Lashley
929f8c148a Bump python-roborock to 2.49.1 (#153396) 2025-10-03 17:22:07 +00:00
Joakim Plate
92db1f5a04 Correct blocking update in ToGrill with lack of notifications (#153387) 2025-10-03 17:22:05 +00:00
starkillerOG
e66b5ce0bf Add Roborock mop intensity translations (#153380) 2025-10-03 17:22:03 +00:00
Michael
1e17150e9f Explicit pass in the config entry to coordinator in airtouch4 (#153361)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-10-03 17:22:02 +00:00
Michael
792902de3d Set config entry to None in ProxmoxVE (#153357) 2025-10-03 17:22:00 +00:00
Andre Lengwenus
04d78c3dd5 Explicitly check for None in raw value processing of modbus (#153352) 2025-10-03 17:21:59 +00:00
G Johansson
5c8d5bfb84 Fix Nord Pool 15 minute interval (#153350) 2025-10-03 17:21:57 +00:00
puddly
99bff31869 Do not reset the adapter twice during ZHA options flow migration (#153345) 2025-10-03 17:21:56 +00:00
Stefan Agner
d949119fb0 Bump aiohasupervisor to 0.3.3 (#153344) 2025-10-03 17:21:54 +00:00
Tom
e7b737ece5 Bump airOS module for alternative login url (#153317) 2025-10-03 17:21:52 +00:00
Tom
fb8ddac2e8 Bump airOS dependency (#153065) 2025-10-03 17:21:51 +00:00
Franck Nijhof
9a29cc53ef 2025.10.0 (#152881) 2025-10-01 18:17:36 +02:00
Franck Nijhof
55d5e769b2 Bump version to 2025.10.0 2025-10-01 15:19:48 +00:00
Franck Nijhof
6cd1283b00 Bump version to 2025.10.0b7 2025-10-01 14:51:37 +00:00
Maciej Bieniek
dde60cdecb Improve mac_address_from_name() function to avoid double discovery of Shelly devices (#153343) 2025-10-01 14:51:09 +00:00
Michael Hansen
f03b16bdf8 Bump intents to 2025.10.1 (#153340) 2025-10-01 14:51:07 +00:00
Franck Nijhof
f616e5a4e3 Bump version to 2025.10.0b6 2025-10-01 10:41:01 +00:00
Artur Pragacz
c0317f60cc Add analytics platform to esphome (#153311) 2025-10-01 10:40:41 +00:00
Bram Kragten
8abfe424e1 Update frontend to 20251001.0 (#153300) 2025-10-01 10:40:40 +00:00
HarvsG
8de200de0b Fix Bayesian ConfigFlow templates in 2025.10 (#153289)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-10-01 10:40:38 +00:00
Joost Lekkerkerker
f242e294be Add Konnected brand (#153280) 2025-10-01 10:40:36 +00:00
Joost Lekkerkerker
58cc7c8f84 Add Level brand (#153279) 2025-10-01 10:40:35 +00:00
Joost Lekkerkerker
bd10f6ec08 Require cloud for Aladdin Connect (#153278)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-01 10:40:33 +00:00
puddly
ed9cfb4c4b Use hardware bootloader reset methods for firmware config flows (#153277) 2025-10-01 10:40:32 +00:00
Joost Lekkerkerker
a6b6e4c4b8 Add Eltako brand (#153276) 2025-10-01 10:40:31 +00:00
Robert Resch
36ff5c0d45 Bump aioecowitt to 2025.9.2 (#153273) 2025-10-01 10:40:29 +00:00
Artur Pragacz
de6d34fec5 Filter out service type devices in extended analytics (#153271) 2025-10-01 10:40:28 +00:00
Erwin Douna
38f9067970 Portainer fix CONF_VERIFY_SSL (#153269)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-10-01 10:40:27 +00:00
Norbert Rittel
53a8a250d0 Replace "Climate name" with "Climate program" in ecobee action (#153264) 2025-10-01 10:40:25 +00:00
Artur Pragacz
00f6d26ede Add analytics platform to wled (#153258) 2025-10-01 10:40:24 +00:00
andreimoraru
6d09411c07 Bump yt-dlp to 2025.09.26 (#153252) 2025-10-01 10:40:22 +00:00
TheJulianJES
037e2bfd31 Fix ZHA unable to select "none" flow control (#153235) 2025-10-01 10:40:21 +00:00
TheJulianJES
c893552d4a Replace remaining ZHA "radio" strings with "adapter" (#153234) 2025-10-01 10:40:20 +00:00
TheJulianJES
4fd10162c9 Improve ZHA multi-pan firmware repair text (#153232) 2025-10-01 10:40:18 +00:00
Joris Pelgröm
392ee5ae7e Use UnitOfTime.DAYS instead of custom unit for LetPot number entity (#153054) 2025-10-01 10:40:17 +00:00
puddly
bf190609a0 Reduce Connect firmware install times by removing unnecessary firmware probing (#153012) 2025-10-01 10:40:15 +00:00
Samuel Xiao
e982ac1e53 Switchbot Cloud: Fix Roller Shade not work issue (#152528) 2025-10-01 10:40:14 +00:00
Pete Sage
b4747ea87b Fix Sonos Dialog Select type conversion part II (#152491)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-10-01 10:40:12 +00:00
HarvsG
df69bcecb7 Pihole better logging of update errors (#152077) 2025-10-01 10:40:11 +00:00
Franck Nijhof
c75dca743a Bump version to 2025.10.0b5 2025-09-30 09:21:25 +00:00
Jan Bouwhuis
00d667ed51 Add missing translation strings for added sensor device classes pm4 and reactive energy (#153215) 2025-09-30 09:21:19 +00:00
c0ffeeca7
51e098e807 ZHA: rename radio to adapter (#153206) 2025-09-30 09:21:17 +00:00
RogerSelwyn
5e2b27699e Handle return result from ebusd being "empty" (#153199) 2025-09-30 09:21:16 +00:00
Erik Montnemery
be942c2888 Revert "Add mg/m³ as a valid UOM for sensor/number Carbon Monoxide device class" (#153196) 2025-09-30 09:21:14 +00:00
Erik Montnemery
584c1fbd97 Revert "Add comment on conversion factor for Carbon monoxide on dependency molecular weight" (#153195) 2025-09-30 09:21:13 +00:00
Joost Lekkerkerker
abc5c6e2b4 Mark Konnected as Legacy (#153193) 2025-09-30 09:21:11 +00:00
Martin Hjelmare
d9de964035 Add hardware Zigbee flow strategy (#153190) 2025-09-30 09:21:10 +00:00
Artur Pragacz
bb02158d1a Filter out empty integration type in extended analytics (#153188) 2025-09-30 09:21:08 +00:00
Simone Chemelli
be10f097c7 Bump aioamazondevices to 6.2.7 (#153185) 2025-09-30 09:21:07 +00:00
cdnninja
7084bca783 Correct vesync water tank lifted key (#153173) 2025-09-30 09:21:05 +00:00
Michael
cd6f3a0fe5 Add newly added cpu temperatures to diagnostics in FRITZ!Tools (#153168) 2025-09-30 09:21:03 +00:00
starkillerOG
af2888331d Bump reolink-aio to 0.16.0 (#153161) 2025-09-30 09:21:02 +00:00
Allen Porter
b92e5d7131 Add missing translations for Model Context Protocol integration (#153147) 2025-09-30 09:21:00 +00:00
Tom Matheussen
f7265c85d0 Fix entities not being created when adding subentries for Satel Integra (#153139) 2025-09-30 09:20:58 +00:00
G Johansson
8466dbf69f Fix event range in workday calendar (#153128) 2025-09-30 09:20:57 +00:00
Robert Resch
2dd0d69bcd Bump deebot-client to 15.0.0 (#153125) 2025-09-30 09:20:56 +00:00
Luca Graf
6783c4ad83 Ignore gateway device in ViCare integration (#153097) 2025-09-30 09:20:54 +00:00
G Johansson
07d7f4e18d Add timeout to dnsip (to handle stale connections) (#153086) 2025-09-30 09:20:53 +00:00
Simone Chemelli
54b1749986 Remove redundant code for Alexa Devices (#153083) 2025-09-30 09:20:51 +00:00
G Johansson
eaf264361f Fix can exclude optional holidays in workday (#153082) 2025-09-30 09:20:50 +00:00
Kyle Worrall
d8f6f17a4f Fix for Hue Integration motion aware areas (#153079)
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-30 09:20:48 +00:00
Joakim Plate
9a969cea63 Ensure togrill detects disconnected devices (#153067) 2025-09-30 09:20:47 +00:00
Maciej Bieniek
ef16327b2b Add consumed energy sensor for Shelly pm1 and switch components (#153053) 2025-09-30 09:20:46 +00:00
Martin Hjelmare
a6a6261168 Improve hardware flow strings (#153034) 2025-09-30 09:20:44 +00:00
Erwin Douna
a01eb48db8 Portainer switch terminology to API token (#152958)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-09-30 09:20:43 +00:00
Christian McHugh
eb103a8d9a Fix: Set EPH climate heating as on only when boiler is actively heating (#152914) 2025-09-30 09:20:41 +00:00
Shay Levy
2b5f989855 Add Shelly EV charger sensors (#152722) 2025-09-30 09:20:40 +00:00
Thomas D
4e247a6ebe Prevent duplicate entities for Volvo integration (#151779) 2025-09-30 09:20:38 +00:00
Franck Nijhof
77f897a768 Bump version to 2025.10.0b4 2025-09-26 21:30:19 +00:00
Franck Nijhof
4f0a6ef9a1 Update Home Assistant base image to 2025.09.3 (#153064) 2025-09-26 21:30:12 +00:00
Franck Nijhof
66c6b0f5fc Bump version to 2025.10.0b3 2025-09-26 20:37:41 +00:00
Josef Zweck
dd01243391 Ensure token validity in lamarzocco (#153058) 2025-09-26 20:36:47 +00:00
SapuSeven
66c17e250a Add None-check for VeSync fan device.state.display_status (#153055) 2025-09-26 20:36:46 +00:00
DeerMaximum
723902e233 NINA Use better wording for filters (#153050) 2025-09-26 20:36:45 +00:00
Paul Bottein
59fdb9f3b5 Update frontend to 20250926.0 (#153049) 2025-09-26 20:36:44 +00:00
Martin Hjelmare
d83502514a Fix Thread flow abort on multiple flows (#153048) 2025-09-26 20:36:43 +00:00
Stefan Agner
08e81b2ba6 Update Home Assistant base image to 2025.09.2 (#153035) 2025-09-26 20:36:41 +00:00
Josef Zweck
1e808c965d Bump pylamarzocco to 2.1.1 (#153027) 2025-09-26 20:36:40 +00:00
Stefan Agner
563b58c9aa Bump to home-assistant/wheels@2025.09.1 (#153025) 2025-09-26 20:36:39 +00:00
Artur Pragacz
cf223880e8 Use satellite entity area in the assist pipeline (#153017) 2025-09-26 20:36:37 +00:00
J. Nick Koston
4058ca59ed Bump aioesphomeapi to 41.11.0 (#153014) 2025-09-26 20:36:36 +00:00
puddly
1386c01733 Allow ZHA discovery if discovery unique_id conflicts with config entry (#153009)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-26 20:36:35 +00:00
puddly
46504947f7 Bump ZHA to 0.0.73 (#153007) 2025-09-26 20:36:34 +00:00
Paulus Schoutsen
0a44682014 Push ESPHome discovery to ZJS addon (#153004) 2025-09-26 20:36:33 +00:00
Brandon Harvey
06a57473a9 Rename service to action in ESPHome (#152997) 2025-09-26 20:36:32 +00:00
Noah Husby
fbed66ef1f Bump aiorussound to 4.8.2 (#152988) 2025-09-26 20:36:30 +00:00
puddly
99a0380ec5 Ignore discovery for existing ZHA entries (#152984) 2025-09-26 20:36:29 +00:00
Simone Chemelli
68c51dc7aa Fix PIN failure if starting with 0 for Comelit SimpleHome (#152983) 2025-09-26 20:36:28 +00:00
lliwog
3d945b0fc5 Fix EZVIZ devices merging due to empty MAC addr (#152939) (#152981)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-26 20:36:27 +00:00
Erwin Douna
7b26a93d38 Portainer add ability to skip SSL verification (#152955) 2025-09-26 20:36:25 +00:00
Tom
1b2eab00be Add SSL options during config_flow for airOS (#150325)
Co-authored-by: Åke Strandberg <ake@strandberg.eu>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-09-26 20:36:24 +00:00
RogerSelwyn
750e849f09 Protect against last_comms being None (#149366)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-26 20:36:22 +00:00
Franck Nijhof
6aaddad56b Bump version to 2025.10.0b2 2025-09-25 18:19:29 +00:00
Paul Bottein
a5af974209 Update frontend to 20250925.1 (#152985) 2025-09-25 18:18:47 +00:00
Luke Lashley
09e45f6f54 Fix incorrect Roborock test (#152980) 2025-09-25 18:18:46 +00:00
Joost Lekkerkerker
d857d8850c Bump pySmartThings to 3.3.0 (#152977) 2025-09-25 18:18:45 +00:00
J. Nick Koston
ccc50f2412 Bump aioesphomeapi to 41.10.0 (#152975)
Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-09-25 18:18:43 +00:00
Maciej Bieniek
3905723900 Bump accuweather to version 4.2.2 (#152965) 2025-09-25 18:18:42 +00:00
Simone Chemelli
cee88473a2 Remove deprecated sensors and update remaning for Alexa Devices (#151230) 2025-09-25 18:18:40 +00:00
Daniel Potthast
cdf613d3f8 Update mvglive component (#146479)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-25 18:18:39 +00:00
Franck Nijhof
156a0f1a3d Bump version to 2025.10.0b1 2025-09-25 09:37:33 +00:00
Paul Bottein
cc2a5b43dd Update frontend to 20250925.0 (#152945) 2025-09-25 09:37:03 +00:00
Erwin Douna
731064f7e9 Portainer fix unique entity (#152941)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 08:19:21 +00:00
Sab44
2f75661c20 Bump librehardwaremonitor-api to version 1.4.0 (#152938) 2025-09-25 08:19:20 +00:00
Paulus Schoutsen
be6f056f30 Prevent common control calling async methods from thread (#152931)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-09-25 08:19:18 +00:00
Franck Nijhof
79599e1284 Add block Spook < 4.0.0 as breaking Home Assistant (#152930) 2025-09-25 08:19:17 +00:00
Paulus Schoutsen
a255585ab6 Remove some more domains from common controls (#152927) 2025-09-25 08:19:15 +00:00
J. Nick Koston
e9bde225fe Bump aioesphomeapi to 41.9.4 (#152923) 2025-09-25 08:19:14 +00:00
Franck Nijhof
d9521ac2a0 Bump to home-assistant/wheels@2025.09.0 (#152920) 2025-09-25 08:19:13 +00:00
J. Nick Koston
d8b24ccccd Bump aioesphomeapi to 41.9.3 to fix segfault (#152912) 2025-09-25 08:19:12 +00:00
J. Nick Koston
b4417a76d5 Fix ESPHome reauth not being triggered on incorrect password (#152911) 2025-09-25 08:19:10 +00:00
Simone Chemelli
274f6eb54a Update IQS to platinum for Comelit SimpleHome (#152906) 2025-09-25 08:19:09 +00:00
Simone Chemelli
21a5aaf35c Update IQS to platinum for Alexa Devices (#152905) 2025-09-25 08:19:07 +00:00
Luke Lashley
05820a49d0 Fix logical error when user has no Roborock maps (#152752) 2025-09-25 08:19:06 +00:00
Franck Nijhof
17b12d29af Bump version to 2025.10.0b0 2025-09-24 18:57:19 +00:00
Marc Mueller
9cc78680d6 Fix lg_thinq test RuntimeWarning (#152910) 2025-09-24 21:28:49 +03:00
Simone Chemelli
14d42e43bf Add dynamic devices management for Alexa Devices (#151975) 2025-09-24 19:00:35 +02:00
Simone Chemelli
ed5f5d4b33 Add dynamic devices management for Comelit SimpleHome (#152137) 2025-09-24 18:57:56 +02:00
Kinachi249
c3ba086fad Add new Cync by GE integration (#149848)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-24 18:43:50 +02:00
Abílio Costa
7b5314605c Revert "Rename function arguments in modbus (#152814)" (#152904) 2025-09-24 17:25:01 +01:00
Karsten Bade
3a806d6603 Add dc:title support for Sonos sharelinks (#152774)
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
2025-09-24 17:23:58 +01:00
starkillerOG
6dd33f900d Add support for Reolink chime connected to Home Hub (#151199) 2025-09-24 18:07:23 +02:00
Paul Bottein
2844bd474a Update frontend to 20250924.0 (#152901) 2025-09-24 18:05:13 +02:00
Artur Pragacz
d865fcf999 Do not include capabilities in extended analytics (#152900)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-09-24 11:58:44 -04:00
Bouwe Westerdijk
79a2fc5a01 Snapshot testing for Plugwise Select platform (#152827) 2025-09-24 17:51:04 +02:00
alorente
19d87abb8a Add Q-Adapt to Airzone integration (#151945) 2025-09-24 17:43:32 +02:00
Joris Pelgröm
c4de46a85b Add number platform to LetPot integration (#151092) 2025-09-24 17:41:36 +02:00
epenet
e79a434d9b Use DeviceCategory in Tuya remaining platforms (#152890) 2025-09-24 17:39:46 +02:00
Manu
9a801424c7 Fix deleting message filters in ntfy integration (#152783) 2025-09-24 17:38:40 +02:00
Paulus Schoutsen
5cb186980a Mark MQTT as service (#152899) 2025-09-24 11:35:23 -04:00
Ravaka Razafimanantsoa
1629ade97f Add Smart Meter B Route integration (#123446)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-24 17:31:30 +02:00
puddly
ccf0011ac2 Skip ignored discovery entries when showing migrate/setup config flow steps for ZHA and Hardware (#152895) 2025-09-24 17:31:04 +02:00
puddly
70077511a3 Unload ZHA integration before adapter migration (#152896) 2025-09-24 17:28:55 +02:00
Petar Petrov
dfbaf66021 Add progress step decorator for easier config flows (#152739)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-09-24 18:18:42 +03:00
Richard Polzer
62cea48a58 Fix typo in ekeybionyx strings.json (#152889) 2025-09-24 15:46:22 +01:00
Michael Hansen
c493c7dd67 Bump intents and fix tests (#152893) 2025-09-24 09:24:42 -05:00
Maciej Bieniek
fdaceaddfd Add new virtual integration Neo (#152886) 2025-09-24 14:57:22 +02:00
epenet
a2f4073d54 Use DeviceCategory in Tuya more platforms (#152885) 2025-09-24 14:40:25 +02:00
epenet
2d01a99ec2 Bump renault-api to 0.4.1 (#152883) 2025-09-24 12:44:33 +01:00
epenet
311d4c4262 Use DeviceCategory in Tuya binary sensor (#152882) 2025-09-24 13:31:44 +02:00
Erik Montnemery
e14f5ba44d Fix misleading + unclear comment in homeassistant.const (#152878) 2025-09-24 13:22:32 +02:00
Artur Pragacz
9babc85517 Add analytics to core files (#152877) 2025-09-24 13:21:40 +02:00
Marc Mueller
332a3fad3c Fix mypy errors (#152879) 2025-09-24 13:09:32 +02:00
Michael
8782aa4f60 Hide asserts behind TYPE_CHECKING in Synology DSM (#152880) 2025-09-24 12:49:44 +02:00
jan iversen
475b84cc5f Remove codeowner. (#152869) 2025-09-24 12:43:22 +02:00
Artur Pragacz
0f904d418b Filter out integration types in extended analytics (#152874) 2025-09-24 12:32:30 +02:00
Artur Pragacz
4ea4eec2d8 Remove analytics platform in template (#152876) 2025-09-24 12:32:14 +02:00
Artur Pragacz
afefa16615 Remove analytics platform in automation (#152875) 2025-09-24 12:29:33 +02:00
Martin Hjelmare
1dccbee45c Remove hardware flow thread confirm step after install (#152868) 2025-09-24 11:28:10 +01:00
Simone Chemelli
711a56db2f Add dynamic devices management for UptimeRobot (#152139) 2025-09-24 10:56:56 +01:00
Joost Lekkerkerker
9d1c7dadff Make SmartThings AC preset modes translatable (#152833) 2025-09-24 10:55:28 +01:00
Richard Polzer
7d1953e387 Add Ekey Bionyx integration (#139132)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-24 11:54:27 +02:00
Martin Hjelmare
023ecf2a64 Patch async_setup_entry in hardware integration flow tests (#152871) 2025-09-24 11:49:01 +02:00
epenet
934db458a3 Simplify access to Tuya device manager in async_setup_entry (#152859) 2025-09-24 11:47:28 +02:00
epenet
0a6ae3b52a Add enum for Tuya device categories (#152858) 2025-09-24 11:46:33 +02:00
Patrick
bdd0b74d51 Enhance Synology DSM handling of external USB drives (#145943)
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2025-09-24 11:26:22 +02:00
Norbert Rittel
8837f2aca7 Capitalize "Auto Cycle Link" as feature name in smartthings (#152864) 2025-09-24 11:11:35 +02:00
Artur Pragacz
403cd2d8ef Filter out custom integrations in extended analytics (#152820) 2025-09-24 10:24:42 +02:00
Erik Montnemery
ddfc528d63 Fix apparent copy-paste error in tests of trigger helper (#152855) 2025-09-24 08:38:32 +02:00
Nick Kuiper
ddea2206c3 Add start charge session action for blue current integration. (#145446) 2025-09-24 08:11:33 +02:00
J. Nick Koston
32aacac550 Fix async_get_scanner return type for BleakScanner compatibility (#152840) 2025-09-23 23:14:08 -05:00
Luke Lashley
dadba274aa Bump python-roborock to 2.47.1 (#152844) 2025-09-23 22:16:32 -04:00
TheJulianJES
14b5b9742c Bump ZHA to 0.0.72 (#152850) 2025-09-23 22:15:00 -04:00
Michael Hansen
a0be737925 Auto select first active wake word (#152562) 2025-09-23 17:19:04 -04:00
Marcel van der Veldt
ff47839c61 Fix support for new Hue bulbs with very wide color temperature support (#152834) 2025-09-23 16:56:16 -04:00
Copilot
9ba7dda864 Rename logbook integration to "Activity" in user-facing strings (#150950) 2025-09-23 22:55:11 +02:00
J. Nick Koston
911f901d9d Bump aioesphomeapi to 41.9.0 (#152841) 2025-09-23 22:54:31 +02:00
Marcel van der Veldt
2008a73657 Add support for Hue MotionAware sensors (#152811)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-09-23 16:52:09 -04:00
Pete Sage
60bf298ca6 File add read_file action with Response (#139216) 2025-09-23 22:25:56 +02:00
jan iversen
3bc2ea7b5f Use DOMAIN not MODBUS_DOMAIN (#152823) 2025-09-23 22:07:24 +02:00
Kevin Stillhammer
3bac6b86df Fix multiple_here_travel_time_entries issue description (#152839) 2025-09-23 22:06:06 +02:00
Stefan Agner
20293e2a11 Bump aiohasupervisor to 0.3.3b0 (#152835)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 22:05:38 +02:00
Martin Hjelmare
15cc28e6c1 Move first probe firmware to firmware progress in hardware flow (#152819) 2025-09-23 22:03:04 +02:00
puddly
874ca1323b Simplified ZHA adapter migration and setup flow (#152389) 2025-09-23 22:00:31 +02:00
Ludovic BOUÉ
ca186925af Add Matter Thermostat OutdoorTemperature sensor (#152632) 2025-09-23 21:28:24 +02:00
andreimoraru
2ab051b716 Bump yt-dlp to 2025.09.23 (#152818) 2025-09-23 20:03:53 +01:00
epenet
a2a726de34 Rename function arguments in modbus (#152814) 2025-09-23 20:54:52 +02:00
Sarah Seidman
5d543d2185 Bump pydroplet version to 2.3.3 (#152832) 2025-09-23 19:36:06 +01:00
epenet
a78c909b34 Rename cover property in tuya (#152822) 2025-09-23 19:35:47 +01:00
Artur Pragacz
f00ab80d17 Add analytics platform to template (#152824) 2025-09-23 13:53:53 -04:00
Joakim Sørensen
014881d985 Fix error handling in subscription info retrieval and update tests (#148397)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-23 19:53:12 +02:00
Artur Pragacz
29a42a8e58 Add analytics platform to automation (#152828) 2025-09-23 13:52:58 -04:00
Rohan Kapoor
3f70084d7f Handle ignored and disabled entries correctly in zeroconf discovery for Music Assistant (#152792)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-23 10:15:52 -07:00
Felipe Santos
b1ae9c95c9 Add a switch entity for add-ons (#151431)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-09-23 17:27:35 +02:00
Artur Pragacz
8be79ecdb0 Move conversation trigger registration to manager (#152749) 2025-09-23 10:01:46 -05:00
Jan Bouwhuis
f6b8aa893b Add mqtt image subentry support (#151586) 2025-09-23 16:31:40 +02:00
jan iversen
c867026bdd Add test to validate multiple host/port for modbus. (#152658)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-09-23 16:03:42 +02:00
Kevin Stillhammer
da3a164e66 Change here_travel_time update interval to 30min (#147222) 2025-09-23 15:56:13 +02:00
Josef Zweck
32688e1108 Bump aioacaia to 0.1.17 (#152815) 2025-09-23 15:29:16 +02:00
Artur Pragacz
4305ea9b4c Create analytics platform (#151974) 2025-09-23 09:16:37 -04:00
epenet
61153ec456 Deduplicate code in modbus service call (#152808)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
2025-09-23 13:44:28 +02:00
Marcel van der Veldt
9e4a2d5fa9 Bump aiohue to 4.8.0 (#152807) 2025-09-23 13:39:58 +02:00
Marcel van der Veldt
72e608918b Handle toggling of the 'expose_to_ha' setting in Music Assistant integration (#152779) 2025-09-23 13:22:16 +02:00
Erik Montnemery
86db60c442 Freeze time in irm_kmi tests (#152810) 2025-09-23 13:21:59 +02:00
cdnninja
25806615a9 Bump pyvesync to 3.0.0 (#152726) 2025-09-23 13:00:59 +02:00
Paulus Schoutsen
a0f67381e5 Allow configuring Z-Wave JS to talk via ESPHome (#152590)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-09-23 12:58:36 +02:00
Jan Čermák
90bfadda9b Support all reported preset modes in Smartthings climate (#148056)
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-09-23 12:56:27 +02:00
Maciej Bieniek
0f8e700965 Add a cable unplugged sensor for Shelly Flood Gen4 (#152559) 2025-09-23 12:55:44 +02:00
Jan Čermák
21d4ed2837 Add profiler service for dumping sockets used by HA (#152440)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-09-23 12:55:29 +02:00
Luke Lashley
ce363b3835 Make Roborock load_multi_map always cloud dependent. (#152698) 2025-09-23 12:55:07 +02:00
G Johansson
dd3e6b8df5 Only load selected processes in systemmonitor (#152777) 2025-09-23 12:41:31 +02:00
epenet
abbf8390ac Move switch to valve for Tuya sfkzq category (#152478) 2025-09-23 12:39:37 +02:00
epenet
689039959c Improve current_state support in Tuya curtains (#152801) 2025-09-23 12:38:47 +02:00
epenet
52c25cfc88 Rename modbus internal variable (#152805) 2025-09-23 12:14:05 +02:00
G Johansson
00b2017767 Fix resource and payload template in scrape (#152670) 2025-09-23 11:59:00 +02:00
Martin Hjelmare
dd7f7be6ad Move hardware thread add-on install after firmware install (#152800) 2025-09-23 11:47:30 +02:00
Robert Resch
22709506c6 Add Ecovacs custom water amount entity (#152782)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 11:21:11 +02:00
Lukas
f0c0492375 Add MAC address to Pooldose device (#152760)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 11:11:08 +02:00
Robert Resch
58459cb80f Bump deebot-client to 14.0.0 (#152448) 2025-09-23 10:47:37 +02:00
epenet
a19e378447 Add Tuya test fixture files (#152795) 2025-09-23 10:44:36 +02:00
J. Nick Koston
38a5a3ed4b Handle wrong ESPHome device without encryption appearing at the configured IP (#152758) 2025-09-23 10:27:13 +02:00
Matthias Lohr
e76bed4a83 Add reconfigure flow to tolo (#137609)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-09-23 10:21:06 +02:00
Karsten Bade
d73309ba60 Bump SoCo to 0.30.12 (#152797) 2025-09-23 09:49:33 +02:00
Przemko92
19fdea024c Bump compit-inext-api to 0.3.1 (#152781) 2025-09-23 09:48:53 +02:00
Manu
a3cfd7f707 Fix coordinator data handling in Bring integration (#152786) 2025-09-23 09:03:01 +02:00
Petro31
3dd941eff7 Fix section and entity variable resolution for template platforms (#149660)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-23 08:12:24 +02:00
J. Nick Koston
d389141aee Bump aioesphomeapi to 41.6.0 (#152787) 2025-09-22 21:43:20 -05:00
Rohan Kapoor
3c542b8d43 Only update Music Assistant URL on zeroconf discovery when current URL is unreachable (#152030) 2025-09-22 13:49:41 -07:00
starkillerOG
2367df89d9 Bump reolink-aio to 0.15.2 (#152775) 2025-09-22 21:39:06 +01:00
Manu
7bfdfb3fc7 Bump pynecil to v4.2.0 (#152776) 2025-09-22 21:35:19 +01:00
G Johansson
485916265a Fix manual updating of Nord Pool sensors (#152773) 2025-09-22 22:04:17 +02:00
Stefan Agner
1bb3c96fc1 Drop Windows compatibility code from systemmonitor integration (#152545) 2025-09-22 21:26:26 +02:00
Artur Pragacz
4eaf6784af Use satellite entity area in the default agent (#152762) 2025-09-22 13:34:31 -05:00
Abílio Costa
7b7265a6b0 Revert "Add EZVIZ battery camera power status and online status sensor (#146822)" (#152767) 2025-09-22 19:03:32 +02:00
Thomas D
9059e3dadc Prepare Volvo integration for new platforms (#152042) 2025-09-22 18:41:44 +02:00
J. Nick Koston
d9d42b3ad5 Pass timezone to aioesphomeapi to ensure HA timezone takes precedence (#152756) 2025-09-22 09:54:15 -06:00
epenet
d565fb3cb4 Bump mcp to 1.14.1 (#152737) 2025-09-22 17:33:48 +02:00
G Johansson
6e93e480d1 Use automatic reload options flow in integration (#152686) 2025-09-22 15:27:19 +01:00
Maciej Bieniek
5a3570702d Add re-auth flow to AccuWeather integration (#152755) 2025-09-22 16:27:09 +02:00
Abílio Costa
b26b1df143 Fix unitless converter missing valid units (#152665) 2025-09-22 15:19:31 +01:00
Manu
fdbff76733 Add collapse checklist field to Habitica create/update task actions (#150988) 2025-09-22 16:16:47 +02:00
epenet
018d59a892 Drop hass argument from service extraction helpers (#152738) 2025-09-22 16:08:07 +02:00
Andrew Jackson
4b6dd0eb8f Add optional language to Mastodon post action (#151072) 2025-09-22 16:01:09 +02:00
Wendelin
b7db87bd3d Update regex for core logs path to include latest logs (#152747) 2025-09-22 14:07:49 +01:00
Jules Dejaeghere
86dc453c55 Add integration for Belgian weather provider meteo.be (#144689)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-22 13:28:41 +02:00
Abílio Costa
a4f2c88c7f Add TriggerConfig to reduce ambiguity (#152563) 2025-09-22 12:24:47 +01:00
Erik Montnemery
3cdb894e61 Small improvement of exposed_entities test (#152744) 2025-09-22 13:16:02 +02:00
Bouwe Westerdijk
cb837aaae5 Number snapshot testing for Plugwise (#152673) 2025-09-22 11:55:57 +02:00
G Johansson
82443ded34 Use already cached data in Nord Pool if valid (#152664) 2025-09-22 11:55:00 +02:00
Retha Runolfsson
71cc3b7fcd Add K11+ Vacuum for switchbot integration (#152643) 2025-09-22 11:51:39 +02:00
G Johansson
e5658f9747 Use automatic reload options flow in statistics (#152682) 2025-09-22 11:48:58 +02:00
G Johansson
868ded141f Use automatic reload options flow in threshold (#152684) 2025-09-22 11:48:37 +02:00
LG-ThinQ-Integration
1151fa698d Add energy usage sensors of ThinQ devices. (#152141)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-09-22 11:47:18 +02:00
Paulus Schoutsen
2796d6110a Split up media source integration (#152721)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 11:46:24 +02:00
Tom Matheussen
844b97bd32 Add Satel Integra diagnostics (#152621)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-22 11:38:26 +02:00
Lukas
286b2500bd Pooldose: Add Dhcp discovery (#152253)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-22 11:31:40 +02:00
Joshua Leaper
4b7746ab51 Bump nessclient to 1.3.1 (#152700) 2025-09-22 11:01:04 +03:00
Abílio Costa
ca1c366f4f Remove unused var from llm helper (#152724) 2025-09-22 09:57:16 +02:00
epenet
de42ac14ac Drop unused hass argument from internal helper (#152733) 2025-09-22 09:56:52 +02:00
J. Nick Koston
7f7bd5a97f Bump aioesphomeapi to 41.5.0 (#152730) 2025-09-22 09:56:20 +02:00
epenet
8a70a1badb Drop hass argument from verify_domain_control (#147946) 2025-09-22 08:15:41 +02:00
Shay Levy
181741cab6 Use component role in Shelly sensor platform (#152710)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 01:03:29 +03:00
Maciej Bieniek
1e14fb6dab Use the common unique_id schema in the Shelly button platform (#152707) 2025-09-21 19:34:50 +02:00
Maciej Bieniek
2b6a125927 Support Shelly presencezone component (#152393)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2025-09-21 19:27:40 +02:00
Abílio Costa
e61ad10708 Split sensor unit long condition (#152668) 2025-09-21 16:49:13 +01:00
Shay Levy
5177f9e8c2 Add support for Shelly object based entities (#152046)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2025-09-21 17:08:43 +03:00
Luke Lashley
850aeeb5eb Explicitly connect to the Roborock API before sending messages (#152697) 2025-09-21 12:37:15 +02:00
J. Nick Koston
a1b9061060 Bump aiohomekit to 3.2.18 (#152694) 2025-09-21 12:14:21 +02:00
Maciej Bieniek
0ec1f27489 Use DeviceClass.PM4 in NAM integration (#152703) 2025-09-21 11:59:09 +02:00
J. Diego Rodríguez Royo
befc93bc73 Delete Home Connect alarm clock entity from time platform (#152188) 2025-09-21 10:23:51 +01:00
Luke Lashley
1526d953bf Make Roborock A01 initilization threadsafe (#152699) 2025-09-21 07:52:26 +02:00
G Johansson
d38082a5c8 Use automatic reload options flow in Scrape (#152681) 2025-09-20 22:02:23 +01:00
G Johansson
42850421d2 Use automatic reload options flow in wake_on_lan (#152683) 2025-09-20 22:01:11 +01:00
avee87
21a835c4b4 Expose pressure as a separate sensor for metoffice (#152685) 2025-09-20 21:59:14 +01:00
Manu
e9294dbf72 Bump pyecotrend-ista to v3.4.0 (#152678) 2025-09-20 21:52:51 +01:00
Daniel Hjelseth Høyer
5c4dfbff1b Update Tibber lib 0.32.1 (#152677) 2025-09-20 20:52:24 +02:00
G Johansson
abe628506d Use OptionsFlowWithReload in pvpc_hourly_pricing (#151255) 2025-09-20 18:45:06 +02:00
Franck Nijhof
12cc0ed18d Refactor template engine: Extract raise_no_default() into helper module (#152661)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-20 17:20:05 +01:00
Franck Nijhof
8ca7562390 Merge branch 'master' into dev 2025-09-20 14:28:37 +00:00
GSzabados
942f7eebb1 Add PM4 device class for Ecowitt (#152568) 2025-09-20 14:40:21 +02:00
Franck Nijhof
1a167e6aee Refactor template engine: Extract context and render info (#152630) 2025-09-20 14:15:24 +02:00
Stephan van Rooij
9531ae10f2 Remove volvooncall (#150725)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-09-20 14:08:53 +02:00
tronikos
bfc9616abf Deprecate google_generative_ai_conversation.generate_content (#152644) 2025-09-20 07:50:31 -04:00
Jan Bouwhuis
054a5d751a Organize order MQTT subentry (test) globals and translation strings (#152576) 2025-09-20 13:24:30 +02:00
Andrea Turri
a43ba4f966 Miele add new program phase mapping (#152647) 2025-09-20 11:33:12 +02:00
J. Nick Koston
1a5cae125f Handle unparsable responses during HomeKit Controller initial polling (#152636) 2025-09-19 22:51:21 -05:00
Franck Nijhof
f3b9bda876 2025.9.4 (#152634) 2025-09-19 23:08:58 +02:00
Yevhenii Vaskivskyi
3f3aaa2815 Bump asusrouter to 1.21.0 (#151607) 2025-09-19 20:38:05 +00:00
Franck Nijhof
6dc7870779 Bump version to 2025.9.4 2025-09-19 20:07:56 +00:00
G Johansson
be83416c72 Bump holidays to 0.81 (#152569) 2025-09-19 20:07:27 +00:00
G Johansson
c745ee18eb Bump holidays to 0.80 (#152306) 2025-09-19 20:07:25 +00:00
tronikos
cf907ae196 Bump opower to 0.15.5 (#152531) 2025-09-19 20:05:04 +00:00
Pete Sage
8eee53036a Fix Sonos set_volume float precision issue (#152493) 2025-09-19 20:00:42 +00:00
Alexandre CUER
b37237d24b Bump pyemoncms to 0.1.3 (#152436) 2025-09-19 20:00:41 +00:00
Matthias Alphart
950e758b62 Fix KNX UI schema missing DPT (#152430) 2025-09-19 20:00:40 +00:00
Josef Zweck
9cd940b7df Add La Marzocco specific client headers (#152419) 2025-09-19 20:00:38 +00:00
Josef Zweck
10b186a20d Bump pylamarzocco to 2.1.0 (#152364) 2025-09-19 20:00:37 +00:00
Imeon-Energy
757aec1c6b Bump imeon_inverter_api to 0.4.0 (#152351)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-19 20:00:36 +00:00
Allen Porter
0b159bdb9c Update authorization server to prefer absolute urls (#152313)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-19 20:00:34 +00:00
J. Nick Koston
8728312e87 Bump aiohomekit to 3.2.17 (#152297) 2025-09-19 20:00:33 +00:00
Åke Strandberg
bbb67db354 Add proper error handling for /actions endpoint for miele (#152290) 2025-09-19 20:00:31 +00:00
J. Nick Koston
265f5da21a Bump bluetooth-auto-recovery to 1.5.3 (#152256) 2025-09-19 20:00:30 +00:00
J. Nick Koston
54859e8a83 Bump aiohomekit to 3.2.16 (#152255) 2025-09-19 20:00:28 +00:00
Sean Dague
c87dba878d Upgrade waterfurnace to 1.2.0 (#152241) 2025-09-19 20:00:27 +00:00
J. Nick Koston
8d8e008123 Fix HomeKit Controller overwhelming resource-limited devices by batching characteristic polling (#152209) 2025-09-19 20:00:25 +00:00
Yevhenii Vaskivskyi
b30667a469 Fix bug with the hardcoded configuration_url (asuswrt) (#151858) 2025-09-19 20:00:23 +00:00
Manu
8920c548d5 Bump habiticalib to v0.4.5 (#151720) 2025-09-19 20:00:22 +00:00
Manu
eac719f9af Bump habiticalib to v0.4.4 (#151332) 2025-09-19 20:00:20 +00:00
Aidan Timson
71c274cb91 Add power usage sensor to System Bridge (#152625) 2025-09-19 21:16:30 +02:00
Retha Runolfsson
d4902361e6 Bump pySwitchbot to 0.71.0 (#152597) 2025-09-19 22:04:42 +03:00
Norbert Rittel
f63eee3889 Fix typo and sentence-casing in honeywell exception string (#152619) 2025-09-19 17:07:46 +02:00
Aidan Timson
21bfe610d1 Update systembridgeconnector to 5.1.0 (#152623) 2025-09-19 17:07:15 +02:00
J. Nick Koston
21c174e895 Bump aioesphomeapi to 41.4.0 (#152618)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-09-19 08:20:00 -06:00
Simon Roberts
ec148e0459 Add PM4 (particulates < 4μm) sensor and number device classes (#112867)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-09-19 08:12:09 -06:00
Maciej Bieniek
286763b998 Fix KeyError for Shelly Duo Bulb Gen3 (#152612) 2025-09-19 07:22:48 -06:00
Shay Levy
5f88122a2b Fix Shelly Wall Display virtual button platform (#152582) 2025-09-19 11:44:08 +03:00
Åke Strandberg
31968d16ab Refactor miele program phase codes part 2(3) (#144180) 2025-09-19 09:54:24 +02:00
epenet
c125554817 Rename modbus base entities (#152595) 2025-09-19 09:17:31 +02:00
Abílio Costa
10f2955d34 Update Whirlpool quality scale to silver (#152505) 2025-09-19 08:30:10 +02:00
Paulus Schoutsen
55712b784c Bump aioesphomeapi to 41.3.0 (#152588) 2025-09-18 22:49:03 -04:00
Paulus Schoutsen
fe3a929556 Fix reolink test (#152587) 2025-09-18 21:03:13 -04:00
J. Nick Koston
534801e80d Migrate yale and august integrations to use new lock API (#152579) 2025-09-18 17:20:38 -05:00
J. Nick Koston
8aeda5a0c0 Bump aioesphomeapi to 41.2.0 (#152578) 2025-09-19 01:03:59 +03:00
Ville Skyttä
eb1cbbc75c Upgrade upcloud-api to 2.8.0 (#152577) 2025-09-19 01:03:31 +03:00
Przemko92
fa8a4d7098 Add Compit integration (#132164)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-18 21:53:49 +01:00
Andre Lengwenus
2623ebac4d Bump pypck to 0.8.12 (#152573) 2025-09-18 23:34:37 +03:00
Michael Hansen
1746c51ce4 Fix error with pipeline device removal due to multiple selects (#152560) 2025-09-18 14:34:03 -05:00
Joakim Sørensen
8b984a2105 Remove ludeeus as a codeowner for analytics (#152558) 2025-09-18 22:08:22 +03:00
Allen Porter
ebee370a56 Bump python roborock to 2.44.1 (#152557) 2025-09-18 21:51:16 +03:00
starkillerOG
dabd096587 Add color temperature support to Reolink light entity (#152546) 2025-09-18 21:48:18 +03:00
Simone Chemelli
21399818af Remove stale devices for Comelit SimpleHome (#151519) 2025-09-18 19:43:38 +01:00
G Johansson
4354214fbf Bump holidays to 0.81 (#152569) 2025-09-18 20:35:21 +02:00
Erik Montnemery
5bd39804f1 Remove EntityComponent.async_register_legacy_entity_service (#152539) 2025-09-18 20:34:25 +02:00
Norbert Rittel
6d3ad3ab9c Replace "iCloud account" with "Apple Account" (#152561) 2025-09-18 18:39:54 +02:00
Petar Petrov
4c212bdcd4 Enable thread migration for ZBT integration (#152550) 2025-09-18 18:33:06 +03:00
Petar Petrov
b91b39580f Add migrate options to ZBT protocol picker (#152532) 2025-09-18 11:13:58 -04:00
Jan Bouwhuis
472d70b6c9 Add comment on conversion factor for Carbon monoxide on dependency molecular weight (#152535) 2025-09-18 15:36:12 +02:00
tronikos
017a84a859 Bump opower to 0.15.5 (#152531) 2025-09-18 14:29:27 +03:00
starkillerOG
d184540967 Bump reolink-aio to 0.15.1 (#152533) 2025-09-18 14:28:16 +03:00
Erik Montnemery
1740984b3b Improve comments in SelectedEntities (#152540) 2025-09-18 14:12:33 +03:00
droans
4db8592c61 Add support for overriding entity_picture to universal (#149387) 2025-09-18 11:59:29 +02:00
Stefan Agner
27e630c107 Make systemmonitor tests timezone independent (#152537) 2025-09-18 12:58:09 +03:00
Paulus Schoutsen
3499ed7a98 2025.9.3 (#152237) 2025-09-13 08:44:08 -04:00
Paulus Schoutsen
2c809d5903 Bump version to 2025.9.3 2025-09-13 12:14:15 +00:00
J. Nick Koston
40988198f3 Bump habluetooth to 5.6.4 (#152227) 2025-09-13 12:14:08 +00:00
Franck Nijhof
ab5d1d27f1 2025.9.2 (#152198) 2025-09-12 23:10:27 +02:00
wollew
1c10b85fed Use position percentage for closed status in Velux (#151679)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:39:06 +00:00
Franck Nijhof
91a7db08ff Bump version to 2025.9.2 2025-09-12 20:20:56 +00:00
Bram Kragten
a764d54123 Update frontend to 20250903.5 (#152170) 2025-09-12 20:20:44 +00:00
Joakim Sørensen
dc09e33556 Bump hass-nabucasa from 1.1.0 to 1.1.1 (#152147) 2025-09-12 20:20:43 +00:00
Simone Chemelli
14173bd9ec Fix reauth for Alexa Devices (#152128) 2025-09-12 20:20:42 +00:00
Jan Bouwhuis
d2e7537629 Fix supported _color_modes attribute not set for on/off MQTT JSON light (#152126) 2025-09-12 20:20:41 +00:00
J. Nick Koston
9a165a64fe Fix DoorBird being updated with wrong IP addresses during discovery (#152088)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:20:39 +00:00
Abílio Costa
9c749a6abc Fix duplicated IP port usage in Govee Light Local (#152087) 2025-09-12 20:20:38 +00:00
J. Nick Koston
2e33222c71 Fix HomeKit Controller stale values at startup (#152086)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-09-12 20:20:37 +00:00
Maciej Bieniek
ab1c2c4f70 Bump accuweather to version 4.2.1 (#152029) 2025-09-12 20:20:36 +00:00
Joost Lekkerkerker
529219ae69 Bump yt-dlp to 2025.09.05 (#152006) 2025-09-12 20:20:35 +00:00
J. Nick Koston
d6ce71fa61 Bump habluetooth to 5.6.2 (#151985) 2025-09-12 20:20:33 +00:00
peteS-UK
e5b67d513a Fix playlist media_class_filter in search_media for squeezebox (#151973) 2025-09-12 20:20:32 +00:00
peteS-UK
a547179f66 Fix for squeezebox track content_type (#151963) 2025-09-12 20:20:31 +00:00
epenet
8c61788a7d Fix invalid logger in Tuya (#151957) 2025-09-12 20:19:26 +00:00
J. Nick Koston
6b934d94db Bump habluetooth to 5.6.0 (#151942) 2025-09-12 20:18:38 +00:00
J. Nick Koston
d30ad82774 Bump bleak-esphome to 3.3.0 (#151922)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-12 20:18:37 +00:00
J. Nick Koston
4618b33e93 Bump habluetooth to 5.5.1 (#151921) 2025-09-12 20:18:36 +00:00
epenet
d6299094db Fix _is_valid_suggested_unit in sensor platform (#151912) 2025-09-12 20:18:35 +00:00
Simone Chemelli
087d9d30c0 Avoid cleanup/recreate of device_trackers not linked to a device for Vodafone Station (#151904) 2025-09-12 20:18:33 +00:00
Simone Chemelli
f07890cf5c Bump aiovodafone to 1.2.1 (#151901) 2025-09-12 20:18:31 +00:00
Avi Miller
e5b78cc481 Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898)
Signed-off-by: Avi Miller <me@dje.li>
2025-09-12 20:18:30 +00:00
Manu
12b409d8e1 Bump aiontfy to v0.5.5 (#151869) 2025-09-12 20:18:29 +00:00
Maciej Bieniek
def5408db8 Use native_visibility property instead of visibility for OpenWeatherMap weather entity (#151867) 2025-09-12 20:18:28 +00:00
Robert Resch
f105b45ee2 Bump aioecowitt to 2025.9.1 (#151859) 2025-09-12 20:18:26 +00:00
wollew
9d904c30a7 fix rain sensor for Velux GPU windows (#151857) 2025-09-12 20:18:25 +00:00
Martins Sipenko
99b047939f Update pysmarty2 to 0.10.3 (#151855) 2025-09-12 20:18:24 +00:00
J. Nick Koston
3a615908ee Bump aioharmony to 0.5.3 (#151853) 2025-09-12 20:18:23 +00:00
David Knowles
baff541f46 Bump pydrawise to 2025.9.0 (#151842) 2025-09-12 20:18:22 +00:00
jan iversen
6d8c35cfe9 removed assert fron entity in modbus. (#151834) 2025-09-12 20:18:20 +00:00
jan iversen
b8d9883e74 max_temp / min_temp in modbus light could only be int, otherwise an assert was provoked. (#151833) 2025-09-12 20:18:19 +00:00
jan iversen
c3c65af450 Allow delay > 1 in modbus. (#151832) 2025-09-12 20:18:18 +00:00
Maciej Bieniek
3af8616764 Mark Tractive switches as unavailable when tacker is in the enegy saving zone (#151817) 2025-09-12 20:18:17 +00:00
Matthias Alphart
64ec4609c5 Fix KNX Light - individual color initialisation from UI config (#151815) 2025-09-12 20:18:15 +00:00
Matthias Alphart
c78bc26b83 Fix KNX BinarySensor config_store data (#151808) 2025-09-12 20:18:14 +00:00
J. Nick Koston
0c093646c9 Bump habluetooth to 5.3.1 (#151803) 2025-09-12 20:18:13 +00:00
Simone Chemelli
1b27acdde0 Improve config entry migration for edge cases in Alexa Devices (#151788) 2025-09-12 20:18:12 +00:00
Ludovic BOUÉ
9dafc0e02f Remove device class for Matter NitrogenDioxideSensor (#151782)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-12 20:18:10 +00:00
Tsvi Mostovicz
0091dafcb0 Revert "Jewish Calendar add coordinator " (#151780) 2025-09-12 20:18:09 +00:00
Jan Bouwhuis
b387acffb7 Fix update of the entity ID does not clean up an old restored state (#151696)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-12 20:18:08 +00:00
blotus
36b3133fa2 Fix support for Ecowitt soil moisture sensors (#151685) 2025-09-12 20:18:06 +00:00
hbludworth
fe01e96012 Fix Aladdin Connect state not updating (#151652)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-12 20:18:05 +00:00
Thomas55555
0b56ec16ed Add event entity on websocket ready in Husqvarna Automower (#151428) 2025-09-12 20:18:04 +00:00
Mark Adkins
ca79f4c963 Update SharkIQ authentication method (#151046) 2025-09-12 20:18:02 +00:00
Bob Igo
9a43f2776d Fix XMPP not working with non-TLS servers (#150957) 2025-09-12 20:18:01 +00:00
Franck Nijhof
0cda883b56 2025.9.1 (#151766) 2025-09-05 13:13:34 +02:00
Franck Nijhof
ae58e633f0 Bump version to 2025.9.1 2025-09-05 10:33:36 +00:00
jan iversen
06480bfd9d Fix enable/disable entity in modbus (#151626) 2025-09-05 10:33:04 +00:00
Artur Pragacz
625f586945 Fix recognition of entity names in default agent with interpunction (#151759) 2025-09-05 10:30:27 +00:00
Richard Kroegel
7dbeaa475d Bump bimmer_connected to 0.17.3 (#151756) 2025-09-05 10:30:24 +00:00
David Knowles
dff3d5f8af Bump pyschlage to 2025.9.0 (#151731) 2025-09-05 10:30:21 +00:00
Michael Hansen
89c335919a Handle match failures in intent HTTP API (#151726) 2025-09-05 10:30:16 +00:00
Daniel Hjelseth Høyer
2bb4573357 Update Mill library 0.13.1 (#151712) 2025-09-05 10:30:01 +00:00
Dan Raper
7037ce989c Bump ohmepy version to 1.5.2 (#151707) 2025-09-05 10:29:58 +00:00
Dan Raper
bfdd2053ba Require OhmeAdvancedSettingsCoordinator to run regardless of entities (#151701) 2025-09-05 10:29:55 +00:00
Bram Kragten
fcc3f92f8c Update frontend to 20250903.3 (#151694) 2025-09-05 10:29:51 +00:00
Marcel van der Veldt
8710267d53 Bump aiohue to 4.7.5 (#151684) 2025-09-05 10:29:48 +00:00
Imeon-Energy
85b6adcc9a Fix, entities stay unavailable after timeout error, Imeon inverter integration (#151671)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-05 10:29:46 +00:00
Felipe Santos
beec6e86e0 Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) 2025-09-05 10:29:45 +00:00
Pete Sage
3dacffaaf9 Fix Sonos Dialog Select type conversion (#151649) 2025-09-05 10:29:41 +00:00
Manu
d90f2a1de1 Correct capitalization of "FRITZ!Box" in FRITZ!Box Tools integration (#151637) 2025-09-05 10:29:39 +00:00
karwosts
b6c9217429 Add missing device trigger duration localizations (#151578)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-05 10:29:37 +00:00
Marcel van der Veldt
7fc8da6769 Add support for migrated Hue bridge (#151411)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-05 10:29:34 +00:00
961 changed files with 46322 additions and 10864 deletions

View File

@@ -58,6 +58,7 @@ base_platforms: &base_platforms
# Extra components that trigger the full suite
components: &components
- homeassistant/components/alexa/**
- homeassistant/components/analytics/**
- homeassistant/components/application_credentials/**
- homeassistant/components/assist_pipeline/**
- homeassistant/components/auth/**

View File

@@ -160,7 +160,7 @@ jobs:
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@2025.07.0
uses: home-assistant/wheels@2025.09.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -221,7 +221,7 @@ jobs:
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: home-assistant/wheels@2025.07.0
uses: home-assistant/wheels@2025.09.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2

View File

@@ -142,6 +142,7 @@ homeassistant.components.cloud.*
homeassistant.components.co2signal.*
homeassistant.components.comelit.*
homeassistant.components.command_line.*
homeassistant.components.compit.*
homeassistant.components.config.*
homeassistant.components.configurator.*
homeassistant.components.cookidoo.*
@@ -442,6 +443,7 @@ homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roborock.*
homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.route_b_smart_meter.*
homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.*
homeassistant.components.russound_rio.*

24
CODEOWNERS generated
View File

@@ -107,8 +107,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ambient_station/ @bachya
/tests/components/ambient_station/ @bachya
/homeassistant/components/amcrest/ @flacjacket
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
/tests/components/analytics/ @home-assistant/core @ludeeus
/homeassistant/components/analytics/ @home-assistant/core
/tests/components/analytics/ @home-assistant/core
/homeassistant/components/analytics_insights/ @joostlek
/tests/components/analytics_insights/ @joostlek
/homeassistant/components/android_ip_webcam/ @engrbm87
@@ -292,6 +292,8 @@ build.json @home-assistant/supervisor
/tests/components/command_line/ @gjohansson-ST
/homeassistant/components/compensation/ @Petro31
/tests/components/compensation/ @Petro31
/homeassistant/components/compit/ @Przemko92
/tests/components/compit/ @Przemko92
/homeassistant/components/config/ @home-assistant/core
/tests/components/config/ @home-assistant/core
/homeassistant/components/configurator/ @home-assistant/core
@@ -314,6 +316,8 @@ build.json @home-assistant/supervisor
/tests/components/crownstone/ @Crownstone @RicArch97
/homeassistant/components/cups/ @fabaff
/tests/components/cups/ @fabaff
/homeassistant/components/cync/ @Kinachi249
/tests/components/cync/ @Kinachi249
/homeassistant/components/daikin/ @fredrike
/tests/components/daikin/ @fredrike
/homeassistant/components/date/ @home-assistant/core
@@ -408,6 +412,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/egardia/ @jeroenterheerdt
/homeassistant/components/eheimdigital/ @autinerd
/tests/components/eheimdigital/ @autinerd
/homeassistant/components/ekeybionyx/ @richardpolzer
/tests/components/ekeybionyx/ @richardpolzer
/homeassistant/components/electrasmart/ @jafar-atili
/tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000
@@ -754,8 +760,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
@@ -770,6 +776,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/irm_kmi/ @jdejaegh
/tests/components/irm_kmi/ @jdejaegh
/homeassistant/components/iron_os/ @tr4nt0r
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco
@@ -968,8 +976,6 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco
/homeassistant/components/mobile_app/ @home-assistant/core
/tests/components/mobile_app/ @home-assistant/core
/homeassistant/components/modbus/ @janiversen
/tests/components/modbus/ @janiversen
/homeassistant/components/modem_callerid/ @tkdrob
/tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug
@@ -1328,6 +1334,8 @@ build.json @home-assistant/supervisor
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
/homeassistant/components/roon/ @pavoni
/tests/components/roon/ @pavoni
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
/tests/components/route_b_smart_meter/ @SeraphicRav
/homeassistant/components/rpi_power/ @shenxn @swetoast
/tests/components/rpi_power/ @shenxn @swetoast
/homeassistant/components/rss_feed_template/ @home-assistant/core
@@ -1727,8 +1735,8 @@ build.json @home-assistant/supervisor
/tests/components/volumio/ @OnFreund
/homeassistant/components/volvo/ @thomasddn
/tests/components/volvo/ @thomasddn
/homeassistant/components/volvooncall/ @molobrakos
/tests/components/volvooncall/ @molobrakos
/homeassistant/components/volvooncall/ @molobrakos @svrooij
/tests/components/volvooncall/ @molobrakos @svrooij
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.1
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
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -0,0 +1,5 @@
{
"domain": "eltako",
"name": "Eltako",
"iot_standards": ["matter"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "konnected",
"name": "Konnected",
"integrations": ["konnected", "konnected_esphome"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "level",
"name": "Level",
"iot_standards": ["matter"]
}

View File

@@ -8,6 +8,7 @@ import logging
from aioacaia.acaiascale import AcaiaScale
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
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
@@ -42,6 +43,7 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
scanner=async_get_scanner(hass),
)
@property

View File

@@ -26,5 +26,5 @@
"iot_class": "local_push",
"loggers": ["aioacaia"],
"quality_scale": "platinum",
"requirements": ["aioacaia==0.1.14"]
"requirements": ["aioacaia==0.1.17"]
}

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from asyncio import timeout
from collections.abc import Mapping
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
@@ -22,6 +23,8 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather."""
VERSION = 1
_latitude: float | None = None
_longitude: float | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -74,3 +77,46 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self._latitude = entry_data[CONF_LATITUDE]
self._longitude = entry_data[CONF_LONGITUDE]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
if user_input is not None:
websession = async_get_clientsession(self.hass)
try:
async with timeout(10):
accuweather = AccuWeather(
user_input[CONF_API_KEY],
websession,
latitude=self._latitude,
longitude=self._longitude,
)
await accuweather.async_get_location()
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except InvalidApiKeyError:
errors["base"] = "invalid_api_key"
except RequestsExceededError:
errors["base"] = "requests_exceeded"
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)

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

@@ -15,6 +15,7 @@ from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
@@ -30,7 +31,7 @@ from .const import (
UPDATE_INTERVAL_OBSERVATION,
)
EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError)
EXCEPTIONS = (ApiError, ClientConnectorError, RequestsExceededError)
_LOGGER = logging.getLogger(__name__)
@@ -52,6 +53,8 @@ class AccuWeatherObservationDataUpdateCoordinator(
):
"""Class to manage fetching AccuWeather data API."""
config_entry: AccuWeatherConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -87,6 +90,12 @@ class AccuWeatherObservationDataUpdateCoordinator(
translation_key="current_conditions_update_error",
translation_placeholders={"error": repr(error)},
) from error
except InvalidApiKeyError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
translation_placeholders={"entry": self.config_entry.title},
) from err
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
@@ -98,6 +107,8 @@ class AccuWeatherForecastDataUpdateCoordinator(
):
"""Base class for AccuWeather forecast."""
config_entry: AccuWeatherConfigEntry
def __init__(
self,
hass: HomeAssistant,
@@ -137,6 +148,12 @@ class AccuWeatherForecastDataUpdateCoordinator(
translation_key="forecast_update_error",
translation_placeholders={"error": repr(error)},
) from error
except InvalidApiKeyError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
translation_placeholders={"entry": self.config_entry.title},
) from err
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result

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

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.1"]
"requirements": ["accuweather==4.2.2"]
}

View File

@@ -7,6 +7,17 @@
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
},
"data_description": {
"api_key": "API key generated in the AccuWeather APIs portal."
}
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]"
}
}
},
@@ -19,7 +30,8 @@
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
@@ -239,6 +251,9 @@
}
},
"exceptions": {
"auth_error": {
"message": "Authentication failed for {entry}, please update your API key"
},
"current_conditions_update_error": {
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
},

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."""
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
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

@@ -4,10 +4,18 @@ from __future__ import annotations
from airos.airos8 import AirOS8
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [
@@ -21,13 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
# By default airOS 8 comes with self-signed SSL certificates,
# with no option in the web UI to change or upload a custom certificate.
session = async_get_clientsession(hass, verify_ssl=False)
session = async_get_clientsession(
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
)
airos_device = AirOS8(
host=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
)
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
@@ -40,6 +51,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
return True
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
return False
if entry.version == 1 and entry.minor_version == 1:
new_data = {**entry.data}
advanced_data = {
CONF_SSL: DEFAULT_SSL,
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
}
new_data[SECTION_ADVANCED_SETTINGS] = advanced_data
hass.config_entries.async_update_entry(
entry,
data=new_data,
minor_version=2,
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -15,10 +15,17 @@ from airos.exceptions import (
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.data_entry_flow import section
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOS8
_LOGGER = logging.getLogger(__name__)
@@ -28,6 +35,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME, default="ubnt"): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
}
),
{"collapsed": True},
),
}
)
@@ -36,6 +52,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ubiquiti airOS."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self,
@@ -46,13 +63,17 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
# By default airOS 8 comes with self-signed SSL certificates,
# with no option in the web UI to change or upload a custom certificate.
session = async_get_clientsession(self.hass, verify_ssl=False)
session = async_get_clientsession(
self.hass,
verify_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
)
airos_device = AirOS8(
host=user_input[CONF_HOST],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
session=session,
use_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_SSL],
)
try:
await airos_device.login()

View File

@@ -7,3 +7,8 @@ DOMAIN = "airos"
SCAN_INTERVAL = timedelta(minutes=1)
MANUFACTURER = "Ubiquiti"
DEFAULT_VERIFY_SSL = False
DEFAULT_SSL = True
SECTION_ADVANCED_SETTINGS = "advanced_settings"

View File

@@ -2,11 +2,11 @@
from __future__ import annotations
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, CONF_SSL
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSDataUpdateCoordinator
@@ -20,9 +20,14 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
super().__init__(coordinator)
airos_data = self.coordinator.data
url_schema = (
"https"
if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
else "http"
)
configuration_url: str | None = (
f"https://{coordinator.config_entry.data[CONF_HOST]}"
f"{url_schema}://{coordinator.config_entry.data[CONF_HOST]}"
)
self._attr_device_info = DeviceInfo(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.5.1"]
"requirements": ["airos==0.5.5"]
}

View File

@@ -12,6 +12,18 @@
"host": "IP address or hostname of the airOS device",
"username": "Administrator username for the airOS device, normally 'ubnt'",
"password": "Password configured through the UISP app or web interface"
},
"sections": {
"advanced_settings": {
"data": {
"ssl": "Use HTTPS",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"ssl": "Whether the connection should be encrypted (required for most devices)",
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
}
}
}
}
},

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

@@ -2,17 +2,14 @@
from airtouch4pyapi import AirTouch
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import AirtouchDataUpdateCoordinator
from .coordinator import AirTouch4ConfigEntry, AirtouchDataUpdateCoordinator
PLATFORMS = [Platform.CLIMATE]
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
"""Set up AirTouch4 from a config entry."""
@@ -22,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) ->
info = airtouch.GetAcs()
if not info:
raise ConfigEntryNotReady
coordinator = AirtouchDataUpdateCoordinator(hass, airtouch)
coordinator = AirtouchDataUpdateCoordinator(hass, entry, airtouch)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -2,26 +2,34 @@
import logging
from airtouch4pyapi import AirTouch
from airtouch4pyapi.airtouch import AirTouchStatus
from homeassistant.components.climate import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Airtouch data."""
def __init__(self, hass, airtouch):
def __init__(
self, hass: HomeAssistant, entry: AirTouch4ConfigEntry, airtouch: AirTouch
) -> None:
"""Initialize global Airtouch data updater."""
self.airtouch = airtouch
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)

View File

@@ -6,17 +6,19 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Final
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
from aioairzone.common import GrilleAngle, OperationMode, QAdapt, SleepTimeout
from aioairzone.const import (
API_COLD_ANGLE,
API_HEAT_ANGLE,
API_MODE,
API_Q_ADAPT,
API_SLEEP,
AZD_COLD_ANGLE,
AZD_HEAT_ANGLE,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_Q_ADAPT,
AZD_SLEEP,
AZD_ZONES,
)
@@ -65,6 +67,14 @@ SLEEP_DICT: Final[dict[str, int]] = {
"90m": SleepTimeout.SLEEP_90,
}
Q_ADAPT_DICT: Final[dict[str, int]] = {
"standard": QAdapt.STANDARD,
"power": QAdapt.POWER,
"silence": QAdapt.SILENCE,
"minimum": QAdapt.MINIMUM,
"maximum": QAdapt.MAXIMUM,
}
def main_zone_options(
zone_data: dict[str, Any],
@@ -83,6 +93,14 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
options_fn=main_zone_options,
translation_key="modes",
),
AirzoneSelectDescription(
api_param=API_Q_ADAPT,
entity_category=EntityCategory.CONFIG,
key=AZD_Q_ADAPT,
options=list(Q_ADAPT_DICT),
options_dict=Q_ADAPT_DICT,
translation_key="q_adapt",
),
)

View File

@@ -63,6 +63,16 @@
"stop": "Stop"
}
},
"q_adapt": {
"name": "Q-Adapt",
"state": {
"standard": "Standard",
"power": "Power",
"silence": "Silence",
"minimum": "Minimum",
"maximum": "Maximum"
}
},
"sleep_times": {
"name": "Sleep",
"state": {

View File

@@ -22,6 +22,17 @@ class OAuth2FlowHandler(
VERSION = CONFIG_FLOW_VERSION
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Check we have the cloud integration set up."""
if "cloud" not in self.hass.config.components:
return self.async_abort(
reason="cloud_not_enabled",
description_placeholders={"default_config": "default_config"},
)
return await super().async_step_user(user_input)
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:

View File

@@ -24,7 +24,8 @@
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account."
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account.",
"cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@@ -10,6 +10,7 @@ from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SENSOR_STATE_OFF
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
@@ -17,9 +18,12 @@ 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
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -31,6 +35,7 @@ class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
is_on_fn: Callable[[AmazonDevice, str], bool]
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: True
BINARY_SENSORS: Final = (
@@ -40,47 +45,52 @@ BINARY_SENSORS: Final = (
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda device, _: device.online,
),
AmazonBinarySensorEntityDescription(
key="detectionState",
device_class=BinarySensorDeviceClass.MOTION,
is_on_fn=lambda device, key: bool(
device.sensors[key].value != SENSOR_STATE_OFF
),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_available_fn=lambda device, key: (
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, _: device.bluetooth_state,
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="babyCryDetectionState",
translation_key="baby_cry_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="beepingApplianceDetectionState",
translation_key="beeping_appliance_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="coughDetectionState",
translation_key="cough_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="dogBarkDetectionState",
translation_key="dog_bark_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
),
AmazonBinarySensorEntityDescription(
key="humanPresenceDetectionState",
device_class=BinarySensorDeviceClass.MOTION,
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_on_fn=lambda device, key: False,
),
AmazonBinarySensorEntityDescription(
key="waterSoundsDetectionState",
translation_key="water_sounds_detection",
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
is_supported=lambda device, key: device.sensors.get(key) is not None,
is_on_fn=lambda device, key: False,
),
)
@@ -94,13 +104,46 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
entity_registry = er.async_get(hass)
# Replace unique id for "detectionState" binary sensor
await async_update_unique_id(
hass,
coordinator,
BINARY_SENSOR_DOMAIN,
"humanPresenceDetectionState",
"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:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in new_devices
if sensor_desc.is_supported(
coordinator.data[serial_num], sensor_desc.key
)
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
"""Binary sensor device."""
@@ -113,3 +156,13 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
return self.entity_description.is_on_fn(
self.device, self.entity_description.key
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
self.entity_description.is_available_fn(
self.device, self.entity_description.key
)
and super().available
)

View File

@@ -64,7 +64,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
data = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except (CannotAuthenticate, TypeError):
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
@@ -112,7 +112,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
)
except CannotConnect:
errors["base"] = "cannot_connect"
except (CannotAuthenticate, TypeError):
except CannotAuthenticate:
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"

View File

@@ -68,7 +68,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_key="cannot_retrieve_data_with_error",
translation_placeholders={"error": repr(err)},
) from err
except (CannotAuthenticate, TypeError) as err:
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",

View File

@@ -60,7 +60,5 @@ def build_device_data(device: AmazonDevice) -> dict[str, Any]:
"online": device.online,
"serial number": device.serial_number,
"software version": device.software_version,
"do not disturb": device.do_not_disturb,
"response style": device.response_style,
"bluetooth state": device.bluetooth_state,
"sensors": device.sensors,
}

View File

@@ -1,44 +1,4 @@
{
"entity": {
"binary_sensor": {
"bluetooth": {
"default": "mdi:bluetooth-off",
"state": {
"on": "mdi:bluetooth"
}
},
"baby_cry_detection": {
"default": "mdi:account-voice-off",
"state": {
"on": "mdi:account-voice"
}
},
"beeping_appliance_detection": {
"default": "mdi:bell-off",
"state": {
"on": "mdi:bell-ring"
}
},
"cough_detection": {
"default": "mdi:blur-off",
"state": {
"on": "mdi:blur"
}
},
"dog_bark_detection": {
"default": "mdi:dog-side-off",
"state": {
"on": "mdi:dog-side"
}
},
"water_sounds_detection": {
"default": "mdi:water-pump-off",
"state": {
"on": "mdi:water-pump"
}
}
}
},
"services": {
"send_sound": {
"service": "mdi:cast-audio"

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "silver",
"requirements": ["aioamazondevices==6.0.0"]
"quality_scale": "platinum",
"requirements": ["aioamazondevices==6.4.4"]
}

View File

@@ -57,13 +57,23 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in new_devices
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):

View File

@@ -53,7 +53,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done

View File

@@ -31,15 +31,20 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
"""Amazon Devices sensor entity description."""
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
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,
@@ -62,12 +67,22 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in coordinator.data
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in new_devices
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSensorEntity(AmazonEntity, SensorEntity):
@@ -89,3 +104,13 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.device.sensors[self.entity_description.key].value
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
self.entity_description.is_available_fn(
self.device, self.entity_description.key
)
and super().available
)

View File

@@ -58,26 +58,6 @@
}
},
"entity": {
"binary_sensor": {
"bluetooth": {
"name": "Bluetooth"
},
"baby_cry_detection": {
"name": "Baby crying"
},
"beeping_appliance_detection": {
"name": "Beeping appliance"
},
"cough_detection": {
"name": "Coughing"
},
"dog_bark_detection": {
"name": "Dog barking"
},
"water_sounds_detection": {
"name": "Water sounds"
}
},
"notify": {
"speak": {
"name": "Speak"

View File

@@ -8,13 +8,21 @@ from typing import TYPE_CHECKING, Any, Final
from aioamazondevices.api import AmazonDevice
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import alexa_api_call
from .utils import (
alexa_api_call,
async_remove_dnd_from_virtual_group,
async_update_unique_id,
)
PARALLEL_UPDATES = 1
@@ -24,16 +32,19 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
"""Alexa Devices switch entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
subkey: str
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
device.online
and (sensor := device.sensors.get(key)) is not None
and sensor.error is False
)
method: str
SWITCHES: Final = (
AmazonSwitchEntityDescription(
key="do_not_disturb",
subkey="AUDIO_PLAYER",
key="dnd",
translation_key="do_not_disturb",
is_on_fn=lambda _device: _device.do_not_disturb,
is_on_fn=lambda device: bool(device.sensors["dnd"].value),
method="set_do_not_disturb",
),
)
@@ -48,13 +59,31 @@ async def async_setup_entry(
coordinator = entry.runtime_data
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in coordinator.data
if switch_desc.subkey in coordinator.data[serial_num].capabilities
# Replace unique id for "DND" switch and remove from Speaker Group
await async_update_unique_id(
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:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in new_devices
if switch_desc.key in coordinator.data[serial_num].sensors
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
"""Switch device."""
@@ -84,3 +113,13 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
def is_on(self) -> bool:
"""Return True if switch is on."""
return self.entity_description.is_on_fn(self.device)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
self.entity_description.is_available_fn(
self.device, self.entity_description.key
)
and super().available
)

View File

@@ -4,11 +4,16 @@ 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
from .const import DOMAIN
from .const import _LOGGER, DOMAIN
from .coordinator import AmazonDevicesCoordinator
from .entity import AmazonEntity
@@ -38,3 +43,41 @@ def alexa_api_call[_T: AmazonEntity, **_P](
) from err
return cmd_wrapper
async def async_update_unique_id(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
domain: str,
old_key: str,
new_key: str,
) -> None:
"""Update unique id for entities created with old format."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
unique_id = f"{serial_num}-{old_key}"
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
_LOGGER.debug("Updating unique_id for %s", entity_id)
new_unique_id = unique_id.replace(old_key, new_key)
# 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

@@ -41,7 +41,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
call_ids = await async_extract_entity_ids(call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:

View File

@@ -12,10 +12,25 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .analytics import Analytics
from .analytics import (
Analytics,
AnalyticsInput,
AnalyticsModifications,
DeviceAnalyticsModifications,
EntityAnalyticsModifications,
async_devices_payload,
)
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
from .http import AnalyticsDevicesView
__all__ = [
"AnalyticsInput",
"AnalyticsModifications",
"DeviceAnalyticsModifications",
"EntityAnalyticsModifications",
"async_devices_payload",
]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)

View File

@@ -4,9 +4,10 @@ from __future__ import annotations
import asyncio
from asyncio import timeout
from dataclasses import asdict as dataclass_asdict, dataclass
from collections.abc import Awaitable, Callable, Iterable, Mapping
from dataclasses import asdict as dataclass_asdict, dataclass, field
from datetime import datetime
from typing import Any
from typing import Any, Protocol
import uuid
import aiohttp
@@ -35,11 +36,14 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.loader import (
Integration,
IntegrationNotFound,
async_get_integration,
async_get_integrations,
)
from homeassistant.setup import async_get_loaded_integrations
@@ -75,12 +79,115 @@ from .const import (
ATTR_USER_COUNT,
ATTR_UUID,
ATTR_VERSION,
DOMAIN,
LOGGER,
PREFERENCE_SCHEMA,
STORAGE_KEY,
STORAGE_VERSION,
)
DATA_ANALYTICS_MODIFIERS = "analytics_modifiers"
type AnalyticsModifier = Callable[
[HomeAssistant, AnalyticsInput], Awaitable[AnalyticsModifications]
]
@singleton(DATA_ANALYTICS_MODIFIERS)
def _async_get_modifiers(
hass: HomeAssistant,
) -> dict[str, AnalyticsModifier | None]:
"""Return the analytics modifiers."""
return {}
@dataclass
class AnalyticsInput:
"""Analytics input for a single integration.
This is sent to integrations that implement the platform.
"""
device_ids: Iterable[str] = field(default_factory=list)
entity_ids: Iterable[str] = field(default_factory=list)
@dataclass
class AnalyticsModifications:
"""Analytics config for a single integration.
This is used by integrations that implement the platform.
"""
remove: bool = False
devices: Mapping[str, DeviceAnalyticsModifications] | None = None
entities: Mapping[str, EntityAnalyticsModifications] | None = None
@dataclass
class DeviceAnalyticsModifications:
"""Analytics config for a single device.
This is used by integrations that implement the platform.
"""
remove: bool = False
@dataclass
class EntityAnalyticsModifications:
"""Analytics config for a single entity.
This is used by integrations that implement the platform.
"""
remove: bool = False
class AnalyticsPlatformProtocol(Protocol):
"""Define the format of analytics platforms."""
async def async_modify_analytics(
self,
hass: HomeAssistant,
analytics_input: AnalyticsInput,
) -> AnalyticsModifications:
"""Modify the analytics."""
async def _async_get_analytics_platform(
hass: HomeAssistant, domain: str
) -> AnalyticsPlatformProtocol | None:
"""Get analytics platform."""
try:
integration = await async_get_integration(hass, domain)
except IntegrationNotFound:
return None
try:
return await integration.async_get_platform(DOMAIN)
except ImportError:
return None
async def _async_get_modifier(
hass: HomeAssistant, domain: str
) -> AnalyticsModifier | None:
"""Get analytics modifier."""
modifiers = _async_get_modifiers(hass)
modifier = modifiers.get(domain, UNDEFINED)
if modifier is not UNDEFINED:
return modifier
platform = await _async_get_analytics_platform(hass, domain)
if platform is None:
modifiers[domain] = None
return None
modifier = getattr(platform, "async_modify_analytics", None)
modifiers[domain] = modifier
return modifier
def gen_uuid() -> str:
"""Generate a new UUID."""
@@ -393,17 +500,22 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
return domains
async def async_devices_payload(hass: HomeAssistant) -> dict:
DEFAULT_ANALYTICS_CONFIG = AnalyticsModifications()
DEFAULT_DEVICE_ANALYTICS_CONFIG = DeviceAnalyticsModifications()
DEFAULT_ENTITY_ANALYTICS_CONFIG = EntityAnalyticsModifications()
async def async_devices_payload(hass: HomeAssistant) -> dict: # noqa: C901
"""Return detailed information about entities and devices."""
integrations_info: dict[str, dict[str, Any]] = {}
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
# We need to refer to other devices, for example in `via_device` field.
# We don't however send the original device ids outside of Home Assistant,
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
device_id_mapping: dict[str, tuple[str, int]] = {}
integration_inputs: dict[str, tuple[list[str], list[str]]] = {}
integration_configs: dict[str, AnalyticsModifications] = {}
removed_devices: set[str] = set()
# Get device list
for device_entry in dev_reg.devices.values():
if not device_entry.primary_config_entry:
continue
@@ -415,28 +527,119 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
if config_entry is None:
continue
if device_entry.entry_type is dr.DeviceEntryType.SERVICE:
removed_devices.add(device_entry.id)
continue
integration_domain = config_entry.domain
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
integration_input[0].append(device_entry.id)
# Get entity list
for entity_entry in ent_reg.entities.values():
integration_domain = entity_entry.platform
integration_input = integration_inputs.setdefault(integration_domain, ([], []))
integration_input[1].append(entity_entry.entity_id)
integrations = {
domain: integration
for domain, integration in (
await async_get_integrations(hass, integration_inputs.keys())
).items()
if isinstance(integration, Integration)
}
# Filter out custom integrations and integrations that are not device or hub type
integration_inputs = {
domain: integration_info
for domain, integration_info in integration_inputs.items()
if (integration := integrations.get(domain)) is not None
and integration.is_built_in
and integration.manifest.get("integration_type") in ("device", "hub")
}
# Call integrations that implement the analytics platform
for integration_domain, integration_input in integration_inputs.items():
if (
modifier := await _async_get_modifier(hass, integration_domain)
) is not None:
try:
integration_config = await modifier(
hass, AnalyticsInput(*integration_input)
)
except Exception as err: # noqa: BLE001
LOGGER.exception(
"Calling async_modify_analytics for integration '%s' failed: %s",
integration_domain,
err,
)
integration_configs[integration_domain] = AnalyticsModifications(
remove=True
)
continue
if not isinstance(integration_config, AnalyticsModifications):
LOGGER.error( # type: ignore[unreachable]
"Calling async_modify_analytics for integration '%s' did not return an AnalyticsConfig",
integration_domain,
)
integration_configs[integration_domain] = AnalyticsModifications(
remove=True
)
continue
integration_configs[integration_domain] = integration_config
integrations_info: dict[str, dict[str, Any]] = {}
# We need to refer to other devices, for example in `via_device` field.
# We don't however send the original device ids outside of Home Assistant,
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
device_id_mapping: dict[str, tuple[str, int]] = {}
# Fill out information about devices
for integration_domain, integration_input in integration_inputs.items():
integration_config = integration_configs.get(
integration_domain, DEFAULT_ANALYTICS_CONFIG
)
if integration_config.remove:
continue
integration_info = integrations_info.setdefault(
integration_domain, {"devices": [], "entities": []}
)
devices_info = integration_info["devices"]
device_id_mapping[device_entry.id] = (integration_domain, len(devices_info))
for device_id in integration_input[0]:
device_config = DEFAULT_DEVICE_ANALYTICS_CONFIG
if integration_config.devices is not None:
device_config = integration_config.devices.get(device_id, device_config)
devices_info.append(
{
"entities": [],
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
}
)
if device_config.remove:
removed_devices.add(device_id)
continue
device_entry = dev_reg.devices[device_id]
device_id_mapping[device_id] = (integration_domain, len(devices_info))
devices_info.append(
{
"entities": [],
"entry_type": device_entry.entry_type,
"has_configuration_url": device_entry.configuration_url is not None,
"hw_version": device_entry.hw_version,
"manufacturer": device_entry.manufacturer,
"model": device_entry.model,
"model_id": device_entry.model_id,
"sw_version": device_entry.sw_version,
"via_device": device_entry.via_device_id,
}
)
# Fill out via_device with new device ids
for integration_info in integrations_info.values():
@@ -445,10 +648,15 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
continue
device_info["via_device"] = device_id_mapping.get(device_info["via_device"])
ent_reg = er.async_get(hass)
# Fill out information about entities
for integration_domain, integration_input in integration_inputs.items():
integration_config = integration_configs.get(
integration_domain, DEFAULT_ANALYTICS_CONFIG
)
if integration_config.remove:
continue
for entity_entry in ent_reg.entities.values():
integration_domain = entity_entry.platform
integration_info = integrations_info.setdefault(
integration_domain, {"devices": [], "entities": []}
)
@@ -456,53 +664,53 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
devices_info = integration_info["devices"]
entities_info = integration_info["entities"]
entity_state = hass.states.get(entity_entry.entity_id)
entity_info = {
# LIMITATION: `assumed_state` can be overridden by users;
# we should replace it with the original value in the future.
# It is also not present, if entity is not in the state machine,
# which can happen for disabled entities.
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None,
"capabilities": entity_entry.capabilities,
"domain": entity_entry.domain,
"entity_category": entity_entry.entity_category,
"has_entity_name": entity_entry.has_entity_name,
"original_device_class": entity_entry.original_device_class,
# LIMITATION: `unit_of_measurement` can be overridden by users;
# we should replace it with the original value in the future.
"unit_of_measurement": entity_entry.unit_of_measurement,
}
if (
((device_id := entity_entry.device_id) is not None)
and ((new_device_id := device_id_mapping.get(device_id)) is not None)
and (new_device_id[0] == integration_domain)
):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
else:
entities_info.append(entity_info)
integrations = {
domain: integration
for domain, integration in (
await async_get_integrations(hass, integrations_info.keys())
).items()
if isinstance(integration, Integration)
}
for domain, integration_info in integrations_info.items():
if integration := integrations.get(domain):
integration_info["is_custom_integration"] = not integration.is_built_in
# Include version for custom integrations
if not integration.is_built_in and integration.version:
integration_info["custom_integration_version"] = str(
integration.version
for entity_id in integration_input[1]:
entity_config = DEFAULT_ENTITY_ANALYTICS_CONFIG
if integration_config.entities is not None:
entity_config = integration_config.entities.get(
entity_id, entity_config
)
if entity_config.remove:
continue
entity_entry = ent_reg.entities[entity_id]
entity_state = hass.states.get(entity_id)
entity_info = {
# LIMITATION: `assumed_state` can be overridden by users;
# we should replace it with the original value in the future.
# It is also not present, if entity is not in the state machine,
# which can happen for disabled entities.
"assumed_state": (
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None
),
"domain": entity_entry.domain,
"entity_category": entity_entry.entity_category,
"has_entity_name": entity_entry.has_entity_name,
"original_device_class": entity_entry.original_device_class,
# LIMITATION: `unit_of_measurement` can be overridden by users;
# we should replace it with the original value in the future.
"unit_of_measurement": entity_entry.unit_of_measurement,
}
if (device_id_ := entity_entry.device_id) is not None:
if device_id_ in removed_devices:
# The device was removed, so we remove the entity too
continue
if (
new_device_id := device_id_mapping.get(device_id_)
) is not None and (new_device_id[0] == integration_domain):
device_info = devices_info[new_device_id[1]]
device_info["entities"].append(entity_info)
continue
entities_info.append(entity_info)
return {
"version": "home-assistant:1",
"home_assistant": HA_VERSION,

View File

@@ -2,7 +2,7 @@
"domain": "analytics",
"name": "Analytics",
"after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core", "@ludeeus"],
"codeowners": ["@home-assistant/core"],
"dependencies": ["api", "websocket_api", "http"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
"integration_type": "system",

View File

@@ -1308,7 +1308,9 @@ class PipelineRun:
# instead of a full response.
all_targets_in_satellite_area = (
self._get_all_targets_in_satellite_area(
conversation_result.response, self._device_id
conversation_result.response,
self._satellite_id,
self._device_id,
)
)
@@ -1337,39 +1339,62 @@ class PipelineRun:
return (speech, all_targets_in_satellite_area)
def _get_all_targets_in_satellite_area(
self, intent_response: intent.IntentResponse, device_id: str | None
self,
intent_response: intent.IntentResponse,
satellite_id: str | None,
device_id: str | None,
) -> bool:
"""Return true if all targeted entities were in the same area as the device."""
if (
(intent_response.response_type != intent.IntentResponseType.ACTION_DONE)
or (not intent_response.matched_states)
or (not device_id)
):
return False
device_registry = dr.async_get(self.hass)
if (not (device := device_registry.async_get(device_id))) or (
not device.area_id
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
or not intent_response.matched_states
):
return False
entity_registry = er.async_get(self.hass)
for state in intent_response.matched_states:
entity = entity_registry.async_get(state.entity_id)
if not entity:
device_registry = dr.async_get(self.hass)
area_id: str | None = None
if (
satellite_id is not None
and (target_entity_entry := entity_registry.async_get(satellite_id))
is not None
):
area_id = target_entity_entry.area_id
device_id = target_entity_entry.device_id
if area_id is None:
if device_id is None:
return False
if (entity_area_id := entity.area_id) is None:
if (entity.device_id is None) or (
(entity_device := device_registry.async_get(entity.device_id))
is None
):
device_entry = device_registry.async_get(device_id)
if device_entry is None:
return False
area_id = device_entry.area_id
if area_id is None:
return False
for state in intent_response.matched_states:
target_entity_entry = entity_registry.async_get(state.entity_id)
if target_entity_entry is None:
return False
target_area_id = target_entity_entry.area_id
if target_area_id is None:
if target_entity_entry.device_id is None:
return False
entity_area_id = entity_device.area_id
target_device_entry = device_registry.async_get(
target_entity_entry.device_id
)
if target_device_entry is None:
return False
if entity_area_id != device.area_id:
target_area_id = target_device_entry.area_id
if target_area_id != area_id:
return False
return True

View File

@@ -109,7 +109,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
)
state = await self.async_get_last_state()
if state is not None and state.state in self.options:
if (state is not None) and (state.state in self.options):
self._attr_current_option = state.state
if self.registry_entry and (device_id := self.registry_entry.device_id):
@@ -119,7 +119,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
def cleanup() -> None:
"""Clean up registered device."""
pipeline_data.pipeline_devices.pop(device_id)
pipeline_data.pipeline_devices.pop(device_id, None)
self.async_on_remove(cleanup)

View File

@@ -7,13 +7,13 @@ from collections import namedtuple
from collections.abc import Awaitable, Callable, Coroutine
import functools
import logging
from typing import Any, cast
from typing import Any
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.client import AsusClient, 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
@@ -219,7 +219,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 +235,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."""
@@ -437,6 +436,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

@@ -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)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
)
except ValueError as err:
raise ConfigEntryNotReady("OAuth implementation not available") from err
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:

View File

@@ -2,13 +2,12 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
import logging
from typing import Any
from aiohttp import ClientResponseError
from yalexs.activity import ActivityType, ActivityTypes
from yalexs.lock import Lock, LockStatus
from yalexs.activity import ActivityType
from yalexs.lock import Lock, LockOperation, LockStatus
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
@@ -50,30 +49,25 @@ class AugustLock(AugustEntity, RestoreEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
if self._data.push_updates_connected:
await self._data.async_lock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_lock)
await self._perform_lock_operation(LockOperation.LOCK)
async def async_open(self, **kwargs: Any) -> None:
"""Open/unlatch the device."""
if self._data.push_updates_connected:
await self._data.async_unlatch_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlatch)
await self._perform_lock_operation(LockOperation.OPEN)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
if self._data.push_updates_connected:
await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlock)
await self._perform_lock_operation(LockOperation.UNLOCK)
async def _call_lock_operation(
self, lock_operation: Callable[[str], Coroutine[Any, Any, list[ActivityTypes]]]
) -> None:
async def _perform_lock_operation(self, operation: LockOperation) -> None:
"""Perform a lock operation."""
try:
activities = await lock_operation(self._device_id)
activities = await self._data.async_operate_lock(
self._device_id,
operation,
self._data.push_updates_connected,
self._hyper_bridge,
)
except ClientResponseError as err:
if err.status == LOCK_JAMMED_ERR:
self._detail.lock_status = LockStatus.JAMMED

View File

@@ -272,6 +272,13 @@ async def async_setup_entry(
observations: list[ConfigType] = [
dict(subentry.data) for subentry in config_entry.subentries.values()
]
for observation in observations:
if observation[CONF_PLATFORM] == CONF_TEMPLATE:
observation[CONF_VALUE_TEMPLATE] = Template(
observation[CONF_VALUE_TEMPLATE], hass
)
prior: float = config[CONF_PRIOR]
probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD]
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)

View File

@@ -13,20 +13,30 @@ from bluecurrent_api.exceptions import (
RequestLimitReached,
WebsocketError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
BCU_APP,
CHARGEPOINT_SETTINGS,
CHARGEPOINT_STATUS,
CHARGING_CARD_ID,
DOMAIN,
EVSE_ID,
LOGGER,
PLUG_AND_CHARGE,
SERVICE_START_CHARGE_SESSION,
VALUE,
)
@@ -34,6 +44,7 @@ type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
CHARGE_POINTS = "CHARGE_POINTS"
CHARGE_CARDS = "CHARGE_CARDS"
DATA = "data"
DELAY = 5
@@ -41,6 +52,16 @@ GRID = "GRID"
OBJECT = "object"
VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)
async def async_setup_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
@@ -67,6 +88,66 @@ async def async_setup_entry(
return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1]
for identifier in device.identifiers
if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
) -> bool:
@@ -87,6 +168,7 @@ class Connector:
self.client = client
self.charge_points: dict[str, dict] = {}
self.grid: dict[str, Any] = {}
self.charge_cards: dict[str, dict[str, Any]] = {}
async def on_data(self, message: dict) -> None:
"""Handle received data."""

View File

@@ -8,6 +8,12 @@ LOGGER = logging.getLogger(__package__)
EVSE_ID = "evse_id"
MODEL_TYPE = "model_type"
CARD = "card"
UID = "uid"
BCU_APP = "BCU-APP"
WITHOUT_CHARGING_CARD = "without_charging_card"
CHARGING_CARD_ID = "charging_card_id"
SERVICE_START_CHARGE_SESSION = "start_charge_session"
PLUG_AND_CHARGE = "plug_and_charge"
VALUE = "value"
PERMISSION = "permission"

View File

@@ -42,5 +42,10 @@
"default": "mdi:lock"
}
}
},
"services": {
"start_charge_session": {
"service": "mdi:play"
}
}
}

View File

@@ -0,0 +1,12 @@
start_charge_session:
fields:
device_id:
selector:
device:
integration: blue_current
required: true
charging_card_id:
selector:
text:
required: false

View File

@@ -22,6 +22,16 @@
"wrong_account": "Wrong account: Please authenticate with the API token for {email}."
}
},
"options": {
"step": {
"init": {
"data": {
"card": "Card"
},
"description": "Select the default charging card you want to use"
}
}
},
"entity": {
"sensor": {
"activity": {
@@ -136,5 +146,39 @@
"name": "Block charge point"
}
}
},
"selector": {
"select_charging_card": {
"options": {
"without_charging_card": "Without charging card"
}
}
},
"services": {
"start_charge_session": {
"name": "Start charge session",
"description": "Starts a new charge session on a specified charge point.",
"fields": {
"charging_card_id": {
"name": "Charging card ID",
"description": "Optional charging card ID that will be used to start a charge session. When not provided, no charging card will be used."
},
"device_id": {
"name": "Device ID",
"description": "The ID of the Blue Current charge point."
}
}
}
},
"exceptions": {
"invalid_device_id": {
"message": "Invalid device ID given."
},
"config_entry_not_loaded": {
"message": "Config entry not loaded."
},
"no_config_entry": {
"message": "Device has not a valid blue_current config entry."
}
}
}

View File

@@ -10,6 +10,7 @@ from asyncio import Future
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
from habluetooth import (
BaseHaScanner,
BluetoothScannerDevice,
@@ -38,13 +39,16 @@ def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:
@hass_callback
def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper:
"""Return a HaBleakScannerWrapper.
def async_get_scanner(hass: HomeAssistant) -> BleakScanner:
"""Return a HaBleakScannerWrapper cast to BleakScanner.
This is a wrapper around our BleakScanner singleton that allows
multiple integrations to share the same BleakScanner.
The wrapper is cast to BleakScanner for type compatibility with
libraries expecting a BleakScanner instance.
"""
return HaBleakScannerWrapper()
return cast(BleakScanner, HaBleakScannerWrapper())
@hass_callback

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

@@ -205,6 +205,7 @@ class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]
async def _async_update_data(self) -> dict[str, BringActivityData]:
"""Fetch activity data from bring."""
self.lists = self.coordinator.lists
list_dict: dict[str, BringActivityData] = {}
for lst in self.lists:

View File

@@ -43,7 +43,7 @@ async def async_setup_entry(
)
lists_added |= new_lists
coordinator.activity.async_add_listener(add_entities)
coordinator.data.async_add_listener(add_entities)
add_entities()
@@ -67,7 +67,8 @@ class BringEventEntity(BringBaseEntity, EventEntity):
def _async_handle_event(self) -> None:
"""Handle the activity event."""
bring_list = self.coordinator.data[self._list_uuid]
if (bring_list := self.coordinator.data.get(self._list_uuid)) is None:
return
last_event_triggered = self.state
if bring_list.activity.timeline and (
last_event_triggered is None

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

@@ -25,7 +25,11 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
return await cloud.payments.subscription_info()
except PaymentsApiError as exception:
_LOGGER.error("Failed to fetch subscription information - %s", exception)
except TimeoutError:
_LOGGER.error(
"A timeout of %s was reached while trying to fetch subscription information",
REQUEST_TIMEOUT,
)
return None

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

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

@@ -29,10 +29,23 @@ async def async_setup_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
async_add_entities(
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data["alarm_zones"].values()
)
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
)
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
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 (
@@ -25,23 +26,22 @@ from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = 111111
DEFAULT_PIN = "111111"
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.positive_int,
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.positive_int})
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.positive_int,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
}
)
@@ -51,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(
@@ -101,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"
@@ -142,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"
@@ -185,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"
@@ -206,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

@@ -2,7 +2,7 @@
from abc import abstractmethod
from datetime import timedelta
from typing import TypeVar
from typing import Any, TypeVar
from aiocomelit.api import (
AlarmDataObject,
@@ -13,7 +13,16 @@ from aiocomelit.api import (
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.const import (
BRIDGE,
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
VEDO,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession
@@ -111,6 +120,32 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
async def _async_update_system_data(self) -> T:
"""Class method for updating data."""
async def _async_remove_stale_devices(
self,
previous_list: dict[int, Any],
current_list: dict[int, Any],
dev_type: str,
) -> None:
"""Remove stale devices."""
device_registry = dr.async_get(self.hass)
for i in previous_list:
if i not in current_list:
_LOGGER.debug(
"Detected change in %s devices: index %s removed",
dev_type,
i,
)
identifier = f"{self.config_entry.entry_id}-{dev_type}-{i}"
device = device_registry.async_get_device(
identifiers={(DOMAIN, identifier)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
class ComelitSerialBridge(
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
@@ -126,7 +161,7 @@ class ComelitSerialBridge(
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
@@ -137,7 +172,15 @@ class ComelitSerialBridge(
self,
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
"""Specific method for updating data."""
return await self.api.get_all_devices()
data = await self.api.get_all_devices()
if self.data:
for dev_type in (CLIMATE, COVER, LIGHT, IRRIGATION, OTHER, SCENARIO):
await self._async_remove_stale_devices(
self.data[dev_type], data[dev_type], dev_type
)
return data
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
@@ -152,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
entry: ComelitConfigEntry,
host: str,
port: int,
pin: int,
pin: str,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
@@ -163,4 +206,14 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
self,
) -> AlarmDataObject:
"""Specific method for updating data."""
return await self.api.get_all_areas_and_zones()
data = await self.api.get_all_areas_and_zones()
if self.data:
for obj_type in ("alarm_areas", "alarm_zones"):
await self._async_remove_stale_devices(
self.data[obj_type],
data[obj_type],
"area" if obj_type == "alarm_areas" else "zone",
)
return data

View File

@@ -7,7 +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 (
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -29,10 +36,21 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
async_add_entities(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
)
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(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@@ -51,7 +69,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."""
@@ -87,7 +104,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()
@@ -113,5 +129,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 == STATE_CLOSED:
self._last_action = STATE_COVER.index(STATE_CLOSING)
if state.state == STATE_OPEN:
self._last_action = STATE_COVER.index(STATE_OPENING)
self._attr_is_closed = state.state == STATE_CLOSED

View File

@@ -27,10 +27,21 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
)
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(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "silver",
"requirements": ["aiocomelit==0.12.3"]
"quality_scale": "platinum",
"requirements": ["aiocomelit==1.1.2"]
}

View File

@@ -57,9 +57,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: todo
comment: missing implementation
dynamic-devices: done
entity-category:
status: exempt
comment: no config or diagnostic entities
@@ -72,9 +70,7 @@ rules:
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: missing implementation
stale-devices: done
# Platinum
async-dependency: done

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Final, cast
from aiocomelit import ComelitSerialBridgeObject, ComelitVedoZoneObject
from aiocomelit.api import ComelitSerialBridgeObject, ComelitVedoZoneObject
from aiocomelit.const import BRIDGE, OTHER, AlarmZoneState
from homeassistant.components.sensor import (
@@ -65,15 +65,24 @@ async def async_setup_bridge_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
entities: list[ComelitBridgeSensorEntity] = []
for device in coordinator.data[OTHER].values():
entities.extend(
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
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(
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 sensor_desc in SENSOR_BRIDGE_TYPES
)
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
async def async_setup_vedo_entry(
@@ -85,15 +94,24 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
entities: list[ComelitVedoSensorEntity] = []
for device in coordinator.data["alarm_zones"].values():
entities.extend(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
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(
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
)
for sensor_desc in SENSOR_VEDO_TYPES
)
async_add_entities(entities)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
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%]"
}
},

View File

@@ -39,6 +39,25 @@ async def async_setup_entry(
)
async_add_entities(entities)
known_devices: dict[str, set[int]] = {
dev_type: set() for dev_type in (IRRIGATION, OTHER)
}
def _check_device() -> None:
for dev_type in (IRRIGATION, OTHER):
current_devices = set(coordinator.data[dev_type])
new_devices = current_devices - known_devices[dev_type]
if new_devices:
known_devices[dev_type].update(new_devices)
async_add_entities(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
"""Switch device."""

View File

@@ -0,0 +1,45 @@
"""The Compit integration."""
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PLATFORMS = [
Platform.CLIMATE,
]
async def async_setup_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Set up Compit from a config entry."""
session = async_get_clientsession(hass)
connector = CompitApiConnector(session)
try:
connected = await connector.init(
entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], hass.config.language
)
except CannotConnect as e:
raise ConfigEntryNotReady(f"Error while connecting to Compit: {e}") from e
except InvalidAuth as e:
raise ConfigEntryAuthFailed(
f"Invalid credentials for {entry.data[CONF_EMAIL]}"
) from e
if not connected:
raise ConfigEntryAuthFailed("Authentication API error")
coordinator = CompitDataUpdateCoordinator(hass, entry, connector)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Unload an entry for the Compit integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,265 @@
"""Module contains the CompitClimate class for controlling climate entities."""
import logging
from typing import Any
from compit_inext_api import Param, Parameter
from compit_inext_api.consts import (
CompitFanMode,
CompitHVACMode,
CompitParameter,
CompitPresetMode,
)
from propcache.api import cached_property
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
PRESET_AWAY,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
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, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
_LOGGER: logging.Logger = logging.getLogger(__name__)
# Device class for climate devices in Compit system
CLIMATE_DEVICE_CLASS = 10
PARALLEL_UPDATES = 0
COMPIT_MODE_MAP = {
CompitHVACMode.COOL: HVACMode.COOL,
CompitHVACMode.HEAT: HVACMode.HEAT,
CompitHVACMode.OFF: HVACMode.OFF,
}
COMPIT_FANSPEED_MAP = {
CompitFanMode.OFF: FAN_OFF,
CompitFanMode.AUTO: FAN_AUTO,
CompitFanMode.LOW: FAN_LOW,
CompitFanMode.MEDIUM: FAN_MEDIUM,
CompitFanMode.HIGH: FAN_HIGH,
CompitFanMode.HOLIDAY: FAN_AUTO,
}
COMPIT_PRESET_MAP = {
CompitPresetMode.AUTO: PRESET_HOME,
CompitPresetMode.HOLIDAY: PRESET_ECO,
CompitPresetMode.MANUAL: PRESET_NONE,
CompitPresetMode.AWAY: PRESET_AWAY,
}
HVAC_MODE_TO_COMPIT_MODE = {v: k for k, v in COMPIT_MODE_MAP.items()}
FAN_MODE_TO_COMPIT_FAN_MODE = {v: k for k, v in COMPIT_FANSPEED_MAP.items()}
PRESET_MODE_TO_COMPIT_PRESET_MODE = {v: k for k, v in COMPIT_PRESET_MAP.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the CompitClimate platform from a config entry."""
coordinator = entry.runtime_data
climate_entities = []
for device_id in coordinator.connector.all_devices:
device = coordinator.connector.all_devices[device_id]
if device.definition.device_class == CLIMATE_DEVICE_CLASS:
climate_entities.append(
CompitClimate(
coordinator,
device_id,
{
parameter.parameter_code: parameter
for parameter in device.definition.parameters
},
device.definition.name,
)
)
async_add_devices(climate_entities)
class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntity):
"""Representation of a Compit climate device."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [*COMPIT_MODE_MAP.values()]
_attr_name = None
_attr_has_entity_name = True
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
)
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
parameters: dict[str, Parameter],
device_name: str,
) -> None:
"""Initialize the climate device."""
super().__init__(coordinator)
self._attr_unique_id = f"{device_name}_{device_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=device_name,
manufacturer=MANUFACTURER_NAME,
model=device_name,
)
self.parameters = parameters
self.device_id = device_id
self.available_presets: Parameter | None = self.parameters.get(
CompitParameter.PRESET_MODE.value
)
self.available_fan_modes: Parameter | None = self.parameters.get(
CompitParameter.FAN_MODE.value
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.device_id in self.coordinator.connector.all_devices
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
value = self.get_parameter_value(CompitParameter.CURRENT_TEMPERATURE)
if value is None:
return None
return float(value.value)
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
value = self.get_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE)
if value is None:
return None
return float(value.value)
@cached_property
def preset_modes(self) -> list[str] | None:
"""Return the available preset modes."""
if self.available_presets is None or self.available_presets.details is None:
return []
preset_modes = []
for item in self.available_presets.details:
if item is not None:
ha_preset = COMPIT_PRESET_MAP.get(CompitPresetMode(item.state))
if ha_preset and ha_preset not in preset_modes:
preset_modes.append(ha_preset)
return preset_modes
@cached_property
def fan_modes(self) -> list[str] | None:
"""Return the available fan modes."""
if self.available_fan_modes is None or self.available_fan_modes.details is None:
return []
fan_modes = []
for item in self.available_fan_modes.details:
if item is not None:
ha_fan_mode = COMPIT_FANSPEED_MAP.get(CompitFanMode(item.state))
if ha_fan_mode and ha_fan_mode not in fan_modes:
fan_modes.append(ha_fan_mode)
return fan_modes
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
preset_mode = self.get_parameter_value(CompitParameter.PRESET_MODE)
if preset_mode:
compit_preset_mode = CompitPresetMode(preset_mode.value)
return COMPIT_PRESET_MAP.get(compit_preset_mode)
return None
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self.get_parameter_value(CompitParameter.FAN_MODE)
if fan_mode:
compit_fan_mode = CompitFanMode(fan_mode.value)
return COMPIT_FANSPEED_MAP.get(compit_fan_mode)
return None
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
hvac_mode = self.get_parameter_value(CompitParameter.HVAC_MODE)
if hvac_mode:
compit_hvac_mode = CompitHVACMode(hvac_mode.value)
return COMPIT_MODE_MAP.get(compit_hvac_mode)
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None:
raise ServiceValidationError("Temperature argument missing")
await self.set_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE, temp)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target HVAC mode."""
if not (mode := HVAC_MODE_TO_COMPIT_MODE.get(hvac_mode)):
raise ServiceValidationError(f"Invalid hvac mode {hvac_mode}")
await self.set_parameter_value(CompitParameter.HVAC_MODE, mode.value)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
compit_preset = PRESET_MODE_TO_COMPIT_PRESET_MODE.get(preset_mode)
if compit_preset is None:
raise ServiceValidationError(f"Invalid preset mode: {preset_mode}")
await self.set_parameter_value(CompitParameter.PRESET_MODE, compit_preset.value)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
compit_fan_mode = FAN_MODE_TO_COMPIT_FAN_MODE.get(fan_mode)
if compit_fan_mode is None:
raise ServiceValidationError(f"Invalid fan mode: {fan_mode}")
await self.set_parameter_value(CompitParameter.FAN_MODE, compit_fan_mode.value)
async def set_parameter_value(self, parameter: CompitParameter, value: int) -> None:
"""Call the API to set a parameter to a new value."""
await self.coordinator.connector.set_device_parameter(
self.device_id, parameter, value
)
self.async_write_ha_state()
def get_parameter_value(self, parameter: CompitParameter) -> Param | None:
"""Get the parameter value from the device state."""
return self.coordinator.connector.get_device_parameter(
self.device_id, parameter
)

View File

@@ -0,0 +1,110 @@
"""Config flow for Compit integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class CompitConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Compit."""
VERSION = 1
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_create_clientsession(self.hass)
api = CompitApiConnector(session)
success = False
try:
success = await api.init(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
self.hass.config.language,
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if not success:
# Api returned unexpected result but no exception
_LOGGER.error("Compit api returned unexpected result")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL])
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle re-auth."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
reauth_entry_data = reauth_entry.data
if user_input:
# Reuse async_step_user with combined credentials
return await self.async_step_user(
{
CONF_EMAIL: reauth_entry_data[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={CONF_EMAIL: reauth_entry_data[CONF_EMAIL]},
errors=errors,
)

View File

@@ -0,0 +1,4 @@
"""Constants for the Compit integration."""
DOMAIN = "compit"
MANUFACTURER_NAME = "Compit"

View File

@@ -0,0 +1,43 @@
"""Define an object to manage fetching Compit data."""
from datetime import timedelta
import logging
from compit_inext_api import CompitApiConnector, DeviceInstance
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER: logging.Logger = logging.getLogger(__name__)
type CompitConfigEntry = ConfigEntry[CompitDataUpdateCoordinator]
class CompitDataUpdateCoordinator(DataUpdateCoordinator[dict[int, DeviceInstance]]):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
connector: CompitApiConnector,
) -> None:
"""Initialize."""
self.connector = connector
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
)
async def _async_update_data(self) -> dict[int, DeviceInstance]:
"""Update data via library."""
await self.connector.update_state(device_id=None) # Update all devices
return self.connector.all_devices

View File

@@ -0,0 +1,12 @@
{
"domain": "compit",
"name": "Compit",
"codeowners": ["@Przemko92"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/compit",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.3.1"]
}

View File

@@ -0,0 +1,86 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules:
status: exempt
comment: |
This integration does not use any common modules.
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration is a cloud service and does not support discovery.
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: todo
icon-translations:
status: exempt
comment: |
There is no need for icon translations.
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: done

View File

@@ -0,0 +1,35 @@
{
"config": {
"step": {
"user": {
"description": "Please enter your https://inext.compit.pl/ credentials.",
"title": "Connect to Compit iNext",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "The email address of your inext.compit.pl account",
"password": "The password of your inext.compit.pl account"
}
},
"reauth_confirm": {
"description": "Please update your password for {email}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::compit::config::step::user::data_description::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}

View File

@@ -8,7 +8,13 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.core import Context, HomeAssistant, async_get_hass, callback
from homeassistant.core import (
CALLBACK_TYPE,
Context,
HomeAssistant,
async_get_hass,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent, singleton
@@ -30,6 +36,7 @@ _LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from .default_agent import DefaultAgent
from .trigger import TriggerDetails
@singleton.singleton("conversation_agent")
@@ -140,6 +147,7 @@ class AgentManager:
self.hass = hass
self._agents: dict[str, AbstractConversationAgent] = {}
self.default_agent: DefaultAgent | None = None
self.triggers_details: list[TriggerDetails] = []
@callback
def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None:
@@ -191,4 +199,20 @@ class AgentManager:
async def async_setup_default_agent(self, agent: DefaultAgent) -> None:
"""Set up the default agent."""
agent.update_triggers(self.triggers_details)
self.default_agent = agent
def register_trigger(self, trigger_details: TriggerDetails) -> CALLBACK_TYPE:
"""Register a trigger."""
self.triggers_details.append(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
@callback
def unregister_trigger() -> None:
"""Unregister the trigger."""
self.triggers_details.remove(trigger_details)
if self.default_agent is not None:
self.default_agent.update_triggers(self.triggers_details)
return unregister_trigger

View File

@@ -514,7 +514,7 @@ class ChatLog:
"""Set the LLM system prompt."""
llm_api: llm.APIInstance | None = None
if user_llm_hass_api is None:
if not user_llm_hass_api:
pass
elif isinstance(user_llm_hass_api, llm.API):
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)

View File

@@ -4,13 +4,11 @@ from __future__ import annotations
import asyncio
from collections import OrderedDict
from collections.abc import Awaitable, Callable, Iterable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from enum import Enum, auto
import functools
import logging
from pathlib import Path
import re
import time
from typing import IO, Any, cast
@@ -53,6 +51,7 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_should_expose,
)
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
from homeassistant.core import Event, callback
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
@@ -74,17 +73,16 @@ from .const import DOMAIN, ConversationEntityFeature
from .entity import ConversationEntity
from .models import ConversationInput, ConversationResult
from .trace import ConversationTraceEventType, async_conversation_trace_append
from .trigger import TriggerDetails
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]
_DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
REGEX_TYPE = type(re.compile(""))
TRIGGER_CALLBACK_TYPE = Callable[
[ConversationInput, RecognizeResult], Awaitable[str | None]
]
METADATA_CUSTOM_SENTENCE = "hass_custom_sentence"
METADATA_CUSTOM_FILE = "hass_custom_file"
METADATA_FUZZY_MATCH = "hass_fuzzy_match"
@@ -110,14 +108,6 @@ class LanguageIntents:
fuzzy_responses: FuzzyLanguageResponses | None = None
@dataclass(slots=True)
class TriggerData:
"""List of sentences and the callback for a trigger."""
sentences: list[str]
callback: TRIGGER_CALLBACK_TYPE
@dataclass(slots=True)
class SentenceTriggerResult:
"""Result when matching a sentence trigger in an automation."""
@@ -153,8 +143,8 @@ class IntentCacheKey:
language: str
"""Language of text."""
device_id: str | None
"""Device id from user input."""
satellite_id: str | None
"""Satellite id from user input."""
@dataclass(frozen=True)
@@ -240,21 +230,23 @@ class DefaultAgent(ConversationEntity):
"""Initialize the default agent."""
self.hass = hass
self._lang_intents: dict[str, LanguageIntents | object] = {}
self._load_intents_lock = asyncio.Lock()
# intent -> [sentences]
self._config_intents: dict[str, Any] = config_intents
# Sentences that will trigger a callback (skipping intent recognition)
self._triggers_details: list[TriggerDetails] = []
self._trigger_intents: Intents | None = None
# Slot lists for entities, areas, etc.
self._slot_lists: dict[str, SlotList] | None = None
self._unsub_clear_slot_list: list[Callable[[], None]] | None = None
# Used to filter slot lists before intent matching
self._exposed_names_trie: Trie | None = None
self._unexposed_names_trie: Trie | None = None
# Sentences that will trigger a callback (skipping intent recognition)
self.trigger_sentences: list[TriggerData] = []
self._trigger_intents: Intents | None = None
self._unsub_clear_slot_list: list[Callable[[], None]] | None = None
self._load_intents_lock = asyncio.Lock()
# LRU cache to avoid unnecessary intent matching
self._intent_cache = IntentCache(capacity=128)
@@ -443,9 +435,15 @@ class DefaultAgent(ConversationEntity):
}
for entity in result.entities_list
}
device_area = self._get_device_area(user_input.device_id)
if device_area:
slots["preferred_area_id"] = {"value": device_area.id}
satellite_id = user_input.satellite_id
device_id = user_input.device_id
satellite_area, device_id = self._get_satellite_area_and_device(
satellite_id, device_id
)
if satellite_area is not None:
slots["preferred_area_id"] = {"value": satellite_area.id}
async_conversation_trace_append(
ConversationTraceEventType.TOOL_CALL,
{
@@ -467,8 +465,8 @@ class DefaultAgent(ConversationEntity):
user_input.context,
language,
assistant=DOMAIN,
device_id=user_input.device_id,
satellite_id=user_input.satellite_id,
device_id=device_id,
satellite_id=satellite_id,
conversation_agent_id=user_input.agent_id,
)
except intent.MatchFailedError as match_error:
@@ -534,7 +532,9 @@ class DefaultAgent(ConversationEntity):
# Try cache first
cache_key = IntentCacheKey(
text=user_input.text, language=language, device_id=user_input.device_id
text=user_input.text,
language=language,
satellite_id=user_input.satellite_id,
)
cache_value = self._intent_cache.get(cache_key)
if cache_value is not None:
@@ -1190,8 +1190,8 @@ class DefaultAgent(ConversationEntity):
fuzzy_responses=fuzzy_responses,
)
@core.callback
def _async_clear_slot_list(self, event: core.Event[Any] | None = None) -> None:
@callback
def _async_clear_slot_list(self, event: Event[Any] | None = None) -> None:
"""Clear slot lists when a registry has changed."""
# Two subscribers can be scheduled at same time
_LOGGER.debug("Clearing slot lists")
@@ -1304,28 +1304,40 @@ class DefaultAgent(ConversationEntity):
self, user_input: ConversationInput
) -> dict[str, Any] | None:
"""Return intent recognition context for user input."""
if not user_input.device_id:
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
if satellite_area is None:
return None
device_area = self._get_device_area(user_input.device_id)
if device_area is None:
return None
return {"area": {"value": satellite_area.name, "text": satellite_area.name}}
return {"area": {"value": device_area.name, "text": device_area.name}}
def _get_satellite_area_and_device(
self, satellite_id: str | None, device_id: str | None = None
) -> tuple[ar.AreaEntry | None, str | None]:
"""Return area entry and device id."""
hass = self.hass
def _get_device_area(self, device_id: str | None) -> ar.AreaEntry | None:
"""Return area object for given device identifier."""
if device_id is None:
return None
area_id: str | None = None
devices = dr.async_get(self.hass)
device = devices.async_get(device_id)
if (device is None) or (device.area_id is None):
return None
if (
satellite_id is not None
and (entity_entry := er.async_get(hass).async_get(satellite_id)) is not None
):
area_id = entity_entry.area_id
device_id = entity_entry.device_id
areas = ar.async_get(self.hass)
if (
area_id is None
and device_id is not None
and (device_entry := dr.async_get(hass).async_get(device_id)) is not None
):
area_id = device_entry.area_id
return areas.async_get_area(device.area_id)
if area_id is None:
return None, device_id
return ar.async_get(hass).async_get_area(area_id), device_id
def _get_error_text(
self,
@@ -1349,22 +1361,14 @@ class DefaultAgent(ConversationEntity):
return response_template.async_render(response_args)
@core.callback
def register_trigger(
self,
sentences: list[str],
callback: TRIGGER_CALLBACK_TYPE,
) -> core.CALLBACK_TYPE:
"""Register a list of sentences that will trigger a callback when recognized."""
trigger_data = TriggerData(sentences=sentences, callback=callback)
self.trigger_sentences.append(trigger_data)
@callback
def update_triggers(self, triggers_details: list[TriggerDetails]) -> None:
"""Update triggers."""
self._triggers_details = triggers_details
# Force rebuild on next use
self._trigger_intents = None
return functools.partial(self._unregister_trigger, trigger_data)
@core.callback
def _rebuild_trigger_intents(self) -> None:
"""Rebuild the HassIL intents object from the current trigger sentences."""
intents_dict = {
@@ -1373,8 +1377,8 @@ class DefaultAgent(ConversationEntity):
# Use trigger data index as a virtual intent name for HassIL.
# This works because the intents are rebuilt on every
# register/unregister.
str(trigger_id): {"data": [{"sentences": trigger_data.sentences}]}
for trigger_id, trigger_data in enumerate(self.trigger_sentences)
str(trigger_id): {"data": [{"sentences": trigger_details.sentences}]}
for trigger_id, trigger_details in enumerate(self._triggers_details)
},
}
@@ -1394,14 +1398,6 @@ class DefaultAgent(ConversationEntity):
_LOGGER.debug("Rebuilt trigger intents: %s", intents_dict)
@core.callback
def _unregister_trigger(self, trigger_data: TriggerData) -> None:
"""Unregister a set of trigger sentences."""
self.trigger_sentences.remove(trigger_data)
# Force rebuild on next use
self._trigger_intents = None
async def async_recognize_sentence_trigger(
self, user_input: ConversationInput
) -> SentenceTriggerResult | None:
@@ -1410,7 +1406,7 @@ class DefaultAgent(ConversationEntity):
Calls the registered callbacks if there's a match and returns a sentence
trigger result.
"""
if not self.trigger_sentences:
if not self._triggers_details:
# No triggers registered
return None
@@ -1455,7 +1451,7 @@ class DefaultAgent(ConversationEntity):
# Gather callback responses in parallel
trigger_callbacks = [
self.trigger_sentences[trigger_id].callback(user_input, trigger_result)
self._triggers_details[trigger_id].callback(user_input, trigger_result)
for trigger_id, trigger_result in result.matched_triggers.items()
]

View File

@@ -169,12 +169,11 @@ async def websocket_list_sentences(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""List custom registered sentences."""
agent = get_agent_manager(hass).default_agent
assert agent is not None
manager = get_agent_manager(hass)
sentences = []
for trigger_data in agent.trigger_sentences:
sentences.extend(trigger_data.sentences)
for trigger_details in manager.triggers_details:
sentences.extend(trigger_details.sentences)
connection.send_result(msg["id"], {"trigger_sentences": sentences})

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"]
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.10.1"]
}

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from hassil.recognize import RecognizeResult
@@ -15,7 +17,7 @@ import voluptuous as vol
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.script import ScriptRunResult
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import UNDEFINED, ConfigType
@@ -24,6 +26,18 @@ from .agent_manager import get_agent_manager
from .const import DOMAIN
from .models import ConversationInput
TRIGGER_CALLBACK_TYPE = Callable[
[ConversationInput, RecognizeResult], Awaitable[str | None]
]
@dataclass(slots=True)
class TriggerDetails:
"""List of sentences and the callback for a trigger."""
sentences: list[str]
callback: TRIGGER_CALLBACK_TYPE
def has_no_punctuation(value: list[str]) -> list[str]:
"""Validate result does not contain punctuation."""
@@ -71,6 +85,8 @@ async def async_attach_trigger(
trigger_data = trigger_info["trigger_data"]
sentences = config.get(CONF_COMMAND, [])
ent_reg = er.async_get(hass)
job = HassJob(action)
async def call_action(
@@ -92,6 +108,14 @@ async def async_attach_trigger(
for entity_name, entity in result.entities.items()
}
satellite_id = user_input.satellite_id
device_id = user_input.device_id
if (
satellite_id is not None
and (satellite_entry := ent_reg.async_get(satellite_id)) is not None
):
device_id = satellite_entry.device_id
trigger_input: dict[str, Any] = { # Satisfy type checker
**trigger_data,
"platform": DOMAIN,
@@ -100,8 +124,8 @@ async def async_attach_trigger(
"slots": { # direct access to values
entity_name: entity["value"] for entity_name, entity in details.items()
},
"device_id": user_input.device_id,
"satellite_id": user_input.satellite_id,
"device_id": device_id,
"satellite_id": satellite_id,
"user_input": user_input.as_dict(),
}
@@ -124,6 +148,6 @@ async def async_attach_trigger(
# two trigger copies for who will provide a response.
return None
agent = get_agent_manager(hass).default_agent
assert agent is not None
return agent.register_trigger(sentences, call_action)
return get_agent_manager(hass).register_trigger(
TriggerDetails(sentences=sentences, callback=call_action)
)

View File

@@ -0,0 +1,58 @@
"""The Cync integration."""
from __future__ import annotations
from pycync import Auth, Cync, User
from pycync.exceptions import AuthFailedError, CyncError
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_AUTHORIZE_STRING,
CONF_EXPIRES_AT,
CONF_REFRESH_TOKEN,
CONF_USER_ID,
)
from .coordinator import CyncConfigEntry, CyncCoordinator
_PLATFORMS: list[Platform] = [Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
"""Set up Cync from a config entry."""
user_info = User(
entry.data[CONF_ACCESS_TOKEN],
entry.data[CONF_REFRESH_TOKEN],
entry.data[CONF_AUTHORIZE_STRING],
entry.data[CONF_USER_ID],
expires_at=entry.data[CONF_EXPIRES_AT],
)
cync_auth = Auth(async_get_clientsession(hass), user=user_info)
try:
cync = await Cync.create(cync_auth)
except AuthFailedError as ex:
raise ConfigEntryAuthFailed("User token invalid") from ex
except CyncError as ex:
raise ConfigEntryNotReady("Unable to connect to Cync") from ex
devices_coordinator = CyncCoordinator(hass, entry, cync)
cync.set_update_callback(devices_coordinator.on_data_update)
await devices_coordinator.async_config_entry_first_refresh()
entry.runtime_data = devices_coordinator
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
"""Unload a config entry."""
cync = entry.runtime_data.cync
await cync.shut_down()
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -0,0 +1,118 @@
"""Config flow for the Cync integration."""
from __future__ import annotations
import logging
from typing import Any
from pycync import Auth
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_AUTHORIZE_STRING,
CONF_EXPIRES_AT,
CONF_REFRESH_TOKEN,
CONF_TWO_FACTOR_CODE,
CONF_USER_ID,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_TWO_FACTOR_SCHEMA = vol.Schema({vol.Required(CONF_TWO_FACTOR_CODE): str})
class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cync."""
VERSION = 1
cync_auth: Auth
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Attempt login with user credentials."""
errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login()
except AuthFailedError:
errors["base"] = "invalid_auth"
except TwoFactorRequiredError:
return await self.async_step_two_factor()
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_two_factor(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Attempt login with the two factor auth code sent to the user."""
errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)
try:
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
except AuthFailedError:
errors["base"] = "invalid_auth"
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
"""Create the Cync config entry using input user data."""
cync_user = self.cync_auth.user
await self.async_set_unique_id(str(cync_user.user_id))
self._abort_if_unique_id_configured()
config = {
CONF_USER_ID: cync_user.user_id,
CONF_AUTHORIZE_STRING: cync_user.authorize,
CONF_EXPIRES_AT: cync_user.expires_at,
CONF_ACCESS_TOKEN: cync_user.access_token,
CONF_REFRESH_TOKEN: cync_user.refresh_token,
}
return self.async_create_entry(title=user_email, data=config)

View File

@@ -0,0 +1,9 @@
"""Constants for the Cync integration."""
DOMAIN = "cync"
CONF_TWO_FACTOR_CODE = "two_factor_code"
CONF_USER_ID = "user_id"
CONF_AUTHORIZE_STRING = "authorize_string"
CONF_EXPIRES_AT = "expires_at"
CONF_REFRESH_TOKEN = "refresh_token"

View File

@@ -0,0 +1,87 @@
"""Coordinator to handle keeping device states up to date."""
from __future__ import annotations
from datetime import timedelta
import logging
import time
from pycync import Cync, CyncDevice, User
from pycync.exceptions import AuthFailedError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_EXPIRES_AT, CONF_REFRESH_TOKEN
_LOGGER = logging.getLogger(__name__)
type CyncConfigEntry = ConfigEntry[CyncCoordinator]
class CyncCoordinator(DataUpdateCoordinator[dict[int, CyncDevice]]):
"""Coordinator to handle updating Cync device states."""
config_entry: CyncConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: CyncConfigEntry, cync: Cync
) -> None:
"""Initialize the Cync coordinator."""
super().__init__(
hass,
_LOGGER,
name="Cync Data Coordinator",
config_entry=config_entry,
update_interval=timedelta(seconds=30),
always_update=True,
)
self.cync = cync
async def on_data_update(self, data: dict[int, CyncDevice]) -> None:
"""Update registered devices with new data."""
merged_data = self.data | data if self.data else data
self.async_set_updated_data(merged_data)
async def _async_setup(self) -> None:
"""Set up the coordinator with initial device states."""
logged_in_user = self.cync.get_logged_in_user()
if logged_in_user.access_token != self.config_entry.data[CONF_ACCESS_TOKEN]:
await self._update_config_cync_credentials(logged_in_user)
async def _async_update_data(self) -> dict[int, CyncDevice]:
"""First, refresh the user's auth token if it is set to expire in less than one hour.
Then, fetch all current device states.
"""
logged_in_user = self.cync.get_logged_in_user()
if logged_in_user.expires_at - time.time() < 3600:
await self._async_refresh_cync_credentials()
self.cync.update_device_states()
current_device_states = self.cync.get_devices()
return {device.device_id: device for device in current_device_states}
async def _async_refresh_cync_credentials(self) -> None:
"""Attempt to refresh the Cync user's authentication token."""
try:
refreshed_user = await self.cync.refresh_credentials()
except AuthFailedError as ex:
raise ConfigEntryAuthFailed("Unable to refresh user token") from ex
else:
await self._update_config_cync_credentials(refreshed_user)
async def _update_config_cync_credentials(self, user_info: User) -> None:
"""Update the config entry with current user info."""
new_data = {**self.config_entry.data}
new_data[CONF_ACCESS_TOKEN] = user_info.access_token
new_data[CONF_REFRESH_TOKEN] = user_info.refresh_token
new_data[CONF_EXPIRES_AT] = user_info.expires_at
self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)

View File

@@ -0,0 +1,45 @@
"""Setup for a generic entity type for the Cync integration."""
from pycync.devices import CyncDevice
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import CyncCoordinator
class CyncBaseEntity(CoordinatorEntity[CyncCoordinator]):
"""Generic base entity for Cync devices."""
_attr_has_entity_name = True
def __init__(
self,
device: CyncDevice,
coordinator: CyncCoordinator,
room_name: str | None = None,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._cync_device_id = device.device_id
self._attr_unique_id = device.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
manufacturer="GE Lighting",
name=device.name,
suggested_area=room_name,
)
@property
def available(self) -> bool:
"""Determines whether this device is currently available."""
return (
super().available
and self.coordinator.data is not None
and self._cync_device_id in self.coordinator.data
and self.coordinator.data[self._cync_device_id].is_online
)

View File

@@ -0,0 +1,180 @@
"""Support for Cync light entities."""
from typing import Any
from pycync import CyncLight
from pycync.devices.capabilities import CyncCapability
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ColorMode,
LightEntity,
filter_supported_color_modes,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import value_to_brightness
from homeassistant.util.scaling import scale_ranged_value_to_int_range
from .coordinator import CyncConfigEntry, CyncCoordinator
from .entity import CyncBaseEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: CyncConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Cync lights from a config entry."""
coordinator = entry.runtime_data
cync = coordinator.cync
entities_to_add = []
for home in cync.get_homes():
for room in home.rooms:
room_lights = [
CyncLightEntity(device, coordinator, room.name)
for device in room.devices
if isinstance(device, CyncLight)
]
entities_to_add.extend(room_lights)
group_lights = [
CyncLightEntity(device, coordinator, room.name)
for group in room.groups
for device in group.devices
if isinstance(device, CyncLight)
]
entities_to_add.extend(group_lights)
async_add_entities(entities_to_add)
class CyncLightEntity(CyncBaseEntity, LightEntity):
"""Representation of a Cync light."""
_attr_color_mode = ColorMode.ONOFF
_attr_min_color_temp_kelvin = 2000
_attr_max_color_temp_kelvin = 7000
_attr_translation_key = "light"
_attr_name = None
BRIGHTNESS_SCALE = (0, 100)
def __init__(
self,
device: CyncLight,
coordinator: CyncCoordinator,
room_name: str | None = None,
) -> None:
"""Set up base attributes."""
super().__init__(device, coordinator, room_name)
supported_color_modes = {ColorMode.ONOFF}
if device.supports_capability(CyncCapability.CCT_COLOR):
supported_color_modes.add(ColorMode.COLOR_TEMP)
if device.supports_capability(CyncCapability.DIMMING):
supported_color_modes.add(ColorMode.BRIGHTNESS)
if device.supports_capability(CyncCapability.RGB_COLOR):
supported_color_modes.add(ColorMode.RGB)
self._attr_supported_color_modes = filter_supported_color_modes(
supported_color_modes
)
@property
def is_on(self) -> bool | None:
"""Return True if the light is on."""
return self._device.is_on
@property
def brightness(self) -> int:
"""Provide the light's current brightness."""
return value_to_brightness(self.BRIGHTNESS_SCALE, self._device.brightness)
@property
def color_temp_kelvin(self) -> int:
"""Return color temperature in kelvin."""
return scale_ranged_value_to_int_range(
(1, 100),
(self.min_color_temp_kelvin, self.max_color_temp_kelvin),
self._device.color_temp,
)
@property
def rgb_color(self) -> tuple[int, int, int]:
"""Provide the light's current color in RGB format."""
return self._device.rgb
@property
def color_mode(self) -> str | None:
"""Return the active color mode."""
if (
self._device.supports_capability(CyncCapability.CCT_COLOR)
and self._device.color_mode > 0
and self._device.color_mode <= 100
):
return ColorMode.COLOR_TEMP
if (
self._device.supports_capability(CyncCapability.RGB_COLOR)
and self._device.color_mode == 254
):
return ColorMode.RGB
if self._device.supports_capability(CyncCapability.DIMMING):
return ColorMode.BRIGHTNESS
return ColorMode.ONOFF
async def async_turn_on(self, **kwargs: Any) -> None:
"""Process an action on the light."""
if not kwargs:
await self._device.turn_on()
elif kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
converted_color_temp = self._normalize_color_temp(color_temp)
await self._device.set_color_temp(converted_color_temp)
elif kwargs.get(ATTR_RGB_COLOR) is not None:
rgb = kwargs.get(ATTR_RGB_COLOR)
await self._device.set_rgb(rgb)
elif kwargs.get(ATTR_BRIGHTNESS) is not None:
brightness = kwargs.get(ATTR_BRIGHTNESS)
converted_brightness = self._normalize_brightness(brightness)
await self._device.set_brightness(converted_brightness)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self._device.turn_off()
def _normalize_brightness(self, brightness: float | None) -> int | None:
"""Return calculated brightness value scaled between 0-100."""
if brightness is not None:
return int((brightness / 255) * 100)
return None
def _normalize_color_temp(self, color_temp_kelvin: float | None) -> int | None:
"""Return calculated color temp value scaled between 1-100."""
if color_temp_kelvin is not None:
kelvin_range = self.max_color_temp_kelvin - self.min_color_temp_kelvin
scaled_kelvin = int(
((color_temp_kelvin - self.min_color_temp_kelvin) / kelvin_range) * 100
)
if scaled_kelvin == 0:
scaled_kelvin += 1
return scaled_kelvin
return None
@property
def _device(self) -> CyncLight:
"""Fetch the reference to the backing Cync light for this device."""
return self.coordinator.data[self._cync_device_id]

View File

@@ -0,0 +1,11 @@
{
"domain": "cync",
"name": "Cync",
"codeowners": ["@Kinachi249"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cync",
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.1"]
}

View File

@@ -0,0 +1,69 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional 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 provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
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:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: todo
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

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