Compare commits

...

589 Commits

Author SHA1 Message Date
Robert Resch
4a68cce4df Bump deebot-client to 18.1.0 2026-03-25 17:07:26 +01:00
Paul Bottein
b522db1daf Use state selector for climate service mode fields (#166486) 2026-03-25 16:43:41 +01:00
Paul Bottein
338836cba2 Use state selector for light service fields (#166489) 2026-03-25 16:43:24 +01:00
Paul Bottein
f5e7605502 Use state selector for fan service fields (#166488) 2026-03-25 16:43:11 +01:00
Paul Bottein
22ddb18ce2 Use state selector for humidifier service fields (#166487) 2026-03-25 16:42:52 +01:00
crash0verride11
b541dc0a97 Add names for sound programs in Yamaha Musiccast (#166231)
Co-authored-by: crash0verride11 <3526616+crash0verride11@users.noreply.github.com>
Co-authored-by: jtjart <80978647+jtjart@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-25 16:42:48 +01:00
Mike Degatano
15d0a01833 Replace calls to ingress panels API with aiohasupervisor (#166400) 2026-03-25 16:42:32 +01:00
Abode Systems
71be2073eb Add measurement state class for Abode multi-sensor entities (#166431) 2026-03-25 16:42:06 +01:00
Ronald van der Meer
e6886fc562 Add binary sensors for PoolDose delay/pump status entities (#166485)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-25 16:37:31 +01:00
Joost Lekkerkerker
7f0f038bcd Add entities for stick vacuum cleaner to SmartThings (#166127) 2026-03-25 16:28:20 +01:00
Joost Lekkerkerker
686ab66a52 Add sensors for more game modes to Chess.com (#166331) 2026-03-25 16:27:58 +01:00
hanwg
7a4f953fa6 Add send_media_group action for Telegram bot (#160939)
Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 16:18:25 +01:00
Erwin Douna
cd0834bfbe Add storages to Proxmox (#166409) 2026-03-25 16:11:41 +01:00
AlCalzone
c598aa6964 Re-discover Z-Wave list sensors when metadata states change (#166271)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-25 16:10:25 +01:00
Willem-Jan van Rootselaar
5ef28932e5 Bump python-bsblan to 5.1.3 (#166479) 2026-03-25 15:51:37 +01:00
Erik Montnemery
f2eac87673 Fix handling of units in NumericThresholdSelector (#166475) 2026-03-25 15:41:17 +01:00
Michael
aeb920e8ef Add domain driven triggers to counter helper (#164545)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-25 15:40:15 +01:00
Petar Petrov
8540a27f0d Filter artificial zero values at UTC midnight from Forecast.Solar data (#166447) 2026-03-25 15:14:48 +01:00
jorgenvi
fe2d8a31b8 Add battery sensor to Roth Touchline SL integration (#166283)
Co-authored-by: Jørgen Vinne Iversen <jorgenvi@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 15:05:38 +01:00
Erwin Douna
f4efc929d6 Fix Proxmox offline node (#165986)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-25 15:04:31 +01:00
Eniot
15d7febffd feat(transmission): add session and cumulative stats sensors (#166134) 2026-03-25 14:44:47 +01:00
Andres Ruiz
0a8f5449f2 Add initial quality scale for waterfurnace (#165756)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-25 14:41:29 +01:00
Fredrik Mårtensson
d2179d9243 Bump tuya-device-handlers to 0.0.15 (#166477) 2026-03-25 14:40:02 +01:00
7eaves
bf1327e355 Fix Meter Pro CO2 not discoverable via BT proxies (#165173)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 14:38:52 +01:00
Erwin Douna
9afa827eab Add backups sensors to Proxmox (#166380) 2026-03-25 14:35:52 +01:00
Mike O'Driscoll
3ae6f8e7a0 Updates for Casper glow Integraiton - Add Buttons (#166083)
Signed-off-by: Mike O'Driscoll <mike@unusedbytes.ca>
2026-03-25 14:32:47 +01:00
Tom Matheussen
56962ff907 Update IQS to Bronze for Satel Integra (#166469) 2026-03-25 14:31:32 +01:00
Erwin Douna
719b9bdc3c Add snapshot button to Proxmox (#166462) 2026-03-25 14:27:43 +01:00
Renat Sibgatulin
bb1dc51a6b Add a missing regression test for airq config flow (#166473) 2026-03-25 14:25:18 +01:00
Nathan Spencer
abbbb7df13 Bump pylitterbot to 2025.2.0 and update Litter-Robot 3 test data to match underlying API data (#166350)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-25 14:11:58 +01:00
Robert Resch
5a308d11e4 Bump uv to 0.11.1 (#166472) 2026-03-25 14:08:07 +01:00
Jordan Harvey
6bf487c3f3 Bump pynintendoparental to 2.3.3 (#166471) 2026-03-25 14:07:56 +01:00
Bram Kragten
3162b637ea Add mode to numeric threshold selector (#166453) 2026-03-25 13:56:44 +01:00
Michael
8cc1dd8091 Add is_closed state attribute to valve (#165227) 2026-03-25 13:49:41 +01:00
Ariel Ebersberger
83ff038188 Add humidifier condition (#166464) 2026-03-25 12:18:18 +00:00
Erik Montnemery
13a8d7f7a8 Add moisture triggers (#166249)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-25 13:01:11 +01:00
Lukas
a721d32889 Pooldose additional entities for advanced pooldose device (#165608)
Co-authored-by: Ronald van der Meer <ronald@vandermeer.frl>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-25 12:54:32 +01:00
Laxen
bce65d4f35 Allow test vendor IDs to set Matter label (#161974) 2026-03-25 12:23:45 +01:00
Franck Nijhof
daa0ddffb9 Improve scene action naming consistency (#166456) 2026-03-25 12:21:35 +01:00
Galorhallen
ee7dd329f0 Update and fix govee light local (#166454)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 12:08:02 +01:00
Tom Matheussen
00cd07736e Bump satel_integra to 1.0.0 (#164257) 2026-03-25 12:05:46 +01:00
Renat Sibgatulin
78871e1766 Permit manual setup for air-Q integration alongside zeroconf (#166459) 2026-03-25 11:59:42 +01:00
AlCalzone
bb6f739861 Fix AssertionError for Z-Wave opening state value on non-zero endpoint (#166461) 2026-03-25 11:52:41 +01:00
Ariel Ebersberger
9948431012 Add illuminance conditions (#166353)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-25 11:01:14 +01:00
Erik Montnemery
4f9241be79 Add air quality conditions (#166407) 2026-03-25 10:35:41 +01:00
Mike O'Driscoll
5215e674b1 Add Binary Sensors to Casper Glow (#166130)
Signed-off-by: Mike O'Driscoll <mike@unusedbytes.ca>
2026-03-25 10:17:15 +01:00
Joost Lekkerkerker
31b12701dc Migrate touchline to has_entity_name = true (#166403) 2026-03-25 10:15:16 +01:00
Stefan Agner
d5ff890a18 Use Unix socket for Supervisor communication (#163907)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 10:06:36 +01:00
hanwg
32221a1ec4 Fix open sockets in tests for Telegram bot (#166451) 2026-03-25 09:45:44 +01:00
Sab44
a6dd56eed0 Add custom equivalent units to recorder platform (#164893) 2026-03-25 09:42:45 +01:00
tronikos
682eba9773 Bump opower to 0.18.0 (#166444) 2026-03-25 09:19:22 +01:00
J. Diego Rodríguez Royo
c055972887 Get program from base program option at Home Connect (#164885) 2026-03-25 09:12:51 +01:00
Raphael Hehl
78e2514b46 Bump uiprotect to 10.2.3 (#166406)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-25 09:09:17 +01:00
Franck Nijhof
0af6a86507 Improve reload action naming for YAML-based integrations (#166442) 2026-03-25 09:03:33 +01:00
Erwin Douna
2367d7c168 Proxmox add runtime entities (#166416) 2026-03-25 08:55:14 +01:00
Franck Nijhof
8d91fd0655 Improve lock action naming consistency (#166445) 2026-03-25 08:54:41 +01:00
mettolen
171b8dfa89 Add Presentation light to Liebherr (#166154) 2026-03-25 08:12:46 +01:00
jorgenvi
f299b009fa Add PARALLEL_UPDATES to Touchline SL climate platform (#166415)
Co-authored-by: Jørgen Vinne Iversen <jorgenvi@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 08:05:36 +01:00
Abode Systems
91e9eb0ab3 Fix Abode retrofit lock discovery (#166433) 2026-03-25 08:04:44 +01:00
Maciej Bieniek
a2b91a9ac0 Fix KeyError for device temperature sensor in Unifi integration (#166410) 2026-03-25 08:00:14 +01:00
Brett Adams
a3add179a0 Fix Tesla Fleet partner_login to not require vehicle scope. (#166435) 2026-03-25 07:46:47 +01:00
Franck Nijhof
6075becbab Improve siren action naming consistency (#166399) 2026-03-25 07:20:58 +01:00
Marc Mueller
193f519366 Warn about *.pth files in dependencies (#166411) 2026-03-25 07:18:47 +01:00
Brett Adams
b6508c2ca4 Bump Tesla Fleet API to 1.4.5 (#166432) 2026-03-25 07:12:52 +01:00
Paulus Schoutsen
3dc478a357 Filter out WiiM devices from LinkPlay discovery (#166436) 2026-03-25 07:11:38 +01:00
TheJulianJES
bd407872b0 Bump ZHA to 1.1.0 (#166438) 2026-03-25 07:10:00 +01:00
Franck Nijhof
8b696044c3 Improve select action naming consistency (#166398) 2026-03-25 06:55:13 +01:00
Abílio Costa
1a772b6df2 Add button platform to LG Infrared (#166375) 2026-03-25 06:53:16 +01:00
Allen Porter
a880ad2904 Update Roborock entities to handle unavailable data (#165618) 2026-03-24 20:20:37 -07:00
mettolen
ea73f2d0f1 Refactor Huum test fixtures (#166115) 2026-03-25 00:22:25 +01:00
Magnus Nordseth
11351500ea Update Touchline codeowner (#166420) 2026-03-25 00:20:01 +01:00
Erwin Douna
86901bfd80 Add suspend all button Proxmox (#166417) 2026-03-24 22:36:38 +00:00
Franck Nijhof
d2ef60125f Improve update action naming consistency (#166401) 2026-03-24 22:20:24 +00:00
Brett Adams
471b49f12b Mark Tessie docs-data-update quality scale item as done (#166404) 2026-03-24 22:03:08 +01:00
Andries Louw Wolthuizen
33e9e663da Add Conductivity (EC), pH, ORP support to Tuya DGNBJ (#159584)
Co-authored-by: ramarro123 <5493729+ramarro123@users.noreply.github.com>
Co-authored-by: Erik <erik@montnemery.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-24 21:58:17 +01:00
Joost Lekkerkerker
31ff44f1a6 Use common preset names in Touchline (#166390) 2026-03-24 21:52:24 +01:00
Raj Laud
9274bd7867 Bump pysqueezebox to 0.14.0 (#166395)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 21:49:45 +01:00
andreimoraru
e36f9eb639 Bump yt-dlp version to 2026.03.17 (#166394) 2026-03-24 21:48:54 +01:00
Franck Nijhof
5149932ec8 Improve button action naming consistency (#166385) 2026-03-24 21:47:20 +01:00
Franck Nijhof
bdd3fc7059 Improve lawn mower action naming consistency (#166388) 2026-03-24 21:45:53 +01:00
Franck Nijhof
c795cbc5a3 Improve to-do list action naming consistency (#166393) 2026-03-24 21:44:27 +01:00
Franck Nijhof
20dd604292 Improve number action naming consistency (#166391) 2026-03-24 21:44:10 +01:00
Paul Laffitte
c35a6dc044 Add reconfiguration flow to QNAP (#166064)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 21:30:43 +01:00
Franck Nijhof
cbe767c9c5 Improve Home Assistant core action naming consistency (#166387) 2026-03-24 21:13:31 +01:00
Nathan Spencer
eea3b78665 Update Whisker quality scale to platinum (#166369) 2026-03-24 20:57:41 +01:00
Simone Chemelli
a78a553bab Align FritzBoxProfileSwitch signature for Fritz (#165601) 2026-03-24 20:42:41 +01:00
Joost Lekkerkerker
7c7af7f0df Add basic climate tests to Touchline (#166360) 2026-03-24 20:40:07 +01:00
Simone Chemelli
d52ad38dca Add reconfigure config flow to SamsungTV (#165907) 2026-03-24 20:38:28 +01:00
David Bishop
477384ce9b Use current track's album thumbnail as our entity_picture for "radio" sources in Music Assistant (#166302)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:30:42 +01:00
Franck Nijhof
d1be6e1c68 Improve input helper action naming consistency (#166382) 2026-03-24 20:26:41 +01:00
Artur Pragacz
151eae4d5a Add compatibility layer for entities without has_entity_name to entity registry (#166246) 2026-03-24 20:22:15 +01:00
Radded
035e0042fa Battery status for Roborock Q7 (#165886)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-24 20:11:04 +01:00
Tom
2568db5fdf Add API token authentication to Proxmox (#166197)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 20:09:57 +01:00
Franck Nijhof
28b1ded702 Improve calendar action naming consistency (#166378) 2026-03-24 19:02:05 +00:00
Franck Nijhof
236cd795b9 Improve climate action naming consistency (#166361) 2026-03-24 18:58:17 +00:00
Franck Nijhof
65e90b9b9f Improve fan action naming consistency (#166379) 2026-03-24 18:55:09 +00:00
Franck Nijhof
96c3f3f054 Improve camera action naming consistency (#166381) 2026-03-24 18:53:51 +00:00
Frank Wickström
bd8e90bb00 Activate strict type checks for Huum integration (#166357)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 19:45:14 +01:00
Eniot
d488bdad8a Add port forwarding binary sensor to Transmission (#166108)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 19:44:24 +01:00
Franck Nijhof
dec6f955f3 Improve switch action naming consistency (#166376) 2026-03-24 19:39:50 +01:00
Franck Nijhof
bdb74ca37a Improve alarm control panel action naming consistency (#166367) 2026-03-24 19:36:43 +01:00
Franck Nijhof
14c0a82284 Improve cover action naming consistency (#166366)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 19:32:17 +01:00
Artur Pragacz
b42bd4909b Move pipeline input validation into execute method (#166373) 2026-03-24 19:32:13 +01:00
Franck Nijhof
001a1aada6 Improve light action naming consistency (#166362) 2026-03-24 19:31:01 +01:00
Abílio Costa
cd28c924ac Move common code to entity class in LG Infrared (#166371) 2026-03-24 19:22:56 +01:00
Ariel Ebersberger
a19c1a7ba1 Rename humidity.value condition to humidity.is_value (#166372) 2026-03-24 19:17:25 +01:00
Franck Nijhof
e0d3298e77 Improve vacuum action naming consistency (#166359) 2026-03-24 19:09:25 +01:00
Mattie
2296c92a3e Add Qube Heat Pump integration (#160409)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-24 19:06:56 +01:00
Erik Montnemery
66311508ad Add power conditions (#166364) 2026-03-24 19:05:16 +01:00
Raphael Hehl
d628463471 Add image platform to UniFi Access integration (#165848)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-24 18:49:18 +01:00
Artur Pragacz
a5f9c400cc Fix limited template unsupported lists (#166356) 2026-03-24 18:20:30 +01:00
Bram Kragten
36051d015a Add numeric threshold selector (#166314) 2026-03-24 18:16:53 +01:00
Ariel Ebersberger
65ae221ba7 Add humidity condition (#166358) 2026-03-24 18:14:11 +01:00
Abílio Costa
0fd9360249 Add LG Infrared integration (#162359) 2026-03-24 18:10:18 +01:00
Erik Montnemery
55f56c6632 Add water_heater conditions (#166335)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 17:56:49 +01:00
Paulus Schoutsen
0336ffca77 Handle validation error when starting stream from audio (#166185) 2026-03-24 11:45:08 -05:00
Magnus Nordseth
f33bd2de22 Add unique_id and device info to Roth Touchline (#166289) 2026-03-24 17:19:28 +01:00
David Bishop
0599550e04 Add DHCP discovery support to Whisker integration (#165635)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 17:11:20 +01:00
Simone Chemelli
c384d41625 Bump aioamazondevices to 13.3.0 (#166346) 2026-03-24 17:02:19 +01:00
Michael Hansen
57b0456760 Bump intents to 2026.3.24 (#166355) 2026-03-24 10:55:30 -05:00
Erwin Douna
85c9b00035 Portainer add runtime entities (#166320) 2026-03-24 16:22:34 +01:00
Franck Nijhof
d9df5f1fab Fix unmocked DNS lookups in minecraft_server config flow tests (#166347) 2026-03-24 16:21:00 +01:00
SOLARMAN
f3cea5160b Add solarman integration (#152525)
Co-authored-by: xiaozhouhhh <13588112144@163.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 16:09:14 +01:00
alorente
ac7b5a2957 Fix Firefly iii sensors not updating (#165450)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-24 16:01:11 +01:00
Erik Montnemery
031830f004 Add context support for conditions.yaml (#166333) 2026-03-24 15:57:11 +01:00
epenet
39a655e100 Migrate Tuya climate to TuyaClimateDefinition (#166351) 2026-03-24 15:54:29 +01:00
Ariel Ebersberger
714411c072 Rename battery.percentage to battery.is_level (#166348) 2026-03-24 15:44:44 +01:00
epenet
94eb1031cc Migrate remaning Tuya entities to TuyaDefinition (#166345)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 15:33:22 +01:00
epenet
fa98eb52ad Migrate Tuya fan to TuyaFanDefinition (#166344) 2026-03-24 15:25:59 +01:00
Manu
7b1fbbd278 Allow subclasses to set state in NotifyEntity (#154127)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 15:24:26 +01:00
Josef Zweck
b518729367 Bump aiotedee to 0.3.0 (#166321)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-24 15:23:48 +01:00
Stefan Agner
d04c5ccc44 Bump aiohasupervisor to 0.4.3 (#166315) 2026-03-24 15:19:24 +01:00
epenet
d8ba32bc8e Migrate Tuya camera to TuyaCameraDefinition (#166343) 2026-03-24 15:18:15 +01:00
Jan Čermák
7ae3c2012d Remove intel-nuc machine from image build matrix (#166326) 2026-03-24 15:17:13 +01:00
epenet
05b78a22cf Migrate Tuya light to TuyaLightDefinition (#166337) 2026-03-24 15:11:34 +01:00
epenet
0a5589c800 Migrate Tuya sensor to TuyaSensorDefinition (#166341) 2026-03-24 15:11:23 +01:00
epenet
9fb5bceeef Migrate Tuya humidifier to TuyaHumidifierDefinition (#166340) 2026-03-24 15:09:47 +01:00
epenet
f4cce71d1f Migrate Tuya vacuum to TuyaVacuumDefinition (#166339) 2026-03-24 15:09:01 +01:00
Erik Montnemery
2209c9e0f7 Fix bug in EntityOriginStateTriggerBase (#166324) 2026-03-24 15:03:33 +01:00
Matthias Alphart
979045bed3 Bump pyfronius to 0.8.2 (#166334) 2026-03-24 14:52:02 +01:00
epenet
d3a8a7e9be Migrate Tuya binary sensor to TuyaBinarySensorDefinition (#166330) 2026-03-24 14:48:16 +01:00
epenet
ca63f299ff Migrate Tuya cover to TuyaCoverDefinition (#166328) 2026-03-24 14:44:37 +01:00
Josef Zweck
1e9c8ec32c Add upload progress tracking to S3 integrations (#166325) 2026-03-24 14:44:05 +01:00
epenet
f38f3626fb Migrate Tuya alarm to TuyaAlarmDefinition (#166329) 2026-03-24 14:42:36 +01:00
TheJulianJES
4a3cc511a7 Bump universal-silabs-flasher to 1.0.3 (#166338) 2026-03-24 14:42:13 +01:00
TimL
b4e012fcdf Add light platform to SMLIGHT integration (#166092) 2026-03-24 14:41:11 +01:00
Erik Montnemery
9da9eaf338 Add power triggers (#166253) 2026-03-24 14:38:07 +01:00
Retha Runolfsson
422d69f2b3 Bump PySwitchbot to 2.0.0 (#165995)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-24 14:29:02 +01:00
Erik Montnemery
583524e841 Add illuminance triggers (#166250)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 14:04:48 +01:00
Joost Lekkerkerker
740e21a23b Add new fridge fixture to SmartThings (#165198) 2026-03-24 13:56:36 +01:00
Michael
9693ca39d1 Also listen for input_text in text.changed trigger (#165161)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 13:27:44 +01:00
epenet
52a0ed6c1c Bump tuya-device-handlers to 0.0.14 (#166323) 2026-03-24 13:14:17 +01:00
Mike Woudenberg
1702a594aa Bump LoqedAPI to 2.1.11 (#166311) 2026-03-24 13:08:53 +01:00
Joakim Sørensen
e6b7ce97f3 Add progress tracking when uploading a cloud backup (#166316) 2026-03-24 12:10:38 +01:00
Robert Resch
0b13274271 Add some water heater triggers (#164864)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 11:59:33 +01:00
Erik Montnemery
580ae1e81b Add numerical climate conditions (#166309) 2026-03-24 11:38:14 +01:00
Erik Montnemery
4c802fba7e Remove useless string split from conditions (#166319) 2026-03-24 11:32:18 +01:00
Franck Nijhof
41031b1cad Merge branch 'master' into dev 2026-03-24 09:14:56 +00:00
Erik Montnemery
ff59604085 Use helper when creating air_quality triggers (#166287) 2026-03-24 09:39:34 +01:00
Erwin Douna
f9cac69172 Add network sensors to Proxmox (#166281) 2026-03-24 09:37:18 +01:00
Erik Montnemery
81a8dee22a Add event entity triggers (#165456) 2026-03-24 08:20:21 +01:00
Franck Nijhof
00d5e89951 2026.3.4 (#166285) 2026-03-24 08:11:42 +01:00
Jan-Philipp Benecke
748f8b78f7 Handle invalid manifest in WebDAV backup agent gracefully (#166306) 2026-03-24 08:10:49 +01:00
Allen Porter
191f49a326 Add RFC9728 OAuth2 Protected Resource metadata endpoint (#166213)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-24 08:06:48 +01:00
Erwin Douna
8178c8afa0 Add new sensors to Proxmox (#166275) 2026-03-24 07:48:35 +01:00
Petro31
557d072a4d Update template light test framework (#164688) 2026-03-24 06:38:58 +00:00
Brett Adams
2d4c96864b Add exception translations to Tessie (#166047) 2026-03-23 23:20:36 +01:00
Artur Pragacz
745dc0e183 Remove stale area entries from limited template unsupported lists (#166079) 2026-03-23 23:15:53 +01:00
Artur Pragacz
8d63c9ccbd Fix set states in service intent handler (#165432) 2026-03-23 23:10:04 +01:00
J. Nick Koston
713475ddb0 Log ffmpeg conversion errors in ESPHome media proxy (#166086) 2026-03-23 23:06:55 +01:00
J. Nick Koston
4badc291d9 Don't update ESPHome host when device is already connected (#166084) 2026-03-23 23:03:52 +01:00
Maciej Bieniek
aa83f534c1 Improve error handling in the Tractive config flow (#166290) 2026-03-23 23:01:21 +01:00
Maciej Bieniek
b3d51a061a Bump aiotractive to 1.0.1 (#166288) 2026-03-23 23:00:54 +01:00
Raphael Hehl
7e707d757a Bump UniFi Access integration to Silver quality scale (#166216)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-23 22:59:07 +01:00
Brett Adams
8c71965557 Fix Tesla Fleet token refresh handling for expired tokens (#165354) 2026-03-23 22:55:56 +01:00
Frank Wickström
4e42478ece Add diagnostics to Huum integration (#166230) 2026-03-23 22:49:41 +01:00
Ariel Ebersberger
03c672a4f3 Add battery conditions (#165208)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-23 22:18:48 +01:00
Erik Montnemery
66b5a3755c Add text conditions (#165918) 2026-03-23 20:41:48 +01:00
Franck Nijhof
6c3917e927 Bump version to 2026.3.4 2026-03-23 19:24:24 +00:00
Bram Kragten
e895c1b2fd Update frontend to 20260312.1 (#166251) 2026-03-23 19:20:37 +00:00
Matrix
dae971cd98 Bump yolink-api to 0.6.3 (#166232) 2026-03-23 19:20:36 +00:00
Peter Grauvogel
807df50eab Bump greenplanet-energy-api from 0.1.4 to 0.1.10 (#166217) 2026-03-23 19:15:57 +00:00
MarkGodwin
aa05ff03b3 Bump tplink-omada-client to fix breaking changes in Omada API (#166206) 2026-03-23 19:15:56 +00:00
Norbert Rittel
c645bbb3f8 Replace "grid return" with "grid export" in opower issue (#165888)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-23 12:12:30 -07:00
Erik Montnemery
319f9fda92 Add air quality triggers (#166248)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 19:31:29 +01:00
cdnninja
f9525ebda7 Add switch tests to vesync (#163325) 2026-03-23 17:46:35 +00:00
Tommy Goode
622b92682e Fix zwave_js fan speed mapping for GE/Jasco Enbrighten 55258 / ZW4002 (#166169) 2026-03-23 17:46:26 +00:00
J. Nick Koston
a81146a227 Bump oralb-ble to 1.1.0 (#166165) 2026-03-23 17:46:24 +00:00
Artur Pragacz
579dd6785d Add entity name template function (#166078) 2026-03-23 18:33:45 +01:00
Abílio Costa
84992b875a Allow TODO entity listeners to handle None state (#166276) 2026-03-23 17:32:41 +00:00
EnjoyingM
530dcadf19 Bump wolf_comm to 0.0.48 (#166144) 2026-03-23 17:31:22 +00:00
Michael
4aa67ddf22 Fix reload of FRITZ!Box Tools in case of connection issues (#166111) 2026-03-23 17:24:39 +00:00
Josef Zweck
8e95b19c4c Bump aiotedee to 0.2.27 (#166101)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-23 17:24:38 +00:00
Sean O'Keeffe
5558b33600 Add additional miele oven programs (#166100) 2026-03-23 17:24:36 +00:00
Ray Xue
0130ac6770 Bump xiaomi-ble to 1.10.0 (#166099) 2026-03-23 17:24:35 +00:00
tronikos
26d22e4d62 Bump python-google-weather-api to 0.0.6 (#166085) 2026-03-23 17:24:33 +00:00
Jack Boswell
532bc02d66 Update starlink-grpc-core to 1.2.4 (#165882) 2026-03-23 17:24:32 +00:00
Petro31
893eac0e84 Correct validation of scripts in template entities (#165226) 2026-03-23 17:22:39 +00:00
Magnus Nordseth
18a6478d9a Add config flow to touchline integration (#165790)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-23 17:50:46 +01:00
epenet
3d1a8fb08c Use SensorDeviceClass.PH in mysensors (#166274) 2026-03-23 17:43:59 +01:00
Erik Montnemery
3657a8eb07 Adjust humidity triggers (#166261)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 17:31:52 +01:00
epenet
83e8d1878b Simplify Tuya climate entity initialisation (#166277) 2026-03-23 17:17:04 +01:00
mettolen
6f635adb6b Bump pyliebherrhomeapi to 0.4.1 (#166269) 2026-03-23 17:14:08 +01:00
epenet
b3f4805afe Add missing type hint to Camera entity description (#166273) 2026-03-23 17:11:49 +01:00
Joakim Sørensen
b70651a811 Bump hass-nabucasa from 2.0.0 to 2.2.0 (#166267) 2026-03-23 17:10:16 +01:00
epenet
dc1e330e4a Simplify Tuya entity initialisation (#166266) 2026-03-23 16:59:56 +01:00
Erik Montnemery
a45da11ec1 Adjust light triggers (#166263)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-23 16:48:33 +01:00
Erik Montnemery
31c7553e68 Minor improvements of occupancy trigger tests (#166265) 2026-03-23 16:22:25 +01:00
Erik Montnemery
44e704a6e0 Minor improvements of motion trigger tests (#166264) 2026-03-23 16:21:16 +01:00
Erik Montnemery
2824919a20 Minor improvements of temperature trigger tests (#166259) 2026-03-23 16:17:59 +01:00
Erik Montnemery
ebe0e3ace7 Minor improvements of cover trigger tests (#166256) 2026-03-23 16:03:27 +01:00
Erik Montnemery
e151c9c78c Adjust temperature trigger translations (#166260) 2026-03-23 14:52:30 +01:00
Erik Montnemery
7287c847f4 Remove redundant humidity trigger test (#166257) 2026-03-23 14:31:24 +01:00
Petro31
152e17aee7 Update state template framework to support options other than state (#162737) 2026-03-23 14:26:21 +01:00
Petro31
c53adcb73b Correct validation of scripts in template entities (#165226) 2026-03-23 14:08:11 +01:00
Abílio Costa
dab4a72128 Add copilot-specific instructions (#166254)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 13:06:48 +00:00
hanwg
c94e10efa7 Improve subentry error handling for Telegram bot (#165863)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-23 13:46:13 +01:00
Bram Kragten
ca5ea9ea35 Update frontend to 20260312.1 (#166251) 2026-03-23 13:38:04 +01:00
Mike Degatano
63a09d8e28 Replace calls to set options in Supervisor with aiohasupervisor (#165872)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-23 13:18:37 +01:00
Martin Hjelmare
b5a3c2c014 Fix trane for Python 3.14.3 (#166252) 2026-03-23 13:17:49 +01:00
puddly
ef887c8edc Use device-specific firmware flashers for Yellow/ZBT-1/ZBT-2 (#164695) 2026-03-23 13:01:10 +01:00
epenet
d0eb90274d Cleanup deprecated YAML import from vera (#165659) 2026-03-23 13:00:20 +01:00
Paulus Schoutsen
cac375dafb Only start Assist Pipeline debug thread when capturing audio (#166190) 2026-03-23 12:46:23 +01:00
AlCalzone
2c20b62229 Create repair issue for legacy Z-Wave Door state sensors that are still in use (#165363)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-23 12:00:47 +01:00
Tommy Goode
b5c84b6b7a Fix zwave_js fan speed mapping for GE/Jasco Enbrighten 55258 / ZW4002 (#166169) 2026-03-23 11:55:46 +01:00
Raphael Hehl
e5f9668ded Bump py-unifi-access to 1.1.3 (#166177)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-23 11:54:03 +01:00
Martin Hjelmare
e214ce690a Refactor Z-Wave discovery schemas for sensor platform (#165254)
Co-authored-by: AlCalzone <d.griesel@gmx.net>
2026-03-23 11:43:30 +01:00
Erik Montnemery
a2c64f65e1 Add support for input_boolean to switch triggers (#166242) 2026-03-23 11:42:29 +01:00
Matrix
8bad30234a Add YoLink YS7A06 support (#165987) 2026-03-23 11:19:27 +01:00
dependabot[bot]
c4545b42d8 Bump dorny/paths-filter from 3.0.2 to 4.0.1 (#166237)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 11:08:25 +01:00
Eli Sand
b0a60d1c42 Fixes generic_thermostat config flow validation (#165680) 2026-03-23 10:18:45 +01:00
Norbert Rittel
e1e14bee10 Clarify description of motion_blinds.set_absolute_position action (#166243) 2026-03-23 10:10:10 +01:00
Erik Montnemery
3529aff4b1 Revert "Add turned off and turned on triggers to input boolean (#158824)" (#166240) 2026-03-23 08:46:03 +01:00
Matrix
16e314ccf1 Bump yolink-api to 0.6.3 (#166232) 2026-03-23 08:34:37 +01:00
Erik Montnemery
d634fbcad7 Add unit of measurement handling to numeric climate triggers (#166211) 2026-03-23 08:29:01 +01:00
Ray Xue
b84ca80d55 Add Linptech PS1BB pressure sensor support to xiaomi_ble (#166095) 2026-03-23 00:21:28 +01:00
David Bonnes
41c2c621f0 Bump evohome-async to 1.2.0 (#166227) 2026-03-23 00:10:38 +01:00
Peter Grauvogel
b230e62868 Bump greenplanet-energy-api from 0.1.4 to 0.1.10 (#166217) 2026-03-22 22:08:06 +01:00
Ludovic BOUÉ
12528ec128 Update python-roborock to 5.0.0 (#166219) 2026-03-22 13:38:31 -07:00
Manu
7f4a7670a2 Bump pyrate-limiter to 4.1.0 (#166221) 2026-03-22 13:38:19 -07:00
Erwin Douna
9bdc1b777e Add async_setup and yarl to Immich coordinator (#165900)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-22 21:36:16 +01:00
Stathogon
995e982d7f Add shutdown button for VMs in ProxmoxVE (#165890) 2026-03-22 21:29:59 +01:00
MarkGodwin
b92698e3d5 Bump tplink-omada-client to fix breaking changes in Omada API (#166206) 2026-03-22 19:44:38 +01:00
Ludovic BOUÉ
225052b932 feat(roborock): Remove unnecessary type check for Q10 update coordinator in button setup (#166214) 2026-03-22 19:42:18 +01:00
Raphael Hehl
34ae51677f Add a reauthentication flow to the UniFi Access integration (#165859)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-22 18:09:46 +01:00
Erik Montnemery
3616a52b37 Add temperature triggers (#165247) 2026-03-22 15:24:53 +01:00
Ludovic BOUÉ
0128372258 Update python-roborock to 4.26.3 (#166178) 2026-03-22 14:01:23 +01:00
EnjoyingM
21863cd9d7 Bump wolf_comm to 0.0.48 (#166144) 2026-03-22 10:27:18 +01:00
Sean O'Keeffe
d67caec5c1 Add additional miele oven programs (#166100) 2026-03-22 09:04:07 +01:00
J. Nick Koston
8286014ae1 Bump habluetooth to 5.11.1 (#166161) 2026-03-21 18:22:53 -10:00
J. Nick Koston
1ff8d2279a Bump oralb-ble to 1.1.0 (#166165) 2026-03-21 18:22:21 -10:00
Ludovic BOUÉ
5dcbc1d5d9 feat(roborock): Add Q10 empty dustbin button entity (#166149) 2026-03-22 00:36:43 +01:00
Ludovic BOUÉ
3068653cc7 Update python-roborock to 4.26.2 (#166152) 2026-03-21 23:44:02 +01:00
Paulus Schoutsen
61b1a45889 Add logger to OpenDisplay (#166146) 2026-03-21 22:30:01 +01:00
Ray Xue
573d4eba02 Bump xiaomi-ble to 1.10.0 (#166099) 2026-03-21 20:40:54 +01:00
Ludovic BOUÉ
09895aa601 Update python-roborock to 4.26.1 (#166138) 2026-03-21 20:25:24 +01:00
Joost Lekkerkerker
aa6a4c7eab Add binary sensor for stick cleaner status to SmartThings (#166122) 2026-03-21 20:24:53 +01:00
Michael
662c44b125 Fix reload of FRITZ!Box Tools in case of connection issues (#166111) 2026-03-21 20:24:21 +01:00
Josef Zweck
5a80087cf4 Bump aiotedee to 0.2.27 (#166101)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-21 20:23:15 +01:00
TimL
c28dc32168 Add PSRAM sensor for SMLIGHT integration (#166104) 2026-03-21 20:20:33 +01:00
Matthias Alphart
eef3472c43 KNX: Clean up internal setting of name, unique_id and entity_category for YAML entities (#160265) 2026-03-21 20:12:10 +01:00
tronikos
f9bd9f4982 Add diagnostics in Google Weather (#166105) 2026-03-21 18:43:45 +01:00
Jack Boswell
e4620a208d Update starlink-grpc-core to 1.2.4 (#165882) 2026-03-21 18:30:04 +01:00
Ingmar Stein
c6c5661b4b Add Identify button to Velux integration (#163893)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:20:02 +01:00
Joost Lekkerkerker
d0154e5019 Add stick cleaner fixture to SmartThings (#166121) 2026-03-21 16:57:26 +01:00
Joost Lekkerkerker
16fb7ed21e Bump TRMNL to platinum (#166066) 2026-03-21 06:49:50 +01:00
tronikos
d0a751abe4 Bump python-google-weather-api to 0.0.6 (#166085) 2026-03-20 17:02:22 -07:00
Franck Nijhof
c1bd83c9c0 2026.3.3 (#166076) 2026-03-20 23:01:26 +01:00
J. Nick Koston
a04b168a19 Bump aioesphomeapi to 44.6.2 (#166080) 2026-03-20 22:53:06 +01:00
TimL
b3c27e9f93 Bump Pysmlight 0.3.1 (#166060) 2026-03-20 20:26:10 +00:00
TimL
92e237ade2 Bump Pysmlight to 0.3.0 (#165658) 2026-03-20 20:26:08 +00:00
Franck Nijhof
cbc573a6b1 Bump version to 2026.3.3 2026-03-20 19:56:30 +00:00
TimL
0c059cfc27 Properly handle buttons of SMLIGHT SLZB-MRxU devices (#166058) 2026-03-20 19:55:55 +00:00
tronikos
143ce9d7b3 Bump opower to 0.17.1 (#166044) 2026-03-20 19:55:17 +00:00
Michael
a6aa837d40 Fix enable/disable device tracking feature during setup of FRITZ!Box Tools (#166027) 2026-03-20 19:52:45 +00:00
Joost Lekkerkerker
c58b4a0066 Don't create fridge setpoint if no range in SmartThings (#166018) 2026-03-20 19:52:43 +00:00
Hai-Nam Nguyen
5155242ba7 Bump hyponcloud from 0.3.0 to 0.9.0 (#166005) 2026-03-20 19:52:42 +00:00
Hai-Nam Nguyen
085680f6bf Fix unit when plant power is above 1000W in Hypontech (#165959)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-20 19:52:41 +00:00
AlCalzone
98ecaaa6d2 Do not use moving states for Multilevel Switch CC v1-3 Z-Wave covers (#165909) 2026-03-20 19:52:39 +00:00
Erwin Douna
5ad199fe16 Proxmox fix restart/reboot action (#165901) 2026-03-20 19:52:38 +00:00
Stefan Agner
413cb98424 Fix Abort exception caught by wrong handler in backup encrypt/decrypt (#165852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:52:37 +00:00
Robert Svensson
b38c5bcaf2 Bump axis to v67 (#165840) 2026-03-20 19:52:35 +00:00
Joost Lekkerkerker
fa85dfb3b5 Bump pySmartThings to 3.7.2 (#165810) 2026-03-20 19:52:34 +00:00
Robert Resch
f0c6a035db Bump pyOpenSSL to 26.0.0 (#165770) 2026-03-20 19:52:33 +00:00
Ludovic BOUÉ
3f0c200e56 Fix Matter firmware update detection when version strings are identical (#165509) 2026-03-20 19:52:32 +00:00
Raj Laud
a2259ede28 Fix SmartLithium 8-cell support in victron_ble (#165496)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 19:52:30 +00:00
Willem-Jan van Rootselaar
24c2b6fe81 Fix optional static values in bsblan (#165488) 2026-03-20 19:52:29 +00:00
Alex Merkel
efc7350e6f LG Soundbar: Fix incorrect state and outdated track information (#165148) 2026-03-20 19:52:28 +00:00
Khole
5f525fc2a1 Hive: Fix bug in config flow for authentication and device registration (#165061) 2026-03-20 19:52:26 +00:00
Tucker Kern
f619a3e7af Snapcast: Fix incorrect identifier extraction in async_join_players (#165020) 2026-03-20 19:52:25 +00:00
Paul Tarjan
4e43492342 Skip unmapped and watchdog event types in Hikvision NVR event injection (#165009)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:52:24 +00:00
Erwin Douna
39e70071d3 Start orphaned entries in normal mode only (#164815)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-20 19:52:22 +00:00
Tom
6da0936a66 Improve ProxmoxVE permissions validation (#164770) 2026-03-20 19:52:21 +00:00
Martin Ecker
5257702530 Add correct speed fan mapping for Z-Wave GE/Jasco Enbrighten ZWA4013 (#164500) 2026-03-20 19:52:20 +00:00
Daniel Hjelseth Høyer
93da5be052 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-20 19:52:18 +00:00
Tom
e9576452b2 Improve ProxmoxVE permissions validation (#164770) 2026-03-20 20:41:31 +01:00
Alex Merkel
c8c6815efd LG Soundbar: Fix incorrect state and outdated track information (#165148) 2026-03-20 20:40:12 +01:00
Joost Lekkerkerker
60ef69c21d Don't create fridge setpoint if no range in SmartThings (#166018) 2026-03-20 20:38:38 +01:00
Allen Porter
d5b7792208 Add Roborock Q10 vacuum support (#165624)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-20 18:03:16 +01:00
Michael
fdfc2f4845 Fix FRITZ!Box Tools "the test opens sockets" issue (#165596) 2026-03-20 17:43:42 +01:00
Michael
184d834a91 Fix enable/disable device tracking feature during setup of FRITZ!Box Tools (#166027) 2026-03-20 17:29:33 +01:00
mettolen
0c98bf2676 Implement stale devices and update Liebherr to gold (#164666) 2026-03-20 16:31:09 +01:00
mettolen
229e1ee26b Pump pyliebherrhomeapi to 0.4.0 (#165973) 2026-03-20 16:02:49 +01:00
TimL
fdd2db6f23 Bump Pysmlight 0.3.1 (#166060) 2026-03-20 15:54:03 +01:00
TimL
2886863000 Properly handle buttons of SMLIGHT SLZB-MRxU devices (#166058) 2026-03-20 15:44:59 +01:00
Renat Sibgatulin
bf4170938c Add diagnostics platform to air-Q integration (#166065)
Co-authored-by: Claw <claw@theeggeadventure.com>
2026-03-20 15:25:27 +01:00
Mike O'Driscoll
6b84815c57 Add Casper Glow integration (#164536)
Signed-off-by: Mike O'Driscoll <mike@unusedbytes.ca>
2026-03-20 15:21:07 +01:00
aryanhasgithub
01b873f3bc Add Lichess Integration (#166051) 2026-03-20 12:35:51 +01:00
mettolen
66b1728c13 Implement reauth for Huum integration (#165971) 2026-03-20 10:03:23 +01:00
Erik Montnemery
d11668b868 Remove useless string split from mqtt diagnostics (#166035) 2026-03-20 09:51:59 +01:00
tronikos
ed3f70bc3f Bump androidtvremote2 to 0.3.1 (#166045) 2026-03-20 08:15:04 +01:00
tronikos
008eb39c3b Bump opower to 0.17.1 (#166044) 2026-03-20 08:14:22 +01:00
Erik Montnemery
a085d91a0d Remove useless string split from triggers (#166034) 2026-03-20 07:56:55 +01:00
Logan Rosen
6395a0abd0 Reject entity/number price for external statistics in energy config (#165582)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 08:34:40 +02:00
Erwin Douna
0de2e689f1 Add pause/resume buttons to Portainer (#166028) 2026-03-19 22:35:53 +01:00
Hai-Nam Nguyen
21d06fdace Fix unit when plant power is above 1000W in Hypontech (#165959)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-19 21:52:59 +01:00
AlCalzone
c8cf13ba19 Do not use moving states for Multilevel Switch CC v1-3 Z-Wave covers (#165909) 2026-03-19 21:30:26 +01:00
johanzander
d9a29bd486 growatt_server: add translation keys to all raised exceptions (#165927)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-03-19 21:08:15 +01:00
Norbert Rittel
bd0145cb8d Fix spelling of "Wi-Fi" trademark in user-facing string of sfr_box (#166019) 2026-03-19 20:43:16 +01:00
wollew
d002b48335 Replace deprecated library call in Velux integration (#165996)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 19:29:35 +01:00
Norbert Rittel
c66daf13d3 Fix spelling of "Wi-Fi" in user-facing strings of shelly (#166017) 2026-03-19 19:17:23 +01:00
Christian Lackas
1cae0e3cd3 Bump homematicip to 2.7.0 (#166012) 2026-03-19 17:53:12 +00:00
Paul Tarjan
de93d1d52a Skip unmapped and watchdog event types in Hikvision NVR event injection (#165009)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 18:39:28 +01:00
Tucker Kern
c67438c515 Snapcast: Fix incorrect identifier extraction in async_join_players (#165020) 2026-03-19 18:36:42 +01:00
Linkplay2020
fa57f72f37 Add WiiM media player integration (#148948)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-19 18:33:53 +01:00
Tom Matheussen
29309d1315 Add reconfigure flow to Satel Integra (#164938)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-19 18:31:46 +01:00
Robin Lintermann
130e0db742 Change codeowner of smarla integration (#166015) 2026-03-19 18:30:24 +01:00
Willem-Jan van Rootselaar
450d46f652 Fix optional static values in bsblan (#165488) 2026-03-19 17:07:49 +00:00
DeerMaximum
625603839c Remove DeerMaximum from velux codeowners (#166014) 2026-03-19 17:39:55 +01:00
Michael Hansen
fb66d766a8 Ensure STT metadata enums are passed (#165220) 2026-03-19 17:38:43 +01:00
Paul Bottein
e5f13b4126 Add state_attr_translated template filter and function (#165317)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-19 17:21:43 +01:00
Raj Laud
4a22f2c93e Add reauth flow and auto-trigger to victron_ble integration (#165729)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 17:01:04 +01:00
Mike Degatano
a5c48b190a Remove get_issues_info from hassio __all__ (#165929) 2026-03-19 16:58:20 +01:00
epenet
5e1a0e2152 Use annotationlib.get_annotations in entity helper (#165331) 2026-03-19 15:27:42 +01:00
Hai-Nam Nguyen
9a5516bb1d Bump hyponcloud from 0.3.0 to 0.9.0 (#166005) 2026-03-19 15:25:44 +01:00
J. Diego Rodríguez Royo
b9172cf4a8 Add 3D heating, air fry, and grill programs to Home Connect (#166006) 2026-03-19 15:21:20 +01:00
Ariel Ebersberger
8e4dc29226 Fix backblaze_b2 tests for Python 3.14.3 (#165930) 2026-03-19 14:01:27 +01:00
epenet
b152f2f9a6 Add test fixture for Tuya WiFi smart online 8 in 1 tester (#166003) 2026-03-19 13:27:42 +01:00
epenet
abca80dc13 Simplify mocking of Tuya device notifications (#165998) 2026-03-19 13:24:10 +01:00
Ville Skyttä
6869369ab2 Add some EZVIZ sensor icons (#166000) 2026-03-19 13:23:33 +01:00
Brett Adams
c2dde06713 Fix mixed-language Splunk setup errors in exception translations (#165974) 2026-03-19 13:21:45 +01:00
Retha Runolfsson
e455c05721 Added exception handling when switchbot account login. (#165978) 2026-03-19 13:15:45 +01:00
Ariel Ebersberger
085df1de19 Fix Home Asssitant Cloud test for Python 3.14.3 (#165937) 2026-03-19 12:11:26 +00:00
J. Diego Rodríguez Royo
91a1237965 Bump aiohomeconnect to 0.33.0 (#166001) 2026-03-19 13:07:57 +01:00
Raj Laud
680a6bc856 Add sensor tests for missing victron_ble device types (#165498)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 12:06:14 +01:00
dependabot[bot]
152912c258 Bump actions/download-artifact from 8.0.0 to 8.0.1 (#165982)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 11:20:30 +01:00
wollew
40e8a1b11a Bump pyvlx to 0.2.32 (#165990) 2026-03-19 11:14:57 +01:00
johanzander
69dc354669 growatt_server: add diagnostics support (#165923)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 11:09:25 +01:00
Christopher Fenner
bbe1bf14ae Bump PyViCare to 2.58.1 (#165965) 2026-03-19 10:19:48 +01:00
Joost Lekkerkerker
5470d8f8a7 Add switch for microfilter bypass mode to SmartThings (#165919) 2026-03-19 09:29:48 +01:00
Joost Lekkerkerker
99fe4b10d0 Add sensors for microfilter to SmartThings (#165922) 2026-03-19 08:57:51 +01:00
Brett Adams
886b6b08ac Source Tessie phantom drain and battery sensors from state data (#165970) 2026-03-19 08:24:32 +01:00
Robert Svensson
6a1e7c1cca Switch over to aiohttp on the Axis integration (#165963) 2026-03-19 08:23:06 +01:00
Josef Zweck
d17df13055 Manually update values instead of sending an event in mold_indicator (#165891) 2026-03-19 08:17:07 +01:00
J. Nick Koston
f73502c77a Bump ulid-transform to 2.2.0 (#165964) 2026-03-18 23:15:26 +01:00
Dan Raper
2c37a86bc9 Bump ohme to 1.7.1 (#165951) 2026-03-18 21:47:48 +00:00
tronikos
fa8e976de7 Add exception translations to Google Weather (#165935)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:25:58 -07:00
Andres Ruiz
877bca28ad Stop manually assigning an entity_id in waterfurnace sensors (#165954) 2026-03-18 20:58:36 +01:00
tronikos
a57c65f512 Add reconfigure flow in Google Drive (#165926) 2026-03-18 12:46:43 -07:00
tronikos
7140826dbb Do not abbreviate "reauthentication" in Google Drive (#165941) 2026-03-18 20:38:49 +01:00
Bouwe Westerdijk
5fea8d69d7 Add live firmware update detection to Plugwise (#165936)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 20:37:57 +01:00
Paul Tarjan
98e3b9962e Log Withings webhook URL warning only once (#164551)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 20:21:38 +01:00
Kurt Chrisford
afe19147f8 Test coverage for the Actron Air integration (#164446) 2026-03-18 20:20:51 +01:00
Willem-Jan van Rootselaar
0e7c25488c Add reconfigure flow to BSB-LAN (#164070) 2026-03-18 20:19:50 +01:00
Jan Čermák
412e85203d Add issue and repair for NTP sync failure (#165463)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-18 20:16:46 +01:00
Abílio Costa
55ec4a95fd Update renault snapshots (#165948) 2026-03-18 19:59:39 +01:00
Artur Pragacz
6ea9e9a161 Remove targets from intent response (#165434)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-18 18:35:30 +00:00
tronikos
b56e6d1ff7 Update Google Drive quality scale rules to match #156167 (#165916) 2026-03-18 19:34:55 +01:00
Eduardo Tsen
b502cdd15b Add buttons for controlling dishwasher operation (#160269)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-18 19:32:58 +01:00
Mike Ryan
b7ba85192d Add Trigger Motion Activity button to fully kiosk browser (#164499)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-18 14:24:51 -04:00
Erik Montnemery
04d45c8ada Add schedule conditions (#165913)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-18 18:55:47 +01:00
tronikos
ba0804fefa Add exception translations to Google Drive (#165932) 2026-03-18 18:51:07 +01:00
Erik Montnemery
538b817bf1 Adjust inheritance tree of EntitySelectorConfig (#165915) 2026-03-18 18:32:44 +01:00
Brandon Rothweiler
7efa2d3cac Add Dropbox backup integration (#155644) 2026-03-18 17:58:57 +01:00
Erik Montnemery
3f872fd196 Allow specifying attribute in state selector (#165928) 2026-03-18 17:54:36 +01:00
Erik Montnemery
b00f6593f1 Add unit of measurement to entity selector filter (#165914) 2026-03-18 17:01:21 +01:00
Raj Laud
a63516ff71 Allow retry on invalid encryption key in victron_ble config flow (#165600)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 15:53:03 +01:00
Joost Lekkerkerker
55b082edb6 Add binary sensor for smartthings microfilter blockage (#165917) 2026-03-18 15:44:43 +01:00
Robert Resch
b0c3ede4fd Improve type hints for startca (#165720) 2026-03-18 15:43:50 +01:00
johanzander
84bd1cd336 growatt_server: use icon-translations instead of hardcoded _attr_icon (#165920)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:41:48 +01:00
Erik Montnemery
25bbfcc595 Add gate conditions (#165898) 2026-03-18 15:27:27 +01:00
johanzander
bf05925c8b growatt_server: replace custom precision with suggested_display_precision (#165858)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:20:30 +01:00
Jan Čermák
488d9ad75c Use new home-assistant/builder actions for image builds (#164756) 2026-03-18 14:44:53 +01:00
Vincent Le Ligeour
2dfad3d755 Add battery charge limit controls to Renault (#163079)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-18 14:29:36 +01:00
Stefan Agner
7e759bf730 Fix Abort exception caught by wrong handler in backup encrypt/decrypt (#165852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:28:56 +01:00
Robert Svensson
9678049e72 Bump axis to v67 (#165840) 2026-03-18 14:25:54 +01:00
David Bonnes
8602ba2679 Extend Evohome tests to cover legacy service calls (#164316) 2026-03-18 13:57:31 +01:00
Paulus Schoutsen
78c3503b7d Remove unnecessary volume_up/volume_down overrides from ws66i media player (#164433)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:56:22 +01:00
Paulus Schoutsen
fbb3b81991 Remove unnecessary volume_up/volume_down overrides from songpal media player (#164432)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:56:04 +01:00
Paulus Schoutsen
26eaf510ee Remove unnecessary volume_up/volume_down overrides from clementine media player (#164427)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 13:55:50 +01:00
Erik Montnemery
5c83d16995 Add select triggers (#165378)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:55:02 +01:00
Joost Lekkerkerker
388b258d6c Add Zinvolt problem binary sensors (#164091) 2026-03-18 13:54:42 +01:00
Jeef
2c9a5c10da Add data-description strings to IntelliFire (#165910)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:49:04 +01:00
Erik Montnemery
5a68bafd69 Add garage_door conditions (#165897) 2026-03-18 13:29:13 +01:00
Erik Montnemery
33fce89a2b Add window conditions (#165899) 2026-03-18 13:16:11 +01:00
Erwin Douna
1932f61da3 Proxmox fix restart/reboot action (#165901) 2026-03-18 11:55:51 +01:00
Mike Degatano
5a231b27b9 Add repair for deprecated arch addon issue (#165511) 2026-03-18 11:53:09 +01:00
Steve Easley
5617e8c7bc Move jvc_projector sensor entities to select domain (#165194) 2026-03-18 11:34:03 +01:00
Emil Burzo
2b5b0e9d0f Add battery temperature sensor to Fully Kiosk Browser integration (#165714) 2026-03-18 11:22:25 +01:00
Josef Zweck
732f553b48 Safely consume events in hassio test (#165892) 2026-03-18 10:43:20 +01:00
Erik Montnemery
0a53b227ed Add door conditions (#165885) 2026-03-18 10:19:06 +01:00
Erik Montnemery
44b73ab7bd Add occupancy conditions (#165678) 2026-03-18 09:43:17 +01:00
Erik Montnemery
538061d512 Add motion conditions (#165677) 2026-03-18 09:23:32 +01:00
Raj Laud
e307ceccb5 Bump victron-ble-ha-parser to 0.6.2 (#165832)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 08:47:34 +01:00
Erik Montnemery
ea7558c0ad Improve naming in condition and trigger test helpers (#165847)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 08:44:14 +01:00
johanzander
c4399b5547 growatt_server: add serial_number to DeviceInfo (devices quality scale rule) (#165857)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:42:24 +01:00
Erwin Douna
d989a83d7b Remove NotImplementedError in Volvo integration (#165856) 2026-03-18 08:41:35 +01:00
mettolen
d04f3530df Remove the icon property from Huum climate entity (#165870) 2026-03-18 08:28:02 +01:00
mettolen
647d957ffe Removed redundant logging from Huum integration (#165868) 2026-03-18 08:27:13 +01:00
johanzander
a3f3c87b39 growatt_server: add EntityCategory.DIAGNOSTIC to diagnostic sensors (#165880)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:24:49 +01:00
Nathan Spencer
447b17a2a4 Bump pyweatherflowudp to 1.5.2 (#165874) 2026-03-18 08:24:24 +01:00
Joost Lekkerkerker
eb2b92687c Add camera fixture to SmartThings (#165809) 2026-03-18 08:23:44 +01:00
Jack Boswell
6424e3658e Remove myself from Starlink codeowners (#165883) 2026-03-18 08:22:18 +01:00
Erik Montnemery
d1d8754853 Fix type annotations for set_or_remove_state test helper (#165843) 2026-03-18 08:03:45 +01:00
Erik Montnemery
c4ff7fa676 Fix bug in assert_condition_behavior_any test helper (#165838) 2026-03-18 08:03:18 +01:00
balloob-travel
f1fe1d3956 Update config flow testing instructions for AI (#165873)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2026-03-18 06:39:15 +01:00
Christopher Fenner
fd0d60b787 Fix return type in ViCare integration (#165861) 2026-03-18 03:12:06 +01:00
Stefan Agner
9ddefaaacd Bump aiohasupervisor to 0.4.2 (#165854) 2026-03-17 23:08:57 +01:00
Ludovic BOUÉ
5c8df048b1 Fix timezone in account creation date in test snapshot (#165831) 2026-03-17 22:53:36 +01:00
Raj Laud
d86d85ec56 Fix victron_ble charger error sensor always showing unknown (#165713)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-17 21:45:51 +00:00
tronikos
660f12b683 Implement dynamic-devices and stale-devices in Opower to mark it platinum (#165121)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 22:43:57 +01:00
Jan Bouwhuis
b8238c86e6 Cleanup unused vacuum test helpers (#165851) 2026-03-17 22:36:24 +01:00
Raman Gupta
754828188e Refactor Vizio integration to use DataUpdateCoordinator (#162188)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:20:01 -04:00
Erik Montnemery
6992a3c72b Adjust name and docstring of some trigger tests (#165846) 2026-03-17 22:11:32 +01:00
Joost Lekkerkerker
738d4f662a Bump pySmartThings to 3.7.2 (#165810) 2026-03-17 21:57:20 +01:00
Carlos Sánchez López
7f33ac72ab Add alarm control panel support for Tuya WG2 alarm panel (Duosmart C30) (#165837) 2026-03-17 21:44:57 +01:00
Carlos Sánchez López
0891d814fa Add sensor support for Tuya WG2 alarm panel (Duosmart C30) (#165834) 2026-03-17 21:42:20 +01:00
Carlos Sánchez López
ddab50edcc Add binary sensor support for Tuya WG2 alarm panel (Duosmart C30) (#165833) 2026-03-17 21:41:57 +01:00
Erik Montnemery
c8ce4eb32d Deduplicate tests testing conditions in mode all (#165841) 2026-03-17 21:06:26 +01:00
Jan Bouwhuis
22aca8b7af Add clean segment support to MQTT vacuum entities (#164983)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-17 20:27:42 +01:00
Erik Montnemery
770864082f Deduplicate tests testing conditions in mode any (#165801) 2026-03-17 19:23:47 +00:00
Abílio Costa
14545660e2 Make TODO subscriptions use TodoItem instead of JSON (#165802) 2026-03-17 19:09:13 +00:00
Jamie Magee
836353015b Detect new garage doors automatically in aladdin_connect (#165004) 2026-03-17 20:04:31 +01:00
Allen Porter
c57ffd4d78 Update python-roborock dependency to 4.25.0. (#165800)
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
2026-03-17 19:58:18 +01:00
prana-dev-official
cbebfdf149 Add number platform for Prana integration (#165816) 2026-03-17 19:53:50 +01:00
Ludovic BOUÉ
d8ed9ca66f Fix timestamps in chess_com test diagnostics (#165829) 2026-03-17 19:30:08 +01:00
Cody
5caf8a5b83 Make Season integration timezone aware (#164876) 2026-03-17 18:09:25 +01:00
Aidan Timson
c05210683e Demo valve registry entry and device (#165803)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 17:37:21 +01:00
Joost Lekkerkerker
aa8dd4bb66 Add microfiber filter fixture to SmartThings (#165808) 2026-03-17 17:21:51 +01:00
Joost Lekkerkerker
ee7d6157d9 Fix Indevolt button snapshot (#165812) 2026-03-17 17:19:03 +01:00
Manu
adec1d128c Add exception handling to media source in Radio Browser integration (#164653) 2026-03-17 17:13:11 +01:00
Franck Nijhof
2c47e83342 2026.3.2 (#165675) 2026-03-16 13:23:27 +01:00
Franck Nijhof
e3c6a2184d Bump version to 2026.3.2 2026-03-16 10:27:01 +00:00
Simone Chemelli
0ba0829350 Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:25:08 +00:00
Allen Porter
678048e681 Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 10:25:07 +00:00
Jan Bouwhuis
743eeeae53 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:25:05 +00:00
Raj Laud
46555c6d9a Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 10:25:04 +00:00
Simone Chemelli
dbaca0a723 Bump aioamazondevices to 13.0.1 (#165476) 2026-03-16 10:25:02 +00:00
Joost Lekkerkerker
9bb2959029 Bump pySmartThings to 3.7.0 (#165468) 2026-03-16 10:25:01 +00:00
Robert Resch
0304781fa9 Bump orjson to 3.11.7 (#165443) 2026-03-16 10:25:00 +00:00
J. Nick Koston
e081d28aa4 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-16 10:24:58 +00:00
TheJulianJES
34aa28c72f Bump ZHA to 1.0.2 (#165423) 2026-03-16 10:24:56 +00:00
Bram Kragten
cfa2946db8 Update frontend to 20260312.0 (#165420) 2026-03-16 10:24:55 +00:00
Galorhallen
1b0779347c Update govee local api to 2.4.0 (#165418) 2026-03-16 10:24:54 +00:00
Joost Lekkerkerker
93a281e7af Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-16 10:24:53 +00:00
Josef Zweck
6b32e27fd3 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-16 10:24:51 +00:00
Zach Feldman
79928a8c7c August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-16 10:24:50 +00:00
Steve Easley
9146518e13 Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-16 10:24:48 +00:00
Dan Raper
e9c5172f43 Bump ohme to 1.7.0 (#165318) 2026-03-16 10:24:47 +00:00
TheJulianJES
cce21ad4b9 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-16 10:24:46 +00:00
Simone Chemelli
10ec02ca3c Fix switch set for Vodafone Station (#165273) 2026-03-16 10:18:26 +00:00
Josef Zweck
bdf54491e5 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-16 10:18:25 +00:00
Bram Kragten
0b05d34238 Add reorder support to area selector (#165211) 2026-03-16 10:18:24 +00:00
Åke Strandberg
4c69a1c5f7 Add missing code for Miele dryer (#165122) 2026-03-16 10:17:00 +00:00
Steve Easley
6f1f56dcaa Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-16 10:16:59 +00:00
Jordan Harvey
d0b9991232 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-16 10:16:58 +00:00
Artur Pragacz
aacf39be8a Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-16 10:16:56 +00:00
Erwin Douna
bf055da82c Bump pyportainer to 1.0.33 (#165080) 2026-03-16 10:12:26 +00:00
Erwin Douna
0fb118bcd9 Bump pyportainer 1.0.32 (#164803) 2026-03-16 10:12:25 +00:00
Erwin Douna
954ef7d1f5 Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-16 09:56:32 +00:00
Joakim Plate
b091299320 Update pychromecast to 14.0.10 (#165069) 2026-03-16 09:56:31 +00:00
J. Nick Koston
52483e18b2 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-16 09:56:29 +00:00
AlCalzone
57e8683ed7 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-16 09:56:28 +00:00
Simone Chemelli
67faace978 Fix dnd switch status for Alexa Devices (#164953) 2026-03-16 09:56:26 +00:00
Simone Chemelli
e4be64fcb1 Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 09:56:25 +00:00
Franck Nijhof
f552b8221f 2026.3.1 (#165001) 2026-03-06 22:10:34 +01:00
Franck Nijhof
55dc5392f9 Bump version to 2026.3.1 2026-03-06 20:37:19 +00:00
Karl Beecken
5b93aeae38 Bump teltasync to 0.2.0 (#164995) 2026-03-06 20:37:03 +00:00
Shay Levy
33610bb1a1 Bump aioswitcher to 6.1.1 (#164981) 2026-03-06 20:37:01 +00:00
Manu
6c3cebe413 Change setpoint step size in IronOS integration (#164979) 2026-03-06 20:37:00 +00:00
Willem-Jan van Rootselaar
5346895d9b Bump python-bsblan to 5.1.2 (#164963) 2026-03-06 20:36:58 +00:00
Willem-Jan van Rootselaar
05c3f08c6c Bump python-bsblan to 5.1.1 (#164591) 2026-03-06 20:36:57 +00:00
Daniel Hjelseth Høyer
1ce025733d Fix energy unit in Homevolt (#164959)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-06 20:35:22 +00:00
Simone Chemelli
1537ea86b8 Bump aiovodafone to 3.1.3 (#164955) 2026-03-06 20:35:21 +00:00
Luke Lashley
ec137870fa Pass in Base Url during Roborock reauth (#164903) 2026-03-06 20:35:20 +00:00
Josef Zweck
816ee7f53e Bump onedrive-personal-sdk to 0.1.5 (#164880) 2026-03-06 20:35:18 +00:00
Petro31
6e7eeec827 Fix 'this' variable in template options flow (#164866) 2026-03-06 20:35:17 +00:00
Marc Mueller
d100477a22 Fix volvo test RuntimeWarning (#164845) 2026-03-06 20:35:16 +00:00
Matthias Alphart
98ac6dd2c1 Fix KNX sensor default attributes for energy and volume DPTs (#164838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:35:14 +00:00
John O'Nolan
6b30969f60 Fix Ghost config flow using wrong field name for site UUID (#164836) 2026-03-06 20:35:13 +00:00
Joshua Leaper
e9a6b5d662 Update ness_alarm scan interval to 5 secs (#164835) 2026-03-06 20:35:11 +00:00
Glenn de Haan
f95f3f9982 Add device class to active_liter_lpm sensor (#164809) 2026-03-06 20:35:10 +00:00
epenet
3f884a8cd1 Remove caio from licenses exception list (#164806) 2026-03-06 20:35:09 +00:00
Raphael Hehl
10f284932e Enforce SSRF redirect protection only for connector allowed_protocol_schema_set (#164769)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 20:35:07 +00:00
Sean O'Keeffe
e1c4e6dc42 more programs for Miele steam ovens (#164768)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-06 20:35:06 +00:00
Ian Foster
0976e7de4e Update keyboard_remote dependencies (#164755) 2026-03-06 20:35:05 +00:00
Antonio Mello
ae1012b2f0 Fix IntesisHome outdoor_temp not reported when value is 0.0 (#164703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:35:03 +00:00
TimL
bb7c4faca5 Fix button entity creation for devices with more than two radios (#164699) 2026-03-06 20:35:02 +00:00
Tucker Kern
0b1be61336 Ensure Snapcast client has a valid current group before accessing group attributes. (#164683) 2026-03-06 20:35:00 +00:00
Glenn Waters
3ec44024a2 Hunter Douglas Powerview: Fix missing class in hierarchy. (#164264)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:34:59 +00:00
Joost Lekkerkerker
1200cc5779 Bump spotifyaio to 2.0.2 (#164114)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-06 20:34:58 +00:00
Blake Messer
d632931f74 Fix Rain Bird controllers updated by Rain Bird 2.x (#163915) 2026-03-06 20:34:56 +00:00
Franck Nijhof
2f9faa53a1 2026.3.0 (#164757) 2026-03-04 20:17:05 +01:00
Joost Lekkerkerker
718607a758 Revert "Add diagnostics platform to AWS S3 (#164118)" (#164759) 2026-03-04 19:01:47 +01:00
Franck Nijhof
3789156559 Revert "Add diagnostics platform to AWS S3 (#164118)"
This reverts commit 37d2c946e8.
2026-03-04 17:53:29 +00:00
Franck Nijhof
042ce6f2de Bump version to 2026.3.0 2026-03-04 17:30:58 +00:00
Franck Nijhof
0a5908002f Bump version to 2026.3.0b4 2026-03-04 17:09:32 +00:00
Petro31
3a5f71e10a Fix this variable preview issue with template entities from the UI (#164740) 2026-03-04 17:09:18 +00:00
rappenze
04e4b05ab0 Fix handling of several thermostat QuickApp's in fibaro (#164344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 17:09:17 +00:00
Franck Nijhof
c2c5232899 Bump version to 2026.3.0b3 2026-03-04 14:30:26 +00:00
Stefan Agner
593610094e Ignore transient empty segments in Matter vacuum (#164737)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:25:12 +00:00
Bram Kragten
47cb7870ea Update frontend to 20260304.0 (#164736) 2026-03-04 14:25:11 +00:00
Joakim Plate
045b626e24 Restore handling of is active input for chromecast (#164735) 2026-03-04 14:25:09 +00:00
Artur Pragacz
bea5468dee Add backup integration to recovery mode (#164734) 2026-03-04 14:25:08 +00:00
Erwin Douna
04fc12cc26 Bump pyportainer 1.0.31 (#164733) 2026-03-04 14:25:07 +00:00
starkillerOG
fec33ad42b Bump reolink-aio to 0.19.1 (#164732) 2026-03-04 14:25:06 +00:00
TheJulianJES
07e323f1e9 Bump ZHA to 1.0.1 (#164709) 2026-03-04 14:25:04 +00:00
Ariel Ebersberger
ebe2612713 Influxdb repair issue follow up (#164684) 2026-03-04 14:25:03 +00:00
Michael Hansen
88ca668562 Bump intents to 2026.3.3 (#164676) 2026-03-04 14:25:01 +00:00
Robert Resch
1d46ac0b64 Fix wheels building by using arch dependent requirements_all file (#164675) 2026-03-04 14:25:00 +00:00
starkillerOG
13a5e6e85f Fix Reolink entity unique_id migration when unique_id already exists (#164667) 2026-03-04 14:24:58 +00:00
TimL
d2665f03ff Bump pysmlight to v0.2.16 (#164665)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-04 14:24:56 +00:00
hanwg
80412e4973 Update subentry description for Telegram bot (#164642)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:24:55 +00:00
Matthias Alphart
818d9f774e Update knx-frontend to 2026.3.2.183756 (#164623) 2026-03-04 14:24:54 +00:00
starkillerOG
012e78d625 Fix key error in Reolink DHCP if still setting up (#164619) 2026-03-04 14:24:53 +00:00
Simone Chemelli
74abedbcd2 Bump aioamazondevices to 13.0.0 (#164618) 2026-03-04 14:24:51 +00:00
Tom
e16fb6b5a5 Add informative errors to Proxmox VE buttons (#164417) 2026-03-04 14:24:50 +00:00
Artur Pragacz
8906e5dcb5 Trigger recovery mode on registry major version downgrade (#164340) 2026-03-04 14:24:49 +00:00
Abílio Costa
10067c208a Add Ubisys virtual integration (#164314) 2026-03-04 14:24:48 +00:00
Ariel Ebersberger
d4143205e9 Add repair issue after importing influxdb yaml config (#164145)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:24:46 +00:00
Miguel Angel Nubla
a4da363ff2 Fix infinite loop in esphome assist_satellite (#163097)
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2026-03-04 14:24:45 +00:00
Christian Lackas
bc9ae3dad6 Fix HomematicIP heating group availability with unreachable members (#162571) 2026-03-04 14:24:44 +00:00
J. Diego Rodríguez Royo
9e5daaa784 Improve mobile_app notify.notify with not connected targets (#161855) 2026-03-04 14:24:42 +00:00
Daniel Schneider
ff0a6757cd Bump ring-doorbell to 0.9.14 (#158074)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-04 14:24:41 +00:00
Bram Kragten
62ffeeccb0 Bump version to 2026.3.0b2 2026-03-02 19:32:14 +01:00
Bram Kragten
1afe00670e Update frontend to 20260302.0 (#164612) 2026-03-02 19:32:00 +01:00
Artur Pragacz
500ffe8153 Raise on vacuum area mapping not configured (#164595) 2026-03-02 19:31:59 +01:00
Jan-Philipp Benecke
2cebb28a1b Bump aiotankerkoenig to 0.5.1 (#164590) 2026-03-02 19:31:58 +01:00
Robert Resch
80bfba0981 Bump aiogithubapi to 26.0.0 (#164579) 2026-03-02 19:31:57 +01:00
Norbert Rittel
882e499375 Change one remaining string from "Overseerr" to "Seerr" (#164569) 2026-03-02 19:31:56 +01:00
Jan-Philipp Benecke
e89aafc8e2 Fix large WebDAV backup metadata download (#164563)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 19:31:56 +01:00
Jan-Philipp Benecke
66ae5ab543 Bump aiowebdav2 to 0.6.1 (#164560) 2026-03-02 19:31:54 +01:00
J. Nick Koston
75d39c0b02 Bump yalexs-ble to 3.2.7 (#164555) 2026-03-02 19:31:53 +01:00
Simone Chemelli
989133cb16 Bump aioamazondevices to 12.0.2 (#164518) 2026-03-02 19:31:52 +01:00
Allen Porter
f559f8e014 Update nest access token error handling to use specific OAuth2 token request exceptions (#164506) 2026-03-02 19:31:51 +01:00
willemstuursma
a95207f2ef Bump DSMR parser to 1.5.0 (#164484) 2026-03-02 19:31:50 +01:00
Tom Matheussen
2c28a93ea0 Require user code to be set when toggling Satel Integra switches (#164483) 2026-03-02 19:31:48 +01:00
Klaas Schoute
3ff97a0820 Update error handling messages for Powerfox Local integration (#164465) 2026-03-02 19:31:47 +01:00
Barry vd. Heuvel
f7a56447ae Bump weheat to 2026.2.28 (#164456) 2026-03-02 19:31:45 +01:00
Khole
dfd086f253 Hive - Bump pyhive-integration to v1.0.8 (#164453) 2026-03-02 19:31:44 +01:00
mettolen
b6a166ce48 Remove error translation placeholders from Airobot (#164436) 2026-03-02 19:31:43 +01:00
Stefan Agner
e93b724ce4 Fix Matter vacuum crash on nullable ServiceArea location info (#164411) 2026-03-02 19:31:42 +01:00
Franck Nijhof
d0b25ccc01 Reject relative paths in SFTP storage backup location config flow (#164408) 2026-03-02 19:31:41 +01:00
Joost Lekkerkerker
0a3ef64f28 Bump pySmartThings to 3.6.0 (#164397) 2026-03-02 19:31:40 +01:00
Joost Lekkerkerker
e9ce3ffff9 Fix SmartThings EHS power (#164395) 2026-03-02 19:31:39 +01:00
Joost Lekkerkerker
55415b1559 Add state for washing mop in SmartThings (#164348) 2026-03-02 19:31:37 +01:00
Paulus Schoutsen
0160dbf3a6 Add missing volume supported features to dunehd (#164343)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:31:36 +01:00
Franck Nijhof
7dd83b1e8f Mock firmware data during reauth flow init in airos tests (#164341) 2026-03-02 19:31:35 +01:00
Petro31
e502f5f249 Fix int vs float template sensor issue (#164339) 2026-03-02 19:31:34 +01:00
Johnny Willemsen
6e93ebc912 Update state labels to use common keys in indevolt (#164308) 2026-03-02 19:31:33 +01:00
Erwin Douna
9a4fdf7f80 Proxmox expand data descriptions (#164304) 2026-03-02 19:31:32 +01:00
TheJulianJES
76d69a5f53 Fix ZHA update entities not working after reload (#164290) 2026-03-02 19:31:30 +01:00
Raphael Hehl
ae40c0cf4b Bump uiprotect to version 10.2.2 (#164269)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-02 19:31:29 +01:00
Denis Shulyaka
078647d128 Create reauth flow for Anthropic for auth errors during conversation (#164267)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 19:31:28 +01:00
Artur Pragacz
8a637c4e5b Remove vacuum area mapping not configured issue (#164259) 2026-03-02 19:31:25 +01:00
Willem-Jan van Rootselaar
9e9daff26d Set entity_registry_enabled_default to False for total energy sensor (#164197) 2026-03-02 19:31:24 +01:00
James
41aeedaa82 Handle missing Daikin zone temperature keys (#164170)
Co-authored-by: barneyonline <barneyonline@users.noreply.github.com>
2026-03-02 19:31:23 +01:00
Kamil Breguła
a8297ae65d Add diagnostics platform to AWS S3 (#164118)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-03-02 19:31:22 +01:00
Joost Lekkerkerker
b7f1171c08 Rename Overseerr integration to Seerr (#164060) 2026-03-02 19:31:21 +01:00
Ye Zhiling
226f606cb9 Pass encoding to AtomicWriter in write_utf8_file_atomic (#164015) 2026-03-02 19:31:20 +01:00
HadiAyache
9472be39f2 Fix AccuWeather daily forecast crash when humidity average is missing (#163968)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 19:31:19 +01:00
nopoz
67a9e42b19 Google Cast: detect state and attributes when device is doing active non-media casting (#160819)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-02 19:31:17 +01:00
Simone Chemelli
ba1837859f Fix RpcSensorDescription for Shelly (#150719) 2026-03-02 19:31:16 +01:00
Franck Nijhof
4a301eceac Bump version to 2026.3.0b1 2026-02-26 19:32:15 +00:00
Bram Kragten
d138a99e62 Update frontend to 20260226.0 (#164262) 2026-02-26 19:31:52 +00:00
Johnny Willemsen
a431f84dc9 Update state labels to use common keys in compit (#164261) 2026-02-26 19:31:50 +00:00
epenet
aa9534600e Simplify portainer entity initialisation (#164256) 2026-02-26 19:31:49 +00:00
Denis Shulyaka
54fa49e754 Disable code interpreter with minimal reasoning for OpenAI (#164254) 2026-02-26 19:31:47 +00:00
Joost Lekkerkerker
459b6152f4 Remove invalid color mode from philips_js (#164204) 2026-02-26 19:31:46 +00:00
Denis Shulyaka
60c8d997ca Update reasoning options for gpt-5.3-codex (#164179) 2026-02-26 19:31:45 +00:00
AlCalzone
a598368895 Rename "Z-Wave Supervisor app" to "Z-Wave JS app" (#164147) 2026-02-26 19:31:43 +00:00
Erwin Douna
2ff1499c48 Fix stack devices merging with container devices in Portainer (#164135)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-26 19:31:42 +00:00
Norbert Rittel
348ddbe124 Replace "add-ons" with "apps" in backup issues (#164129) 2026-02-26 19:31:40 +00:00
Paulus Schoutsen
71ed43faf2 Simplify Anthropic integration name (#164124)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 19:31:39 +00:00
mettolen
dc69a90296 Remove error translation placeholders from Saunum (#164121) 2026-02-26 19:31:37 +00:00
Liquidmasl
f5db8e6ba4 Sonarr post merge changes (#164112) 2026-02-26 19:31:36 +00:00
Artur Pragacz
b82a26ef68 Fix Matter vacuum clean area status check (#164108) 2026-02-26 19:31:35 +00:00
Maciej Bieniek
0eaaeedf11 Bump accuweather to 5.1.0 (#164034) 2026-02-26 19:31:33 +00:00
Franck Nijhof
62e26e53ac Bump version to 2026.3.0b0 2026-02-25 19:36:43 +00:00
1304 changed files with 77533 additions and 11317 deletions

View File

@@ -620,12 +620,14 @@ rules:
### Config Flow Testing
- **100% Coverage Required**: All config flow paths must be tested
- **Patch Boundaries**: Only patch library or client methods when testing config flows. Do not patch methods defined in `config_flow.py`; exercise the flow logic end-to-end.
- **Test Scenarios**:
- All flow initiation methods (user, discovery, import)
- Successful configuration paths
- Error recovery scenarios
- Prevention of duplicate entries
- Flow completion after errors
- Reauthentication/reconfigure flows
### Testing
- **Integration-specific tests** (recommended):

1
.gitattributes vendored
View File

@@ -16,6 +16,7 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true

View File

@@ -1,6 +1,12 @@
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
# Copilot code review instructions
- Start review comments with a short, one-sentence summary of the suggested fix.
- Do not add comments about code style, formatting or linting issues.
# GitHub Copilot & Claude Code Instructions
This repository contains the core of Home Assistant, a Python 3 based home automation application.

View File

@@ -35,6 +35,7 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -100,7 +101,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
os: ubuntu-24.04
- arch: aarch64
os: ubuntu-24.04-arm
steps:
@@ -181,7 +182,7 @@ jobs:
fi
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
@@ -195,77 +196,20 @@ jobs:
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build variables
id: vars
shell: bash
env:
ARCH: ${{ matrix.arch }}
run: |
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
env:
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${BASE_IMAGE}"
- name: Verify cache image signature
id: cache
continue-on-error: true
env:
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${CACHE_IMAGE}"
- name: Build base image
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.init.outputs.version }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
image-tags: ${{ needs.init.outputs.version }}
push: true
version: ${{ needs.init.outputs.version }}
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -280,7 +224,6 @@ jobs:
matrix:
machine:
- generic-x86-64
- intel-nuc
- khadas-vim3
- odroid-c2
- odroid-c4
@@ -304,45 +247,44 @@ jobs:
- machine: qemux86-64
arch: amd64
runs-on: ubuntu-24.04
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
- machine: intel-nuc
arch: amd64
runs-on: ubuntu-24.04
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set build additional args
- name: Compute extra tags
id: tags
shell: bash
env:
VERSION: ${{ needs.init.outputs.version }}
run: |
# Create general tags
if [[ "${VERSION}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
elif [[ "${VERSION}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
- name: Build machine image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: machine/
cosign-base-identity: "https://github.com/home-assistant/core/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
file: machine/${{ matrix.machine }}
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
image-tags: |
${{ needs.init.outputs.version }}
${{ steps.tags.outputs.extra_tags }}
push: true
version: ${{ needs.init.outputs.version }}
publish_ha:
name: Publish version files
@@ -543,7 +485,7 @@ jobs:
python-version-file: ".python-version"
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations

View File

@@ -120,7 +120,7 @@ jobs:
run: |
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
- name: Filter for core changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: core
with:
filters: .core_files.yaml
@@ -135,7 +135,7 @@ jobs:
echo "Result:"
cat .integration_paths.yaml
- name: Filter for integration changes
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: integrations
with:
filters: .integration_paths.yaml
@@ -978,7 +978,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pytest_buckets
- name: Compile English translations
@@ -1387,7 +1387,7 @@ jobs:
with:
persist-credentials: false
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1558,7 +1558,7 @@ jobs:
with:
persist-credentials: false
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1587,7 +1587,7 @@ jobs:
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-results-*
- name: Upload test results to Codecov

View File

@@ -121,12 +121,12 @@ jobs:
persist-credentials: false
- name: Download env_file
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_diff
@@ -172,17 +172,17 @@ jobs:
persist-credentials: false
- name: Download env_file
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: env_file
- name: Download requirements_diff
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: requirements_all_wheels

View File

@@ -137,6 +137,7 @@ homeassistant.components.calendar.*
homeassistant.components.cambridge_audio.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.casper_glow.*
homeassistant.components.cert_expiry.*
homeassistant.components.clickatell.*
homeassistant.components.clicksend.*
@@ -173,6 +174,7 @@ homeassistant.components.dnsip.*
homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.*
homeassistant.components.downloader.*
homeassistant.components.dropbox.*
homeassistant.components.droplet.*
homeassistant.components.dsmr.*
homeassistant.components.duckdns.*
@@ -272,10 +274,12 @@ homeassistant.components.homekit_controller.storage
homeassistant.components.homekit_controller.utils
homeassistant.components.homewizard.*
homeassistant.components.homeworks.*
homeassistant.components.hr_energy_qube.*
homeassistant.components.http.*
homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.*
homeassistant.components.huum.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.hypontech.*
@@ -325,6 +329,7 @@ homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.letpot.*
homeassistant.components.lg_infrared.*
homeassistant.components.libre_hardware_monitor.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*

36
CODEOWNERS generated
View File

@@ -214,6 +214,8 @@ build.json @home-assistant/supervisor
/tests/components/balboa/ @garbled1 @natekspencer
/homeassistant/components/bang_olufsen/ @mj23000
/tests/components/bang_olufsen/ @mj23000
/homeassistant/components/battery/ @home-assistant/core
/tests/components/battery/ @home-assistant/core
/homeassistant/components/bayesian/ @HarvsG
/tests/components/bayesian/ @HarvsG
/homeassistant/components/beewi_smartclim/ @alemuro
@@ -273,6 +275,8 @@ build.json @home-assistant/supervisor
/tests/components/cambridge_audio/ @noahhusby
/homeassistant/components/camera/ @home-assistant/core
/tests/components/camera/ @home-assistant/core
/homeassistant/components/casper_glow/ @mikeodr
/tests/components/casper_glow/ @mikeodr
/homeassistant/components/cast/ @emontnemery
/tests/components/cast/ @emontnemery
/homeassistant/components/ccm15/ @ocalvo
@@ -397,6 +401,8 @@ build.json @home-assistant/supervisor
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dropbox/ @bdr99
/tests/components/dropbox/ @bdr99
/homeassistant/components/droplet/ @sarahseidman
/tests/components/droplet/ @sarahseidman
/homeassistant/components/dsmr/ @Robbie1221
@@ -733,6 +739,8 @@ build.json @home-assistant/supervisor
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/hr_energy_qube/ @MattieGit
/tests/components/hr_energy_qube/ @MattieGit
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/http/ @home-assistant/core
@@ -780,6 +788,8 @@ build.json @home-assistant/supervisor
/tests/components/igloohome/ @keithle888
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/illuminance/ @home-assistant/core
/tests/components/illuminance/ @home-assistant/core
/homeassistant/components/image/ @home-assistant/core
/tests/components/image/ @home-assistant/core
/homeassistant/components/image_processing/ @home-assistant/core
@@ -939,12 +949,16 @@ build.json @home-assistant/supervisor
/tests/components/lektrico/ @lektrico
/homeassistant/components/letpot/ @jpelgrom
/tests/components/letpot/ @jpelgrom
/homeassistant/components/lg_infrared/ @home-assistant/core
/tests/components/lg_infrared/ @home-assistant/core
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/libre_hardware_monitor/ @Sab44
/tests/components/libre_hardware_monitor/ @Sab44
/homeassistant/components/lichess/ @aryanhasgithub
/tests/components/lichess/ @aryanhasgithub
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/liebherr/ @mettolen
@@ -1065,6 +1079,8 @@ build.json @home-assistant/supervisor
/tests/components/modern_forms/ @wonderslug
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
/tests/components/moehlenhoff_alpha2/ @j-a-n
/homeassistant/components/moisture/ @home-assistant/core
/tests/components/moisture/ @home-assistant/core
/homeassistant/components/monarch_money/ @jeeftor
/tests/components/monarch_money/ @jeeftor
/homeassistant/components/monoprice/ @etsinko @OnFreund
@@ -1303,6 +1319,8 @@ build.json @home-assistant/supervisor
/tests/components/poolsense/ @haemishkyd
/homeassistant/components/portainer/ @erwindouna
/tests/components/portainer/ @erwindouna
/homeassistant/components/power/ @home-assistant/core
/tests/components/power/ @home-assistant/core
/homeassistant/components/powerfox/ @klaasnicolaas
/tests/components/powerfox/ @klaasnicolaas
/homeassistant/components/powerfox_local/ @klaasnicolaas
@@ -1561,8 +1579,8 @@ build.json @home-assistant/supervisor
/tests/components/sma/ @kellerza @rklomp @erwindouna
/homeassistant/components/smappee/ @bsmappee
/tests/components/smappee/ @bsmappee
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
/tests/components/smarla/ @explicatis @rlint-explicatis
/homeassistant/components/smarla/ @explicatis @johannes-exp
/tests/components/smarla/ @explicatis @johannes-exp
/homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek
@@ -1588,6 +1606,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/solaredge_local/ @drobtravels @scheric
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
/tests/components/solarlog/ @Ernst79 @dontinelli
/homeassistant/components/solarman/ @solarmanpv
/tests/components/solarman/ @solarmanpv
/homeassistant/components/solax/ @squishykid @Darsstar
/tests/components/solax/ @squishykid @Darsstar
/homeassistant/components/soma/ @ratsept
@@ -1616,8 +1636,6 @@ build.json @home-assistant/supervisor
/tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk
/tests/components/starline/ @anonym-tsk
/homeassistant/components/starlink/ @boswelja
/tests/components/starlink/ @boswelja
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
/homeassistant/components/steam_online/ @tkdrob
@@ -1699,6 +1717,8 @@ build.json @home-assistant/supervisor
/tests/components/tellduslive/ @fredrike
/homeassistant/components/teltonika/ @karlbeecken
/tests/components/teltonika/ @karlbeecken
/homeassistant/components/temperature/ @home-assistant/core
/tests/components/temperature/ @home-assistant/core
/homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
@@ -1744,6 +1764,8 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek
/homeassistant/components/touchline/ @mnordseth
/tests/components/touchline/ @mnordseth
/homeassistant/components/touchline_sl/ @jnsgruk
/tests/components/touchline_sl/ @jnsgruk
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
@@ -1831,8 +1853,8 @@ build.json @home-assistant/supervisor
/tests/components/vegehub/ @thulrus
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/velux/ @Julius2342 @pawlizio @wollew
/tests/components/velux/ @Julius2342 @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz
@@ -1915,6 +1937,8 @@ build.json @home-assistant/supervisor
/tests/components/whois/ @frenck
/homeassistant/components/wiffi/ @mampfes
/tests/components/wiffi/ @mampfes
/homeassistant/components/wiim/ @Linkplay2020
/tests/components/wiim/ @Linkplay2020
/homeassistant/components/wilight/ @leofig-rj
/tests/components/wilight/ @leofig-rj
/homeassistant/components/window/ @home-assistant/core

3
Dockerfile generated
View File

@@ -10,7 +10,6 @@ LABEL \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"
@@ -30,7 +29,7 @@ RUN \
# Verify go2rtc can be executed
go2rtc --version \
# Install uv
&& pip3 install uv==0.10.6
&& pip3 install uv==0.11.1
WORKDIR /usr/src

View File

@@ -241,12 +241,18 @@ DEFAULT_INTEGRATIONS = {
*BASE_PLATFORMS,
#
# Integrations providing triggers and conditions for base platforms:
"air_quality",
"battery",
"door",
"garage_door",
"gate",
"humidity",
"illuminance",
"moisture",
"motion",
"occupancy",
"power",
"temperature",
"window",
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {

View File

@@ -1,5 +1,11 @@
{
"domain": "lg",
"name": "LG",
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
"integrations": [
"lg_infrared",
"lg_netcast",
"lg_soundbar",
"lg_thinq",
"webostv"
]
}

View File

@@ -9,6 +9,6 @@
},
"iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==6.2.1"],
"requirements": ["jaraco.abode==6.4.0"],
"single_config_entry": true
}

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
@@ -40,6 +41,7 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
device.temp_unit
],
@@ -48,12 +50,14 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
value_fn=lambda device: cast(float, device.humidity),
),
AbodeSensorDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
value_fn=lambda device: cast(float, device.lux),
),

View File

@@ -120,7 +120,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="timeout",
)
del self.login_task
self.login_task = None
return await self.async_step_user()
async def async_step_reauth(

View File

@@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/actron_air",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.4.1"]
}

View File

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

View File

@@ -0,0 +1,150 @@
"""Provides conditions for air quality."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.condition import (
Condition,
make_entity_numerical_condition,
make_entity_numerical_condition_with_unit,
make_entity_state_condition,
)
from homeassistant.util.unit_conversion import (
CarbonMonoxideConcentrationConverter,
MassVolumeConcentrationConverter,
NitrogenDioxideConcentrationConverter,
NitrogenMonoxideConcentrationConverter,
OzoneConcentrationConverter,
SulphurDioxideConcentrationConverter,
UnitlessRatioConverter,
)
def _make_detected_condition(
device_class: BinarySensorDeviceClass,
) -> type[Condition]:
"""Create a detected condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
)
def _make_cleared_condition(
device_class: BinarySensorDeviceClass,
) -> type[Condition]:
"""Create a cleared condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
)
CONDITIONS: dict[str, type[Condition]] = {
# Binary sensor conditions (detected/cleared)
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
# Numerical sensor conditions with unit conversion
"is_co_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"is_ozone_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"is_voc_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
),
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitlessRatioConverter,
),
"is_no_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
),
"is_no2_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
),
"is_so2_value": make_entity_numerical_condition_with_unit(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
),
# Numerical sensor conditions without unit conversion (single-unit device classes)
"is_co2_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
),
"is_pm1_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm25_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm4_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_pm10_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"is_n2o_value": make_entity_numerical_condition(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the air quality conditions."""
return CONDITIONS

View File

@@ -0,0 +1,588 @@
# --- Common condition fields ---
.condition_behavior: &condition_behavior
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
# --- Number or entity selectors ---
.number_or_entity_co: &number_or_entity_co
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
- domain: sensor
device_class: carbon_monoxide
- domain: number
device_class: carbon_monoxide
translation_key: number_or_entity
.number_or_entity_co2: &number_or_entity_co2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "ppm"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "ppm"
- domain: sensor
device_class: carbon_dioxide
- domain: number
device_class: carbon_dioxide
translation_key: number_or_entity
.number_or_entity_pm1: &number_or_entity_pm1
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm1
- domain: number
device_class: pm1
translation_key: number_or_entity
.number_or_entity_pm25: &number_or_entity_pm25
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm25
- domain: number
device_class: pm25
translation_key: number_or_entity
.number_or_entity_pm4: &number_or_entity_pm4
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm4
- domain: number
device_class: pm4
translation_key: number_or_entity
.number_or_entity_pm10: &number_or_entity_pm10
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm10
- domain: number
device_class: pm10
translation_key: number_or_entity
.number_or_entity_ozone: &number_or_entity_ozone
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: ozone
- domain: number
device_class: ozone
translation_key: number_or_entity
.number_or_entity_voc: &number_or_entity_voc
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "μg/m³"
- "mg/m³"
- domain: sensor
device_class: volatile_organic_compounds
- domain: number
device_class: volatile_organic_compounds
translation_key: number_or_entity
.number_or_entity_voc_ratio: &number_or_entity_voc_ratio
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- domain: sensor
device_class: volatile_organic_compounds_parts
- domain: number
device_class: volatile_organic_compounds_parts
translation_key: number_or_entity
.number_or_entity_no: &number_or_entity_no
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: nitrogen_monoxide
- domain: number
device_class: nitrogen_monoxide
translation_key: number_or_entity
.number_or_entity_no2: &number_or_entity_no2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: nitrogen_dioxide
- domain: number
device_class: nitrogen_dioxide
translation_key: number_or_entity
.number_or_entity_n2o: &number_or_entity_n2o
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: nitrous_oxide
- domain: number
device_class: nitrous_oxide
translation_key: number_or_entity
.number_or_entity_so2: &number_or_entity_so2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: sulphur_dioxide
- domain: number
device_class: sulphur_dioxide
translation_key: number_or_entity
# --- Unit selectors ---
.unit_co: &unit_co
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
.unit_ozone: &unit_ozone
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.unit_no2: &unit_no2
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.unit_no: &unit_no
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.unit_so2: &unit_so2
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.unit_voc: &unit_voc
required: false
selector:
select:
options:
- "μg/m³"
- "mg/m³"
.unit_voc_ratio: &unit_voc_ratio
required: false
selector:
select:
options:
- "ppb"
- "ppm"
# --- Binary sensor targets ---
.target_gas: &target_gas
entity:
- domain: binary_sensor
device_class: gas
.target_co_binary: &target_co_binary
entity:
- domain: binary_sensor
device_class: carbon_monoxide
.target_smoke: &target_smoke
entity:
- domain: binary_sensor
device_class: smoke
# --- Sensor targets ---
.target_co_sensor: &target_co_sensor
entity:
- domain: sensor
device_class: carbon_monoxide
.target_co2: &target_co2
entity:
- domain: sensor
device_class: carbon_dioxide
.target_pm1: &target_pm1
entity:
- domain: sensor
device_class: pm1
.target_pm25: &target_pm25
entity:
- domain: sensor
device_class: pm25
.target_pm4: &target_pm4
entity:
- domain: sensor
device_class: pm4
.target_pm10: &target_pm10
entity:
- domain: sensor
device_class: pm10
.target_ozone: &target_ozone
entity:
- domain: sensor
device_class: ozone
.target_voc: &target_voc
entity:
- domain: sensor
device_class: volatile_organic_compounds
.target_voc_ratio: &target_voc_ratio
entity:
- domain: sensor
device_class: volatile_organic_compounds_parts
.target_no: &target_no
entity:
- domain: sensor
device_class: nitrogen_monoxide
.target_no2: &target_no2
entity:
- domain: sensor
device_class: nitrogen_dioxide
.target_n2o: &target_n2o
entity:
- domain: sensor
device_class: nitrous_oxide
.target_so2: &target_so2
entity:
- domain: sensor
device_class: sulphur_dioxide
# --- Binary sensor conditions ---
.condition_binary_common: &condition_binary_common
fields:
behavior: *condition_behavior
is_gas_detected:
<<: *condition_binary_common
target: *target_gas
is_gas_cleared:
<<: *condition_binary_common
target: *target_gas
is_co_detected:
<<: *condition_binary_common
target: *target_co_binary
is_co_cleared:
<<: *condition_binary_common
target: *target_co_binary
is_smoke_detected:
<<: *condition_binary_common
target: *target_smoke
is_smoke_cleared:
<<: *condition_binary_common
target: *target_smoke
# --- Numerical sensor conditions with unit conversion ---
is_co_value:
target: *target_co_sensor
fields:
behavior: *condition_behavior
above: *number_or_entity_co
below: *number_or_entity_co
unit: *unit_co
is_ozone_value:
target: *target_ozone
fields:
behavior: *condition_behavior
above: *number_or_entity_ozone
below: *number_or_entity_ozone
unit: *unit_ozone
is_voc_value:
target: *target_voc
fields:
behavior: *condition_behavior
above: *number_or_entity_voc
below: *number_or_entity_voc
unit: *unit_voc
is_voc_ratio_value:
target: *target_voc_ratio
fields:
behavior: *condition_behavior
above: *number_or_entity_voc_ratio
below: *number_or_entity_voc_ratio
unit: *unit_voc_ratio
is_no_value:
target: *target_no
fields:
behavior: *condition_behavior
above: *number_or_entity_no
below: *number_or_entity_no
unit: *unit_no
is_no2_value:
target: *target_no2
fields:
behavior: *condition_behavior
above: *number_or_entity_no2
below: *number_or_entity_no2
unit: *unit_no2
is_so2_value:
target: *target_so2
fields:
behavior: *condition_behavior
above: *number_or_entity_so2
below: *number_or_entity_so2
unit: *unit_so2
# --- Numerical sensor conditions without unit conversion ---
is_co2_value:
target: *target_co2
fields:
behavior: *condition_behavior
above: *number_or_entity_co2
below: *number_or_entity_co2
is_pm1_value:
target: *target_pm1
fields:
behavior: *condition_behavior
above: *number_or_entity_pm1
below: *number_or_entity_pm1
is_pm25_value:
target: *target_pm25
fields:
behavior: *condition_behavior
above: *number_or_entity_pm25
below: *number_or_entity_pm25
is_pm4_value:
target: *target_pm4
fields:
behavior: *condition_behavior
above: *number_or_entity_pm4
below: *number_or_entity_pm4
is_pm10_value:
target: *target_pm10
fields:
behavior: *condition_behavior
above: *number_or_entity_pm10
below: *number_or_entity_pm10
is_n2o_value:
target: *target_n2o
fields:
behavior: *condition_behavior
above: *number_or_entity_n2o
below: *number_or_entity_n2o

View File

@@ -1,7 +1,164 @@
{
"conditions": {
"is_co2_value": {
"condition": "mdi:molecule-co2"
},
"is_co_cleared": {
"condition": "mdi:check-circle"
},
"is_co_detected": {
"condition": "mdi:molecule-co"
},
"is_co_value": {
"condition": "mdi:molecule-co"
},
"is_gas_cleared": {
"condition": "mdi:check-circle"
},
"is_gas_detected": {
"condition": "mdi:gas-cylinder"
},
"is_n2o_value": {
"condition": "mdi:factory"
},
"is_no2_value": {
"condition": "mdi:factory"
},
"is_no_value": {
"condition": "mdi:factory"
},
"is_ozone_value": {
"condition": "mdi:weather-sunny-alert"
},
"is_pm10_value": {
"condition": "mdi:blur"
},
"is_pm1_value": {
"condition": "mdi:blur"
},
"is_pm25_value": {
"condition": "mdi:blur"
},
"is_pm4_value": {
"condition": "mdi:blur"
},
"is_smoke_cleared": {
"condition": "mdi:check-circle"
},
"is_smoke_detected": {
"condition": "mdi:smoke-detector-variant"
},
"is_so2_value": {
"condition": "mdi:factory"
},
"is_voc_ratio_value": {
"condition": "mdi:air-filter"
},
"is_voc_value": {
"condition": "mdi:air-filter"
}
},
"entity_component": {
"_": {
"default": "mdi:air-filter"
}
},
"triggers": {
"co2_changed": {
"trigger": "mdi:molecule-co2"
},
"co2_crossed_threshold": {
"trigger": "mdi:molecule-co2"
},
"co_changed": {
"trigger": "mdi:molecule-co"
},
"co_cleared": {
"trigger": "mdi:check-circle"
},
"co_crossed_threshold": {
"trigger": "mdi:molecule-co"
},
"co_detected": {
"trigger": "mdi:molecule-co"
},
"gas_cleared": {
"trigger": "mdi:check-circle"
},
"gas_detected": {
"trigger": "mdi:gas-cylinder"
},
"n2o_changed": {
"trigger": "mdi:factory"
},
"n2o_crossed_threshold": {
"trigger": "mdi:factory"
},
"no2_changed": {
"trigger": "mdi:factory"
},
"no2_crossed_threshold": {
"trigger": "mdi:factory"
},
"no_changed": {
"trigger": "mdi:factory"
},
"no_crossed_threshold": {
"trigger": "mdi:factory"
},
"ozone_changed": {
"trigger": "mdi:weather-sunny-alert"
},
"ozone_crossed_threshold": {
"trigger": "mdi:weather-sunny-alert"
},
"pm10_changed": {
"trigger": "mdi:blur"
},
"pm10_crossed_threshold": {
"trigger": "mdi:blur"
},
"pm1_changed": {
"trigger": "mdi:blur"
},
"pm1_crossed_threshold": {
"trigger": "mdi:blur"
},
"pm25_changed": {
"trigger": "mdi:blur"
},
"pm25_crossed_threshold": {
"trigger": "mdi:blur"
},
"pm4_changed": {
"trigger": "mdi:blur"
},
"pm4_crossed_threshold": {
"trigger": "mdi:blur"
},
"smoke_cleared": {
"trigger": "mdi:check-circle"
},
"smoke_detected": {
"trigger": "mdi:smoke-detector-variant"
},
"so2_changed": {
"trigger": "mdi:factory"
},
"so2_crossed_threshold": {
"trigger": "mdi:factory"
},
"voc_changed": {
"trigger": "mdi:air-filter"
},
"voc_crossed_threshold": {
"trigger": "mdi:air-filter"
},
"voc_ratio_changed": {
"trigger": "mdi:air-filter"
},
"voc_ratio_crossed_threshold": {
"trigger": "mdi:air-filter"
}
}
}

View File

@@ -0,0 +1,964 @@
{
"common": {
"condition_above_description": "Require the value to be above this value.",
"condition_above_name": "Above",
"condition_behavior_description": "How the value should match on the targeted entities.",
"condition_behavior_name": "Behavior",
"condition_below_description": "Require the value to be below this value.",
"condition_below_name": "Below",
"condition_unit_description": "All values will be converted to this unit when evaluating the condition.",
"condition_unit_name": "Unit of measurement",
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
"trigger_behavior_name": "Behavior",
"trigger_changed_above_name": "Above",
"trigger_changed_below_name": "Below",
"trigger_threshold_lower_limit_description": "The lower limit of the threshold.",
"trigger_threshold_lower_limit_name": "Lower limit",
"trigger_threshold_type_description": "The type of threshold to use.",
"trigger_threshold_type_name": "Threshold type",
"trigger_threshold_upper_limit_description": "The upper limit of the threshold.",
"trigger_threshold_upper_limit_name": "Upper limit",
"trigger_unit_description": "All values will be converted to this unit when evaluating the trigger.",
"trigger_unit_name": "Unit of measurement"
},
"conditions": {
"is_co2_value": {
"description": "Tests the carbon dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "Carbon dioxide value"
},
"is_co_cleared": {
"description": "Tests if one or more carbon monoxide sensors are cleared.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Carbon monoxide cleared"
},
"is_co_detected": {
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Carbon monoxide detected"
},
"is_co_value": {
"description": "Tests the carbon monoxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Carbon monoxide value"
},
"is_gas_cleared": {
"description": "Tests if one or more gas sensors are cleared.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Gas cleared"
},
"is_gas_detected": {
"description": "Tests if one or more gas sensors are detecting gas.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Gas detected"
},
"is_n2o_value": {
"description": "Tests the nitrous oxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "Nitrous oxide value"
},
"is_no2_value": {
"description": "Tests the nitrogen dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Nitrogen dioxide value"
},
"is_no_value": {
"description": "Tests the nitrogen monoxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Nitrogen monoxide value"
},
"is_ozone_value": {
"description": "Tests the ozone level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Ozone value"
},
"is_pm10_value": {
"description": "Tests the PM10 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "PM10 value"
},
"is_pm1_value": {
"description": "Tests the PM1 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "PM1 value"
},
"is_pm25_value": {
"description": "Tests the PM2.5 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "PM2.5 value"
},
"is_pm4_value": {
"description": "Tests the PM4 level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
}
},
"name": "PM4 value"
},
"is_smoke_cleared": {
"description": "Tests if one or more smoke sensors are cleared.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Smoke cleared"
},
"is_smoke_detected": {
"description": "Tests if one or more smoke sensors are detecting smoke.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
}
},
"name": "Smoke detected"
},
"is_so2_value": {
"description": "Tests the sulphur dioxide level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Sulphur dioxide value"
},
"is_voc_ratio_value": {
"description": "Tests the volatile organic compounds ratio of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Volatile organic compounds ratio value"
},
"is_voc_value": {
"description": "Tests the volatile organic compounds level of one or more entities.",
"fields": {
"above": {
"description": "[%key:component::air_quality::common::condition_above_description%]",
"name": "[%key:component::air_quality::common::condition_above_name%]"
},
"behavior": {
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"below": {
"description": "[%key:component::air_quality::common::condition_below_description%]",
"name": "[%key:component::air_quality::common::condition_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::condition_unit_description%]",
"name": "[%key:component::air_quality::common::condition_unit_name%]"
}
},
"name": "Volatile organic compounds value"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
},
"trigger_threshold_type": {
"options": {
"above": "Above",
"below": "Below",
"between": "Between",
"outside": "Outside"
}
}
},
"title": "Air Quality",
"triggers": {
"co2_changed": {
"description": "Triggers after one or more carbon dioxide levels change.",
"fields": {
"above": {
"description": "Only trigger when carbon dioxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when carbon dioxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "Carbon dioxide level changed"
},
"co2_crossed_threshold": {
"description": "Triggers after one or more carbon dioxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Carbon dioxide level crossed threshold"
},
"co_changed": {
"description": "Triggers after one or more carbon monoxide levels change.",
"fields": {
"above": {
"description": "Only trigger when carbon monoxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when carbon monoxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Carbon monoxide level changed"
},
"co_cleared": {
"description": "Triggers after one or more carbon monoxide sensors stop detecting carbon monoxide.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Carbon monoxide cleared"
},
"co_crossed_threshold": {
"description": "Triggers after one or more carbon monoxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Carbon monoxide level crossed threshold"
},
"co_detected": {
"description": "Triggers after one or more carbon monoxide sensors start detecting carbon monoxide.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Carbon monoxide detected"
},
"gas_cleared": {
"description": "Triggers after one or more gas sensors stop detecting gas.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Gas cleared"
},
"gas_detected": {
"description": "Triggers after one or more gas sensors start detecting gas.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Gas detected"
},
"n2o_changed": {
"description": "Triggers after one or more nitrous oxide levels change.",
"fields": {
"above": {
"description": "Only trigger when nitrous oxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when nitrous oxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "Nitrous oxide level changed"
},
"n2o_crossed_threshold": {
"description": "Triggers after one or more nitrous oxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Nitrous oxide level crossed threshold"
},
"no2_changed": {
"description": "Triggers after one or more nitrogen dioxide levels change.",
"fields": {
"above": {
"description": "Only trigger when nitrogen dioxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when nitrogen dioxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Nitrogen dioxide level changed"
},
"no2_crossed_threshold": {
"description": "Triggers after one or more nitrogen dioxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Nitrogen dioxide level crossed threshold"
},
"no_changed": {
"description": "Triggers after one or more nitrogen monoxide levels change.",
"fields": {
"above": {
"description": "Only trigger when nitrogen monoxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when nitrogen monoxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Nitrogen monoxide level changed"
},
"no_crossed_threshold": {
"description": "Triggers after one or more nitrogen monoxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Nitrogen monoxide level crossed threshold"
},
"ozone_changed": {
"description": "Triggers after one or more ozone levels change.",
"fields": {
"above": {
"description": "Only trigger when ozone level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when ozone level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Ozone level changed"
},
"ozone_crossed_threshold": {
"description": "Triggers after one or more ozone levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Ozone level crossed threshold"
},
"pm10_changed": {
"description": "Triggers after one or more PM10 levels change.",
"fields": {
"above": {
"description": "Only trigger when PM10 level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when PM10 level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "PM10 level changed"
},
"pm10_crossed_threshold": {
"description": "Triggers after one or more PM10 levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "PM10 level crossed threshold"
},
"pm1_changed": {
"description": "Triggers after one or more PM1 levels change.",
"fields": {
"above": {
"description": "Only trigger when PM1 level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when PM1 level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "PM1 level changed"
},
"pm1_crossed_threshold": {
"description": "Triggers after one or more PM1 levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "PM1 level crossed threshold"
},
"pm25_changed": {
"description": "Triggers after one or more PM2.5 levels change.",
"fields": {
"above": {
"description": "Only trigger when PM2.5 level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when PM2.5 level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "PM2.5 level changed"
},
"pm25_crossed_threshold": {
"description": "Triggers after one or more PM2.5 levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "PM2.5 level crossed threshold"
},
"pm4_changed": {
"description": "Triggers after one or more PM4 levels change.",
"fields": {
"above": {
"description": "Only trigger when PM4 level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when PM4 level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
}
},
"name": "PM4 level changed"
},
"pm4_crossed_threshold": {
"description": "Triggers after one or more PM4 levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "PM4 level crossed threshold"
},
"smoke_cleared": {
"description": "Triggers after one or more smoke sensors stop detecting smoke.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Smoke cleared"
},
"smoke_detected": {
"description": "Triggers after one or more smoke sensors start detecting smoke.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
}
},
"name": "Smoke detected"
},
"so2_changed": {
"description": "Triggers after one or more sulphur dioxide levels change.",
"fields": {
"above": {
"description": "Only trigger when sulphur dioxide level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when sulphur dioxide level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Sulphur dioxide level changed"
},
"so2_crossed_threshold": {
"description": "Triggers after one or more sulphur dioxide levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Sulphur dioxide level crossed threshold"
},
"voc_changed": {
"description": "Triggers after one or more volatile organic compound levels change.",
"fields": {
"above": {
"description": "Only trigger when volatile organic compounds level is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when volatile organic compounds level is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Volatile organic compounds level changed"
},
"voc_crossed_threshold": {
"description": "Triggers after one or more volatile organic compounds levels cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Volatile organic compounds level crossed threshold"
},
"voc_ratio_changed": {
"description": "Triggers after one or more volatile organic compound ratios change.",
"fields": {
"above": {
"description": "Only trigger when volatile organic compounds ratio is above this value.",
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
},
"below": {
"description": "Only trigger when volatile organic compounds ratio is below this value.",
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
}
},
"name": "Volatile organic compounds ratio changed"
},
"voc_ratio_crossed_threshold": {
"description": "Triggers after one or more volatile organic compounds ratios cross a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
},
"threshold_type": {
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
},
"unit": {
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
},
"upper_limit": {
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
}
},
"name": "Volatile organic compounds ratio crossed threshold"
}
}
}

View File

@@ -0,0 +1,238 @@
"""Provides triggers for air quality."""
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.trigger import (
EntityTargetStateTriggerBase,
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_changed_with_unit_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_numerical_state_crossed_threshold_with_unit_trigger,
make_entity_target_state_trigger,
)
from homeassistant.util.unit_conversion import (
CarbonMonoxideConcentrationConverter,
MassVolumeConcentrationConverter,
NitrogenDioxideConcentrationConverter,
NitrogenMonoxideConcentrationConverter,
OzoneConcentrationConverter,
SulphurDioxideConcentrationConverter,
UnitlessRatioConverter,
)
def _make_detected_trigger(
device_class: BinarySensorDeviceClass,
) -> type[EntityTargetStateTriggerBase]:
"""Create a detected trigger for a binary sensor device class."""
return make_entity_target_state_trigger(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
)
def _make_cleared_trigger(
device_class: BinarySensorDeviceClass,
) -> type[EntityTargetStateTriggerBase]:
"""Create a cleared trigger for a binary sensor device class."""
return make_entity_target_state_trigger(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
)
TRIGGERS: dict[str, type[Trigger]] = {
# Binary sensor triggers (detected/cleared)
"gas_detected": _make_detected_trigger(BinarySensorDeviceClass.GAS),
"gas_cleared": _make_cleared_trigger(BinarySensorDeviceClass.GAS),
"co_detected": _make_detected_trigger(BinarySensorDeviceClass.CO),
"co_cleared": _make_cleared_trigger(BinarySensorDeviceClass.CO),
"smoke_detected": _make_detected_trigger(BinarySensorDeviceClass.SMOKE),
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
# Numerical sensor triggers with unit conversion
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
),
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
),
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitlessRatioConverter,
),
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitlessRatioConverter,
),
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
),
"no_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
),
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
),
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
),
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
),
"so2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
),
# Numerical sensor triggers without unit conversion (single-unit device classes)
"co2_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
),
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
),
"pm1_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm25_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm4_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm10_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"n2o_changed": make_entity_numerical_state_changed_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{
SENSOR_DOMAIN: NumericalDomainSpec(
device_class=SensorDeviceClass.NITROUS_OXIDE
)
},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for air quality."""
return TRIGGERS

View File

@@ -0,0 +1,692 @@
.trigger_common_fields:
behavior: &trigger_behavior
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
.number_or_entity_co: &number_or_entity_co
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
- domain: sensor
device_class: carbon_monoxide
- domain: number
device_class: carbon_monoxide
translation_key: number_or_entity
.number_or_entity_co2: &number_or_entity_co2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "ppm"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "ppm"
- domain: sensor
device_class: carbon_dioxide
- domain: number
device_class: carbon_dioxide
translation_key: number_or_entity
.number_or_entity_pm1: &number_or_entity_pm1
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm1
- domain: number
device_class: pm1
translation_key: number_or_entity
.number_or_entity_pm25: &number_or_entity_pm25
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm25
- domain: number
device_class: pm25
translation_key: number_or_entity
.number_or_entity_pm4: &number_or_entity_pm4
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm4
- domain: number
device_class: pm4
translation_key: number_or_entity
.number_or_entity_pm10: &number_or_entity_pm10
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: pm10
- domain: number
device_class: pm10
translation_key: number_or_entity
.number_or_entity_ozone: &number_or_entity_ozone
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: ozone
- domain: number
device_class: ozone
translation_key: number_or_entity
.number_or_entity_voc: &number_or_entity_voc
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "μg/m³"
- "mg/m³"
- domain: sensor
device_class: volatile_organic_compounds
- domain: number
device_class: volatile_organic_compounds
translation_key: number_or_entity
.number_or_entity_voc_ratio: &number_or_entity_voc_ratio
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- domain: sensor
device_class: volatile_organic_compounds_parts
- domain: number
device_class: volatile_organic_compounds_parts
translation_key: number_or_entity
.number_or_entity_no: &number_or_entity_no
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: nitrogen_monoxide
- domain: number
device_class: nitrogen_monoxide
translation_key: number_or_entity
.number_or_entity_no2: &number_or_entity_no2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "ppm"
- "μg/m³"
- domain: sensor
device_class: nitrogen_dioxide
- domain: number
device_class: nitrogen_dioxide
translation_key: number_or_entity
.number_or_entity_n2o: &number_or_entity_n2o
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "μg/m³"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "μg/m³"
- domain: sensor
device_class: nitrous_oxide
- domain: number
device_class: nitrous_oxide
translation_key: number_or_entity
.number_or_entity_so2: &number_or_entity_so2
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "ppb"
- "μg/m³"
- domain: sensor
device_class: sulphur_dioxide
- domain: number
device_class: sulphur_dioxide
translation_key: number_or_entity
.unit_co: &unit_co
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "mg/m³"
- "μg/m³"
.unit_ozone: &unit_ozone
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.unit_no2: &unit_no2
required: false
selector:
select:
options:
- "ppb"
- "ppm"
- "μg/m³"
.unit_no: &unit_no
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.unit_so2: &unit_so2
required: false
selector:
select:
options:
- "ppb"
- "μg/m³"
.unit_voc: &unit_voc
required: false
selector:
select:
options:
- "μg/m³"
- "mg/m³"
.unit_voc_ratio: &unit_voc_ratio
required: false
selector:
select:
options:
- "ppb"
- "ppm"
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
selector:
select:
options:
- above
- below
- between
- outside
translation_key: trigger_threshold_type
# Binary sensor detected/cleared trigger fields
.trigger_binary_fields: &trigger_binary_fields
behavior: *trigger_behavior
# --- Binary sensor targets ---
.target_gas: &target_gas
entity:
- domain: binary_sensor
device_class: gas
.target_co_binary: &target_co_binary
entity:
- domain: binary_sensor
device_class: carbon_monoxide
.target_smoke: &target_smoke
entity:
- domain: binary_sensor
device_class: smoke
# --- Sensor targets ---
.target_co_sensor: &target_co_sensor
entity:
- domain: sensor
device_class: carbon_monoxide
.target_co2: &target_co2
entity:
- domain: sensor
device_class: carbon_dioxide
.target_pm1: &target_pm1
entity:
- domain: sensor
device_class: pm1
.target_pm25: &target_pm25
entity:
- domain: sensor
device_class: pm25
.target_pm4: &target_pm4
entity:
- domain: sensor
device_class: pm4
.target_pm10: &target_pm10
entity:
- domain: sensor
device_class: pm10
.target_ozone: &target_ozone
entity:
- domain: sensor
device_class: ozone
.target_voc: &target_voc
entity:
- domain: sensor
device_class: volatile_organic_compounds
.target_voc_ratio: &target_voc_ratio
entity:
- domain: sensor
device_class: volatile_organic_compounds_parts
.target_no: &target_no
entity:
- domain: sensor
device_class: nitrogen_monoxide
.target_no2: &target_no2
entity:
- domain: sensor
device_class: nitrogen_dioxide
.target_n2o: &target_n2o
entity:
- domain: sensor
device_class: nitrous_oxide
.target_so2: &target_so2
entity:
- domain: sensor
device_class: sulphur_dioxide
# --- Binary sensor triggers ---
gas_detected:
fields: *trigger_binary_fields
target: *target_gas
gas_cleared:
fields: *trigger_binary_fields
target: *target_gas
co_detected:
fields: *trigger_binary_fields
target: *target_co_binary
co_cleared:
fields: *trigger_binary_fields
target: *target_co_binary
smoke_detected:
fields: *trigger_binary_fields
target: *target_smoke
smoke_cleared:
fields: *trigger_binary_fields
target: *target_smoke
# --- Numerical sensor triggers ---
co_changed:
target: *target_co_sensor
fields:
above: *number_or_entity_co
below: *number_or_entity_co
unit: *unit_co
co_crossed_threshold:
target: *target_co_sensor
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_co
upper_limit: *number_or_entity_co
unit: *unit_co
co2_changed:
target: *target_co2
fields:
above: *number_or_entity_co2
below: *number_or_entity_co2
co2_crossed_threshold:
target: *target_co2
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_co2
upper_limit: *number_or_entity_co2
pm1_changed:
target: *target_pm1
fields:
above: *number_or_entity_pm1
below: *number_or_entity_pm1
pm1_crossed_threshold:
target: *target_pm1
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_pm1
upper_limit: *number_or_entity_pm1
pm25_changed:
target: *target_pm25
fields:
above: *number_or_entity_pm25
below: *number_or_entity_pm25
pm25_crossed_threshold:
target: *target_pm25
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_pm25
upper_limit: *number_or_entity_pm25
pm4_changed:
target: *target_pm4
fields:
above: *number_or_entity_pm4
below: *number_or_entity_pm4
pm4_crossed_threshold:
target: *target_pm4
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_pm4
upper_limit: *number_or_entity_pm4
pm10_changed:
target: *target_pm10
fields:
above: *number_or_entity_pm10
below: *number_or_entity_pm10
pm10_crossed_threshold:
target: *target_pm10
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_pm10
upper_limit: *number_or_entity_pm10
ozone_changed:
target: *target_ozone
fields:
above: *number_or_entity_ozone
below: *number_or_entity_ozone
unit: *unit_ozone
ozone_crossed_threshold:
target: *target_ozone
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_ozone
upper_limit: *number_or_entity_ozone
unit: *unit_ozone
voc_changed:
target: *target_voc
fields:
above: *number_or_entity_voc
below: *number_or_entity_voc
unit: *unit_voc
voc_crossed_threshold:
target: *target_voc
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_voc
upper_limit: *number_or_entity_voc
unit: *unit_voc
voc_ratio_changed:
target: *target_voc_ratio
fields:
above: *number_or_entity_voc_ratio
below: *number_or_entity_voc_ratio
unit: *unit_voc_ratio
voc_ratio_crossed_threshold:
target: *target_voc_ratio
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_voc_ratio
upper_limit: *number_or_entity_voc_ratio
unit: *unit_voc_ratio
no_changed:
target: *target_no
fields:
above: *number_or_entity_no
below: *number_or_entity_no
unit: *unit_no
no_crossed_threshold:
target: *target_no
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_no
upper_limit: *number_or_entity_no
unit: *unit_no
no2_changed:
target: *target_no2
fields:
above: *number_or_entity_no2
below: *number_or_entity_no2
unit: *unit_no2
no2_crossed_threshold:
target: *target_no2
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_no2
upper_limit: *number_or_entity_no2
unit: *unit_no2
n2o_changed:
target: *target_n2o
fields:
above: *number_or_entity_n2o
below: *number_or_entity_n2o
n2o_crossed_threshold:
target: *target_n2o
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_n2o
upper_limit: *number_or_entity_n2o
so2_changed:
target: *target_so2
fields:
above: *number_or_entity_so2
below: *number_or_entity_so2
unit: *unit_so2
so2_crossed_threshold:
target: *target_so2
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity_so2
upper_limit: *number_or_entity_so2
unit: *unit_so2

View File

@@ -87,7 +87,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
device_info = await airq.fetch_device_info()
await self.async_set_unique_id(device_info["id"])
await self.async_set_unique_id(device_info["id"], raise_on_progress=False)
self._abort_if_unique_id_configured()
_LOGGER.debug("Creating an entry for %s", device_info["name"])

View File

@@ -0,0 +1,36 @@
"""Diagnostics support for air-Q."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from . import AirQConfigEntry
REDACT_CONFIG = {CONF_PASSWORD, CONF_UNIQUE_ID, CONF_IP_ADDRESS, "title"}
REDACT_DEVICE_INFO = {"identifiers", "name"}
REDACT_COORDINATOR_DATA = {"DeviceID"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirQConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
return {
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
"device_info": async_redact_data(
dict(coordinator.device_info), REDACT_DEVICE_INFO
),
"coordinator_data": async_redact_data(
coordinator.data, REDACT_COORDINATOR_DATA
),
"options": {
"clip_negative": coordinator.clip_negative,
"return_average": coordinator.return_average,
},
}

View File

@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
},
"error": {

View File

@@ -46,19 +46,10 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
try:
doors = await client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
coordinator = AladdinConnectCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
for door in doors
}
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -100,7 +91,7 @@ def remove_stale_devices(
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
all_device_ids = set(config_entry.runtime_data)
all_device_ids = set(config_entry.runtime_data.data)
for device_entry in device_entries:
device_id: str | None = None

View File

@@ -11,22 +11,24 @@ from genie_partner_sdk.model import GarageDoor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
SCAN_INTERVAL = timedelta(seconds=15)
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
"""Coordinator for Aladdin Connect integration."""
config_entry: AladdinConnectConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: AladdinConnectConfigEntry,
client: AladdinConnectClient,
garage_door: GarageDoor,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -37,18 +39,16 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
update_interval=SCAN_INTERVAL,
)
self.client = client
self.data = garage_door
async def _async_update_data(self) -> GarageDoor:
async def _async_update_data(self) -> dict[str, GarageDoor]:
"""Fetch data from the Aladdin Connect API."""
try:
await self.client.update_door(self.data.device_id, self.data.door_number)
doors = await self.client.get_doors()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(err) from err
raise UpdateFailed(f"Error communicating with API: {err}") from err
except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data
return {door.unique_id: door for door in doors}

View File

@@ -7,7 +7,7 @@ from typing import Any
import aiohttp
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -24,11 +24,22 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the cover platform."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@@ -38,10 +49,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
_attr_supported_features = SUPPORTED_FEATURES
_attr_name = None
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize the Aladdin Connect cover."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.unique_id
super().__init__(coordinator, door_id)
self._attr_unique_id = door_id
async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover."""
@@ -66,16 +77,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
if (status := self.coordinator.data.status) is None:
if (status := self.door.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:
"""Update is closing attribute."""
return self.coordinator.data.status == "closing"
return self.door.status == "closing"
@property
def is_opening(self) -> bool | None:
"""Update is opening attribute."""
return self.coordinator.data.status == "opening"
return self.door.status == "opening"

View File

@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"doors": {
uid: {
"device_id": coordinator.data.device_id,
"door_number": coordinator.data.door_number,
"name": coordinator.data.name,
"status": coordinator.data.status,
"link_status": coordinator.data.link_status,
"battery_level": coordinator.data.battery_level,
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
}
for uid, coordinator in config_entry.runtime_data.items()
for uid, door in config_entry.runtime_data.data.items()
},
}

View File

@@ -1,6 +1,7 @@
"""Base class for Aladdin Connect entities."""
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -14,17 +15,28 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
"""Initialize Aladdin Connect entity."""
super().__init__(coordinator)
device = coordinator.data
self._door_id = door_id
door = self.door
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
identifiers={(DOMAIN, door.unique_id)},
manufacturer="Aladdin Connect",
name=device.name,
name=door.name,
)
self._device_id = device.device_id
self._number = device.door_number
self._device_id = door.device_id
self._number = door.door_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._door_id in self.coordinator.data
@property
def door(self) -> GarageDoor:
"""Return the garage door data."""
return self.coordinator.data[self._door_id]
@property
def client(self) -> AladdinConnectClient:

View File

@@ -57,7 +57,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

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
@@ -49,13 +49,24 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aladdin Connect sensor devices."""
coordinators = entry.runtime_data
coordinator = entry.runtime_data
known_devices: set[str] = set()
async_add_entities(
AladdinConnectSensor(coordinator, description)
for coordinator in coordinators.values()
for description in SENSOR_TYPES
)
@callback
def _async_add_new_devices() -> None:
"""Detect and add entities for new doors."""
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AladdinConnectSensor(coordinator, door_id, description)
for door_id in new_devices
for description in SENSOR_TYPES
)
_async_add_new_devices()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
@@ -66,14 +77,15 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
def __init__(
self,
coordinator: AladdinConnectCoordinator,
door_id: str,
entity_description: AladdinConnectSensorEntityDescription,
) -> None:
"""Initialize the Aladdin Connect sensor."""
super().__init__(coordinator)
super().__init__(coordinator, door_id)
self.entity_description = entity_description
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
self._attr_unique_id = f"{door_id}-{entity_description.key}"
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
return self.entity_description.value_fn(self.door)

View File

@@ -173,7 +173,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm away"
"name": "Arm alarm away"
},
"alarm_arm_custom_bypass": {
"description": "Arms an alarm while allowing to bypass a custom area.",
@@ -183,7 +183,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm with custom bypass"
"name": "Arm alarm with custom bypass"
},
"alarm_arm_home": {
"description": "Arms an alarm in the home mode.",
@@ -193,7 +193,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm home"
"name": "Arm alarm home"
},
"alarm_arm_night": {
"description": "Arms an alarm in the night mode.",
@@ -203,7 +203,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm night"
"name": "Arm alarm night"
},
"alarm_arm_vacation": {
"description": "Arms an alarm in the vacation mode.",
@@ -213,7 +213,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm vacation"
"name": "Arm alarm vacation"
},
"alarm_disarm": {
"description": "Disarms an alarm.",
@@ -223,7 +223,7 @@
"name": "Code"
}
},
"name": "Disarm"
"name": "Disarm alarm"
},
"alarm_trigger": {
"description": "Triggers an alarm manually.",
@@ -233,7 +233,7 @@
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Trigger"
"name": "Trigger alarm"
}
},
"title": "Alarm control panel",

View File

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

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.2.3"],
"requirements": ["androidtvremote2==0.3.1"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}

View File

@@ -137,5 +137,4 @@ async def async_pipeline_from_audio_stream(
audio_settings=audio_settings or AudioSettings(),
),
)
await pipeline_input.validate()
await pipeline_input.execute()
await pipeline_input.execute(validate=True)

View File

@@ -1,7 +1,14 @@
"""Assist pipeline errors."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.exceptions import HomeAssistantError
if TYPE_CHECKING:
from .pipeline import PipelineStage
class PipelineError(HomeAssistantError):
"""Base class for pipeline errors."""
@@ -55,3 +62,25 @@ class IntentRecognitionError(PipelineError):
class TextToSpeechError(PipelineError):
"""Error in text-to-speech portion of pipeline."""
class PipelineRunValidationError(PipelineError):
"""Error when a pipeline run is not valid."""
def __init__(self, message: str) -> None:
"""Set error message."""
super().__init__("validation-error", message)
class InvalidPipelineStagesError(PipelineRunValidationError):
"""Error when given an invalid combination of start/end stages."""
def __init__(
self,
start_stage: PipelineStage,
end_stage: PipelineStage,
) -> None:
"""Set error message."""
super().__init__(
f"Invalid stage combination: start={start_stage}, end={end_stage}"
)

View File

@@ -73,8 +73,10 @@ from .const import (
from .error import (
DuplicateWakeUpDetectedError,
IntentRecognitionError,
InvalidPipelineStagesError,
PipelineError,
PipelineNotFound,
PipelineRunValidationError,
SpeechToTextError,
TextToSpeechError,
WakeWordDetectionAborted,
@@ -492,24 +494,6 @@ PIPELINE_STAGE_ORDER = [
]
class PipelineRunValidationError(Exception):
"""Error when a pipeline run is not valid."""
class InvalidPipelineStagesError(PipelineRunValidationError):
"""Error when given an invalid combination of start/end stages."""
def __init__(
self,
start_stage: PipelineStage,
end_stage: PipelineStage,
) -> None:
"""Set error message."""
super().__init__(
f"Invalid stage combination: start={start_stage}, end={end_stage}"
)
@dataclass(frozen=True)
class WakeWordSettings:
"""Settings for wake word detection."""
@@ -662,7 +646,8 @@ class PipelineRun:
"""Emit run start event."""
self._device_id = device_id
self._satellite_id = satellite_id
self._start_debug_recording_thread()
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
self._start_debug_recording_thread()
data: dict[str, Any] = {
"pipeline": self.pipeline.id,
@@ -1504,9 +1489,7 @@ class PipelineRun:
def _start_debug_recording_thread(self) -> None:
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
if self.debug_recording_thread is not None:
# Already started
return
assert self.debug_recording_thread is None
# Directory to save audio for each pipeline run.
# Configured in YAML for assist_pipeline.
@@ -1681,26 +1664,39 @@ class PipelineInput:
satellite_id: str | None = None
"""Identifier of the satellite that is processing the input/output of the pipeline."""
async def execute(self) -> None:
async def execute(self, validate: bool = False) -> None:
"""Run pipeline."""
validation_error: PipelineError | None = None
if validate:
try:
await self.validate()
except PipelineError as err:
validation_error = err
self.run.start(
conversation_id=self.session.conversation_id,
device_id=self.device_id,
satellite_id=self.satellite_id,
)
current_stage: PipelineStage | None = self.run.start_stage
stt_audio_buffer: list[EnhancedAudioChunk] = []
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
if self.stt_stream is not None:
if self.run.audio_settings.needs_processor:
# VAD/noise suppression/auto gain/volume
stt_processed_stream = self.run.process_enhance_audio(self.stt_stream)
else:
# Volume multiplier only
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
try:
if validation_error is not None:
raise validation_error
stt_audio_buffer: list[EnhancedAudioChunk] = []
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
if self.stt_stream is not None:
if self.run.audio_settings.needs_processor:
# VAD/noise suppression/auto gain/volume
stt_processed_stream = self.run.process_enhance_audio(
self.stt_stream
)
else:
# Volume multiplier only
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
if current_stage == PipelineStage.WAKE_WORD:
# wake-word-detection
assert stt_processed_stream is not None

View File

@@ -626,7 +626,7 @@ def websocket_delete_all_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle delete all refresh tokens request."""
current_refresh_token: RefreshToken
current_refresh_token: RefreshToken | None = None
remove_failed = False
token_type = msg.get("token_type")
delete_current_token = msg.get("delete_current_token")
@@ -654,7 +654,7 @@ def websocket_delete_all_refresh_tokens(
else:
connection.send_result(msg["id"], {})
async def _delete_current_token_soon() -> None:
async def _delete_current_token_soon(current_refresh_token: RefreshToken) -> None:
"""Delete the current token after a delay.
We do not want to delete the current token immediately as it will
@@ -675,13 +675,15 @@ def websocket_delete_all_refresh_tokens(
# the token right away.
hass.auth.async_remove_refresh_token(current_refresh_token)
if delete_current_token and (
not limit_token_types or current_refresh_token.token_type == token_type
if (
delete_current_token
and current_refresh_token
and (not limit_token_types or current_refresh_token.token_type == token_type)
):
# Deleting the token will close the connection so we need
# to do it with a delay in a tracked task to ensure it still
# happens if Home Assistant is shutting down.
hass.async_create_task(_delete_current_token_soon())
hass.async_create_task(_delete_current_token_soon(current_refresh_token))
@websocket_api.websocket_command(

View File

@@ -115,6 +115,7 @@ def async_setup(
) -> None:
"""Component to allow users to login."""
hass.http.register_view(WellKnownOAuthInfoView)
hass.http.register_view(WellKnownProtectedResourceView)
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
@@ -154,6 +155,32 @@ class WellKnownOAuthInfoView(HomeAssistantView):
return self.json(metadata)
class WellKnownProtectedResourceView(HomeAssistantView):
"""View to host the OAuth2 Protected Resource Metadata per RFC9728."""
requires_auth = False
url = "/.well-known/oauth-protected-resource"
name = "well-known/oauth-protected-resource"
async def get(self, request: web.Request) -> web.Response:
"""Return the protected resource metadata."""
hass = request.app[KEY_HASS]
try:
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
return self.json_message("No URL available", HTTPStatus.NOT_FOUND)
return self.json(
{
"resource": url_prefix,
"authorization_servers": [url_prefix],
"resource_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
),
}
)
class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers."""

View File

@@ -118,27 +118,10 @@ SERVICE_TRIGGER = "trigger"
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"air_quality",
"alarm_control_panel",
"assist_satellite",
"climate",
"cover",
"device_tracker",
"fan",
"humidifier",
"lawn_mower",
"light",
"lock",
"media_player",
"person",
"siren",
"switch",
"vacuum",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"button",
"battery",
"climate",
"cover",
"device_tracker",
@@ -148,7 +131,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"gate",
"humidifier",
"humidity",
"input_boolean",
"illuminance",
"lawn_mower",
"light",
"lock",
@@ -156,14 +139,53 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"motion",
"occupancy",
"person",
"remote",
"scene",
"power",
"schedule",
"siren",
"switch",
"text",
"vacuum",
"water_heater",
"window",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"air_quality",
"alarm_control_panel",
"assist_satellite",
"button",
"climate",
"counter",
"cover",
"device_tracker",
"door",
"event",
"fan",
"garage_door",
"gate",
"humidifier",
"humidity",
"illuminance",
"lawn_mower",
"light",
"lock",
"media_player",
"moisture",
"motion",
"occupancy",
"person",
"power",
"remote",
"scene",
"schedule",
"select",
"siren",
"switch",
"temperature",
"text",
"update",
"vacuum",
"water_heater",
"window",
}

View File

@@ -147,7 +147,7 @@ class S3BackupAgent(BackupAgent):
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
await self._upload_simple(tar_filename, open_stream)
else:
await self._upload_multipart(tar_filename, open_stream)
await self._upload_multipart(tar_filename, open_stream, on_progress)
# Upload the metadata file
metadata_content = json.dumps(backup.as_dict())
@@ -188,11 +188,13 @@ class S3BackupAgent(BackupAgent):
self,
tar_filename: str,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
):
on_progress: OnProgressCallback,
) -> None:
"""Upload a large file using multipart upload.
:param tar_filename: The target filename for the backup.
:param open_stream: A function returning an async iterator that yields bytes.
:param on_progress: A callback to report the number of uploaded bytes.
"""
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
multipart_upload = await self._client.create_multipart_upload(
@@ -205,6 +207,7 @@ class S3BackupAgent(BackupAgent):
part_number = 1
buffer = bytearray() # bytes buffer to store the data
offset = 0 # start index of unread data inside buffer
bytes_uploaded = 0
stream = await open_stream()
async for chunk in stream:
@@ -233,6 +236,8 @@ class S3BackupAgent(BackupAgent):
Body=part_data.tobytes(),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
bytes_uploaded += len(part_data)
on_progress(bytes_uploaded=bytes_uploaded)
part_number += 1
finally:
view.release()
@@ -261,6 +266,8 @@ class S3BackupAgent(BackupAgent):
Body=remaining_data.tobytes(),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
bytes_uploaded += len(remaining_data)
on_progress(bytes_uploaded=bytes_uploaded)
await cast(Any, self._client).complete_multipart_upload(
Bucket=self._bucket,

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from ..const import LOGGER
from ..errors import AuthenticationRequired, CannotConnect
@@ -26,7 +26,7 @@ async def get_axis_api(
config: Mapping[str, Any],
) -> axis.AxisDevice:
"""Create a Axis device API."""
session = get_async_client(hass, verify_ssl=False)
session = async_get_clientsession(hass, verify_ssl=False)
api = axis.AxisDevice(
Configuration(

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==66"],
"requirements": ["axis==67"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -246,6 +246,8 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -332,8 +334,10 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size

View File

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

View File

@@ -0,0 +1,48 @@
"""Provides conditions for batteries."""
from __future__ import annotations
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
make_entity_numerical_condition,
make_entity_state_condition,
)
BATTERY_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY)
}
BATTERY_CHARGING_DOMAIN_SPECS = {
BINARY_SENSOR_DOMAIN: DomainSpec(
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
)
}
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.BATTERY),
}
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for batteries."""
return CONDITIONS

View File

@@ -0,0 +1,66 @@
.condition_common: &condition_common
target: &target_battery_binary_sensor
entity:
- domain: binary_sensor
device_class: battery
fields:
behavior: &condition_behavior
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
.number_or_entity: &number_or_entity
required: false
selector:
choose:
choices:
number:
selector:
number:
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
domain:
- input_number
- number
- sensor
translation_key: number_or_entity
is_low: *condition_common
is_not_low: *condition_common
is_charging:
target:
entity:
- domain: binary_sensor
device_class: battery_charging
fields:
behavior: *condition_behavior
is_not_charging:
target:
entity:
- domain: binary_sensor
device_class: battery_charging
fields:
behavior: *condition_behavior
is_level:
target:
entity:
- domain: sensor
device_class: battery
- domain: number
device_class: battery
fields:
behavior: *condition_behavior
above: *number_or_entity
below: *number_or_entity

View File

@@ -0,0 +1,19 @@
{
"conditions": {
"is_charging": {
"condition": "mdi:battery-charging"
},
"is_level": {
"condition": "mdi:battery-unknown"
},
"is_low": {
"condition": "mdi:battery-alert"
},
"is_not_charging": {
"condition": "mdi:battery"
},
"is_not_low": {
"condition": "mdi:battery"
}
}
}

View File

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

View File

@@ -0,0 +1,81 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted batteries.",
"condition_behavior_name": "Behavior"
},
"conditions": {
"is_charging": {
"description": "Tests if one or more batteries are charging.",
"fields": {
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
}
},
"name": "Battery is charging"
},
"is_level": {
"description": "Tests the battery level of one or more batteries.",
"fields": {
"above": {
"description": "Require the battery percentage to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"below": {
"description": "Require the battery percentage to be below this value.",
"name": "Below"
}
},
"name": "Battery level"
},
"is_low": {
"description": "Tests if one or more batteries are low.",
"fields": {
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
}
},
"name": "Battery is low"
},
"is_not_charging": {
"description": "Tests if one or more batteries are not charging.",
"fields": {
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
}
},
"name": "Battery is not charging"
},
"is_not_low": {
"description": "Tests if one or more batteries are not low.",
"fields": {
"behavior": {
"description": "[%key:component::battery::common::condition_behavior_description%]",
"name": "[%key:component::battery::common::condition_behavior_name%]"
}
},
"name": "Battery is not low"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
}
},
"title": "Battery"
}

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.4",
"dbus-fast==3.1.2",
"habluetooth==5.10.2"
"habluetooth==5.11.1"
]
}

View File

@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PASSKEY, DOMAIN
from .const import CONF_PASSKEY, DOMAIN, LOGGER
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
@@ -52,7 +52,7 @@ class BSBLanData:
client: BSBLAN
device: Device
info: Info
static: StaticState
static: StaticState | None
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
# the connection by fetching firmware version
await bsblan.initialize()
# Fetch device metadata in parallel for faster startup
device, info, static = await asyncio.gather(
# Fetch required device metadata in parallel for faster startup
device, info = await asyncio.gather(
bsblan.device(),
bsblan.info(),
bsblan.static_values(),
)
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
translation_key="setup_general_error",
) from err
try:
static = await bsblan.static_values()
except (BSBLANError, TimeoutError) as err:
LOGGER.debug(
"Static values not available for %s: %s",
entry.data[CONF_HOST],
err,
)
static = None
# Create coordinators with the already-initialized client
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)

View File

@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
# Set temperature range if available, otherwise use Home Assistant defaults
if data.static.min_temp is not None and data.static.min_temp.value is not None:
self._attr_min_temp = data.static.min_temp.value
if data.static.max_temp is not None and data.static.max_temp.value is not None:
self._attr_max_temp = data.static.max_temp.value
if (static := data.static) is not None:
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
self._attr_min_temp = min_temp.value
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
self._attr_max_temp = max_temp.value
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
@property

View File

@@ -183,90 +183,122 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
existing_entry = self._get_reauth_entry()
if user_input is None:
# Preserve existing values as defaults
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=existing_entry.data.get(
CONF_PASSKEY, vol.UNDEFINED
),
): str,
vol.Optional(
CONF_USERNAME,
default=existing_entry.data.get(
CONF_USERNAME, vol.UNDEFINED
),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
data_schema=self._build_credentials_schema(existing_entry.data),
)
# Combine existing data with the user's new input for validation.
# This correctly handles adding, changing, and clearing credentials.
config_data = existing_entry.data.copy()
config_data.update(user_input)
# Merge existing data with user input for validation
validate_data = {**existing_entry.data, **user_input}
errors = await self._async_validate_credentials(validate_data)
self.host = config_data[CONF_HOST]
self.port = config_data[CONF_PORT]
self.passkey = config_data.get(CONF_PASSKEY)
self.username = config_data.get(CONF_USERNAME)
self.password = config_data.get(CONF_PASSWORD)
if errors:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self._build_credentials_schema(user_input),
errors=errors,
)
return self.async_update_reload_and_abort(
existing_entry, data_updates=user_input, reason="reauth_successful"
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration flow."""
existing_entry = self._get_reconfigure_entry()
if user_input is None:
return self.async_show_form(
step_id="reconfigure",
data_schema=self._build_connection_schema(existing_entry.data),
)
# Merge existing data with user input for validation
validate_data = {**existing_entry.data, **user_input}
errors = await self._async_validate_credentials(validate_data)
if errors:
return self.async_show_form(
step_id="reconfigure",
data_schema=self._build_connection_schema(user_input),
errors=errors,
)
# Prevent reconfiguring to a different physical device
# it gets the unique ID from the device info when it validates credentials
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
existing_entry,
data_updates=user_input,
reason="reconfigure_successful",
)
async def _async_validate_credentials(self, data: dict[str, Any]) -> dict[str, str]:
"""Validate connection credentials and return errors dict."""
self.host = data[CONF_HOST]
self.port = data.get(CONF_PORT, DEFAULT_PORT)
self.passkey = data.get(CONF_PASSKEY)
self.username = data.get(CONF_USERNAME)
self.password = data.get(CONF_PASSWORD)
errors: dict[str, str] = {}
try:
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
except BSBLANAuthError:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
): str,
vol.Optional(
CONF_USERNAME,
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
errors={"base": "invalid_auth"},
)
errors["base"] = "invalid_auth"
except BSBLANError:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
): str,
vol.Optional(
CONF_USERNAME,
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
errors={"base": "cannot_connect"},
)
errors["base"] = "cannot_connect"
return errors
# Update only the fields that were provided by the user
return self.async_update_reload_and_abort(
existing_entry, data_updates=user_input, reason="reauth_successful"
@callback
def _build_credentials_schema(self, defaults: Mapping[str, Any]) -> vol.Schema:
"""Build schema for credentials-only forms (reauth)."""
return vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=defaults.get(CONF_PASSKEY) or vol.UNDEFINED,
): str,
vol.Optional(
CONF_USERNAME,
default=defaults.get(CONF_USERNAME) or vol.UNDEFINED,
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
)
@callback
def _build_connection_schema(self, defaults: Mapping[str, Any]) -> vol.Schema:
"""Build schema for full connection forms (user and reconfigure)."""
return vol.Schema(
{
vol.Required(
CONF_HOST,
default=defaults.get(CONF_HOST, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PORT,
default=defaults.get(CONF_PORT, DEFAULT_PORT),
): int,
vol.Optional(
CONF_PASSKEY,
default=defaults.get(CONF_PASSKEY) or vol.UNDEFINED,
): str,
vol.Optional(
CONF_USERNAME,
default=defaults.get(CONF_USERNAME) or vol.UNDEFINED,
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
)
@callback
@@ -274,32 +306,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show the setup form to the user."""
# Preserve user input if provided, otherwise use defaults
defaults = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED)
): str,
vol.Optional(
CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)
): int,
vol.Optional(
CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED)
): str,
vol.Optional(
CONF_USERNAME,
default=defaults.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=defaults.get(CONF_PASSWORD, vol.UNDEFINED),
): str,
}
),
data_schema=self._build_connection_schema(user_input or {}),
errors=errors or {},
)

View File

@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
"sensor": data.fast_coordinator.data.sensor.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump(),
},
"static": data.static.model_dump(),
"static": data.static.model_dump() if data.static is not None else None,
}
# Add DHW config and schedule from slow coordinator if available

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.1.2"],
"requirements": ["python-bsblan==5.1.3"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -58,7 +58,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |

View File

@@ -3,7 +3,9 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The device you are trying to reconfigure is not the same as the one previously configured."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -39,6 +41,24 @@
"description": "The BSB-LAN integration needs to re-authenticate with {name}",
"title": "[%key:common::config_flow::title::reauth%]"
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "[%key:component::bsblan::config::step::user::data_description::host%]",
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
"password": "[%key:component::bsblan::config::step::user::data_description::password%]",
"port": "[%key:component::bsblan::config::step::user::data_description::port%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]"
},
"description": "Update connection settings for your BSB-LAN device.",
"title": "Reconfigure BSB-LAN"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",

View File

@@ -23,8 +23,8 @@
},
"services": {
"press": {
"description": "Presses a button entity.",
"name": "Press"
"description": "Presses a button.",
"name": "Press button"
}
},
"title": "Button",

View File

@@ -90,7 +90,7 @@
"name": "Summary"
}
},
"name": "Create event"
"name": "Create calendar event"
},
"get_events": {
"description": "Retrieves events on a calendar within a time range.",
@@ -108,7 +108,7 @@
"name": "Start time"
}
},
"name": "Get events"
"name": "Get calendar events"
}
},
"title": "Calendar",

View File

@@ -432,6 +432,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
# Entity Properties
entity_description: CameraEntityDescription
_attr_brand: str | None = None
_attr_frame_interval: float = MIN_STREAM_INTERVAL
_attr_is_on: bool = True

View File

@@ -51,11 +51,11 @@
"services": {
"disable_motion_detection": {
"description": "Disables the motion detection of a camera.",
"name": "Disable motion detection"
"name": "Disable camera motion detection"
},
"enable_motion_detection": {
"description": "Enables the motion detection of a camera.",
"name": "Enable motion detection"
"name": "Enable camera motion detection"
},
"play_stream": {
"description": "Plays a camera stream on a supported media player.",
@@ -69,7 +69,7 @@
"name": "Media player"
}
},
"name": "Play stream"
"name": "Play camera stream"
},
"record": {
"description": "Creates a recording of a live camera feed.",
@@ -87,7 +87,7 @@
"name": "Lookback"
}
},
"name": "Record"
"name": "Record camera feed"
},
"snapshot": {
"description": "Takes a snapshot from a camera.",
@@ -97,15 +97,15 @@
"name": "Filename"
}
},
"name": "Take snapshot"
"name": "Take camera snapshot"
},
"turn_off": {
"description": "Turns off a camera.",
"name": "[%key:common::action::turn_off%]"
"name": "Turn off camera"
},
"turn_on": {
"description": "Turns on a camera.",
"name": "[%key:common::action::turn_on%]"
"name": "Turn on camera"
}
},
"title": "Camera"

View File

@@ -0,0 +1,39 @@
"""The Casper Glow integration."""
from __future__ import annotations
from pycasperglow import CasperGlow
from homeassistant.components import bluetooth
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
"""Set up Casper Glow from a config entry."""
address: str = entry.data[CONF_ADDRESS]
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Casper Glow device with address {address}"
)
glow = CasperGlow(ble_device)
coordinator = CasperGlowCoordinator(hass, glow, entry.title)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(coordinator.async_start())
return True
async def async_unload_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,51 @@
"""Casper Glow integration binary sensor platform."""
from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the binary sensor platform for Casper Glow."""
async_add_entities([CasperGlowPausedBinarySensor(entry.runtime_data)])
class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
"""Binary sensor indicating whether the Casper Glow dimming is paused."""
_attr_translation_key = "paused"
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the paused binary sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_paused"
if coordinator.device.state.is_paused is not None:
self._attr_is_on = coordinator.device.state.is_paused
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.is_paused is not None:
self._attr_is_on = state.is_paused
self.async_write_ha_state()

View File

@@ -0,0 +1,73 @@
"""Casper Glow integration button platform."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pycasperglow import CasperGlow
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
"""Describe a Casper Glow button entity."""
press_fn: Callable[[CasperGlow], Awaitable[None]]
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
CasperGlowButtonEntityDescription(
key="pause",
translation_key="pause",
press_fn=lambda device: device.pause(),
),
CasperGlowButtonEntityDescription(
key="resume",
translation_key="resume",
press_fn=lambda device: device.resume(),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the button platform for Casper Glow."""
async_add_entities(
CasperGlowButton(entry.runtime_data, description)
for description in BUTTON_DESCRIPTIONS
)
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
"""A Casper Glow button entity."""
entity_description: CasperGlowButtonEntityDescription
def __init__(
self,
coordinator: CasperGlowCoordinator,
description: CasperGlowButtonEntityDescription,
) -> None:
"""Initialize a Casper Glow button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{format_mac(coordinator.device.address)}_{description.key}"
)
async def async_press(self) -> None:
"""Press the button."""
await self._async_command(self.entity_description.press_fn(self._device))

View File

@@ -0,0 +1,151 @@
"""Config flow for Casper Glow integration."""
from __future__ import annotations
import logging
from typing import Any
from bluetooth_data_tools import human_readable_name
from pycasperglow import CasperGlow, CasperGlowError
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN, LOCAL_NAMES
_LOGGER = logging.getLogger(__name__)
class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Casper Glow."""
VERSION = 1
MINOR_VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(format_mac(discovery_info.address))
self._abort_if_unique_id_configured()
self._discovery_info = discovery_info
self.context["title_placeholders"] = {
"name": human_readable_name(
None, discovery_info.name, discovery_info.address
)
}
return await self.async_step_bluetooth_confirm()
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm a discovered Casper Glow device."""
assert self._discovery_info is not None
if user_input is not None:
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={CONF_ADDRESS: self._discovery_info.address},
)
glow = CasperGlow(self._discovery_info.device)
try:
await glow.handshake()
except CasperGlowError:
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception(
"Unexpected error during Casper Glow config flow "
"(step=bluetooth_confirm, address=%s)",
self._discovery_info.address,
)
return self.async_abort(reason="unknown")
self._set_confirm_only()
return self.async_show_form(
step_id="bluetooth_confirm",
description_placeholders=self.context["title_placeholders"],
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step to pick discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
address = user_input[CONF_ADDRESS]
discovery_info = self._discovered_devices[address]
await self.async_set_unique_id(
format_mac(discovery_info.address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
glow = CasperGlow(discovery_info.device)
try:
await glow.handshake()
except CasperGlowError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception(
"Unexpected error during Casper Glow config flow "
"(step=user, address=%s)",
discovery_info.address,
)
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=human_readable_name(
None, discovery_info.name, discovery_info.address
),
data={
CONF_ADDRESS: discovery_info.address,
},
)
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
current_addresses = self._async_current_ids(include_ignore=False)
for discovery in async_discovered_service_info(self.hass):
if (
format_mac(discovery.address) in current_addresses
or discovery.address in self._discovered_devices
or not (
discovery.name
and any(
discovery.name.startswith(local_name)
for local_name in LOCAL_NAMES
)
)
):
continue
self._discovered_devices[discovery.address] = discovery
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
data_schema = vol.Schema(
{
vol.Required(CONF_ADDRESS): vol.In(
{
service_info.address: human_readable_name(
None, service_info.name, service_info.address
)
for service_info in self._discovered_devices.values()
}
),
}
)
return self.async_show_form(
step_id="user",
data_schema=data_schema,
errors=errors,
)

View File

@@ -0,0 +1,16 @@
"""Constants for the Casper Glow integration."""
from datetime import timedelta
from pycasperglow import BRIGHTNESS_LEVELS, DEVICE_NAME_PREFIX, DIMMING_TIME_MINUTES
DOMAIN = "casper_glow"
LOCAL_NAMES = {DEVICE_NAME_PREFIX}
SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
# Interval between periodic state polls to catch externally-triggered changes.
STATE_POLL_INTERVAL = timedelta(seconds=30)

View File

@@ -0,0 +1,103 @@
"""Coordinator for the Casper Glow integration."""
from __future__ import annotations
import logging
from bleak import BleakError
from bluetooth_data_tools import monotonic_time_coarse
from pycasperglow import CasperGlow
from homeassistant.components.bluetooth import (
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfoBleak,
)
from homeassistant.components.bluetooth.active_update_coordinator import (
ActiveBluetoothDataUpdateCoordinator,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from .const import STATE_POLL_INTERVAL
_LOGGER = logging.getLogger(__name__)
type CasperGlowConfigEntry = ConfigEntry[CasperGlowCoordinator]
class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
"""Coordinator for Casper Glow BLE devices."""
def __init__(
self,
hass: HomeAssistant,
device: CasperGlow,
title: str,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass=hass,
logger=_LOGGER,
address=device.address,
mode=BluetoothScanningMode.PASSIVE,
needs_poll_method=self._needs_poll,
poll_method=self._async_update,
connectable=True,
)
self.device = device
self.last_dimming_time_minutes: int | None = (
device.state.configured_dimming_time_minutes
)
self.title = title
@callback
def _needs_poll(
self,
service_info: BluetoothServiceInfoBleak,
seconds_since_last_poll: float | None,
) -> bool:
"""Return True if a poll is needed."""
return (
seconds_since_last_poll is None
or seconds_since_last_poll >= STATE_POLL_INTERVAL.total_seconds()
)
async def _async_update(self, service_info: BluetoothServiceInfoBleak) -> None:
"""Poll device state."""
await self.device.query_state()
async def _async_poll(self) -> None:
"""Poll the device and log availability changes."""
assert self._last_service_info
try:
await self._async_poll_data(self._last_service_info)
except BleakError as exc:
if self.last_poll_successful:
_LOGGER.info("%s is unavailable: %s", self.title, exc)
self.last_poll_successful = False
return
except Exception:
if self.last_poll_successful:
_LOGGER.exception("%s: unexpected error while polling", self.title)
self.last_poll_successful = False
return
finally:
self._last_poll = monotonic_time_coarse()
if not self.last_poll_successful:
_LOGGER.info("%s is back online", self.title)
self.last_poll_successful = True
self._async_handle_bluetooth_poll()
@callback
def _async_handle_bluetooth_event(
self,
service_info: BluetoothServiceInfoBleak,
change: BluetoothChange,
) -> None:
"""Update BLE device reference on each advertisement."""
self.device.set_ble_device(service_info.device)
super()._async_handle_bluetooth_event(service_info, change)

View File

@@ -0,0 +1,47 @@
"""Base entity for the Casper Glow integration."""
from __future__ import annotations
from collections.abc import Awaitable
from pycasperglow import CasperGlowError
from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from .const import DOMAIN
from .coordinator import CasperGlowCoordinator
class CasperGlowEntity(PassiveBluetoothCoordinatorEntity[CasperGlowCoordinator]):
"""Base class for Casper Glow entities."""
_attr_has_entity_name = True
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize a Casper Glow entity."""
super().__init__(coordinator)
self._device = coordinator.device
self._attr_device_info = DeviceInfo(
manufacturer="Casper",
model="Glow",
model_id="G01",
connections={
(dr.CONNECTION_BLUETOOTH, format_mac(coordinator.device.address))
},
)
async def _async_command(self, coro: Awaitable[None]) -> None:
"""Execute a device command."""
try:
await coro
except CasperGlowError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(err)},
) from err

View File

@@ -0,0 +1,17 @@
{
"entity": {
"binary_sensor": {
"paused": {
"default": "mdi:timer-pause"
}
},
"button": {
"pause": {
"default": "mdi:pause"
},
"resume": {
"default": "mdi:play"
}
}
}
}

View File

@@ -0,0 +1,104 @@
"""Casper Glow integration light platform."""
from __future__ import annotations
from typing import Any
from pycasperglow import GlowState
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from .const import DEFAULT_DIMMING_TIME_MINUTES, SORTED_BRIGHTNESS_LEVELS
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
def _ha_brightness_to_device_pct(brightness: int) -> int:
"""Convert HA brightness (1-255) to device percentage by snapping to nearest."""
return percentage_to_ordered_list_item(
SORTED_BRIGHTNESS_LEVELS, round(brightness * 100 / 255)
)
def _device_pct_to_ha_brightness(pct: int) -> int:
"""Convert device brightness percentage (60-100) to HA brightness (1-255)."""
percent = ordered_list_item_to_percentage(SORTED_BRIGHTNESS_LEVELS, pct)
return round(percent * 255 / 100)
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the light platform for Casper Glow."""
async_add_entities([CasperGlowLight(entry.runtime_data)])
class CasperGlowLight(CasperGlowEntity, LightEntity):
"""Representation of a Casper Glow light."""
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_name = None
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize a Casper Glow light."""
super().__init__(coordinator)
self._attr_unique_id = format_mac(coordinator.device.address)
self._update_from_state(coordinator.device.state)
async def async_added_to_hass(self) -> None:
"""Register state update callback when entity is added."""
await super().async_added_to_hass()
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _update_from_state(self, state: GlowState) -> None:
"""Update entity attributes from device state."""
if state.is_on is not None:
self._attr_is_on = state.is_on
self._attr_color_mode = ColorMode.BRIGHTNESS
if state.brightness_level is not None:
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
self._update_from_state(state)
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
brightness_pct: int | None = None
if ATTR_BRIGHTNESS in kwargs:
brightness_pct = _ha_brightness_to_device_pct(kwargs[ATTR_BRIGHTNESS])
await self._async_command(self._device.turn_on())
self._attr_is_on = True
self._attr_color_mode = ColorMode.BRIGHTNESS
if brightness_pct is not None:
await self._async_command(
self._device.set_brightness_and_dimming_time(
brightness_pct,
self.coordinator.last_dimming_time_minutes
if self.coordinator.last_dimming_time_minutes is not None
else DEFAULT_DIMMING_TIME_MINUTES,
)
)
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self._async_command(self._device.turn_off())
self._attr_is_on = False

View File

@@ -0,0 +1,19 @@
{
"domain": "casper_glow",
"name": "Casper Glow",
"bluetooth": [
{
"connectable": true,
"local_name": "Jar*"
}
],
"codeowners": ["@mikeodr"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/casper_glow",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pycasperglow"],
"quality_scale": "silver",
"requirements": ["pycasperglow==1.1.0"]
}

View File

@@ -0,0 +1,70 @@
rules:
# Bronze
action-setup:
status: exempt
comment: No custom services.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: No custom actions/services.
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: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: Bluetooth device with no authentication credentials.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: No network discovery.
discovery: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: No web session is used by this integration.
strict-typing: done

View File

@@ -0,0 +1,49 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "Do you want to set up {name}?"
},
"user": {
"data": {
"address": "Bluetooth address"
},
"data_description": {
"address": "The Bluetooth address of the Casper Glow light"
}
}
}
},
"entity": {
"binary_sensor": {
"paused": {
"name": "Dimming paused"
}
},
"button": {
"pause": {
"name": "Pause dimming"
},
"resume": {
"name": "Resume dimming"
}
}
},
"exceptions": {
"communication_error": {
"message": "An error occurred while communicating with the Casper Glow: {error}"
}
}
}

View File

@@ -1,20 +1,68 @@
{
"entity": {
"sensor": {
"chess960_daily_draw": {
"default": "mdi:chess-pawn"
},
"chess960_daily_lost": {
"default": "mdi:chess-pawn"
},
"chess960_daily_rating": {
"default": "mdi:chart-line"
},
"chess960_daily_won": {
"default": "mdi:chess-pawn"
},
"chess_blitz_draw": {
"default": "mdi:chess-pawn"
},
"chess_blitz_lost": {
"default": "mdi:chess-pawn"
},
"chess_blitz_rating": {
"default": "mdi:chart-line"
},
"chess_blitz_won": {
"default": "mdi:chess-pawn"
},
"chess_bullet_draw": {
"default": "mdi:chess-pawn"
},
"chess_bullet_lost": {
"default": "mdi:chess-pawn"
},
"chess_bullet_rating": {
"default": "mdi:chart-line"
},
"chess_bullet_won": {
"default": "mdi:chess-pawn"
},
"chess_daily_draw": {
"default": "mdi:chess-pawn"
},
"chess_daily_lost": {
"default": "mdi:chess-pawn"
},
"chess_daily_rating": {
"default": "mdi:chart-line"
},
"chess_daily_won": {
"default": "mdi:chess-pawn"
},
"chess_rapid_draw": {
"default": "mdi:chess-pawn"
},
"chess_rapid_lost": {
"default": "mdi:chess-pawn"
},
"chess_rapid_rating": {
"default": "mdi:chart-line"
},
"chess_rapid_won": {
"default": "mdi:chess-pawn"
},
"followers": {
"default": "mdi:account-multiple"
},
"total_daily_draw": {
"default": "mdi:chess-pawn"
},
"total_daily_lost": {
"default": "mdi:chess-pawn"
},
"total_daily_won": {
"default": "mdi:chess-pawn"
}
}
}

View File

@@ -2,6 +2,9 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from chess_com_api import PlayerStats
from homeassistant.components.sensor import (
SensorEntity,
@@ -24,7 +27,14 @@ class ChessEntityDescription(SensorEntityDescription):
value_fn: Callable[[ChessData], float]
SENSORS: tuple[ChessEntityDescription, ...] = (
@dataclass(kw_only=True, frozen=True)
class ChessModeEntityDescription(SensorEntityDescription):
"""Sensor description for a Chess.com game mode."""
value_fn: Callable[[dict[str, Any]], float]
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
ChessEntityDescription(
key="followers",
translation_key="followers",
@@ -33,35 +43,46 @@ SENSORS: tuple[ChessEntityDescription, ...] = (
value_fn=lambda state: state.player.followers,
entity_registry_enabled_default=False,
),
ChessEntityDescription(
key="chess_daily_rating",
translation_key="chess_daily_rating",
)
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
ChessModeEntityDescription(
key="rating",
translation_key="rating",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
value_fn=lambda mode: mode["last"]["rating"],
),
ChessEntityDescription(
key="total_daily_won",
translation_key="total_daily_won",
ChessModeEntityDescription(
key="won",
translation_key="won",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
value_fn=lambda mode: mode["record"]["win"],
),
ChessEntityDescription(
key="total_daily_lost",
translation_key="total_daily_lost",
ChessModeEntityDescription(
key="lost",
translation_key="lost",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
value_fn=lambda mode: mode["record"]["loss"],
),
ChessEntityDescription(
key="total_daily_draw",
translation_key="total_daily_draw",
ChessModeEntityDescription(
key="draw",
translation_key="draw",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
value_fn=lambda mode: mode["record"]["draw"],
),
)
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
"chess_daily": lambda stats: stats.chess_daily,
"chess_rapid": lambda stats: stats.chess_rapid,
"chess_bullet": lambda stats: stats.chess_bullet,
"chess_blitz": lambda stats: stats.chess_blitz,
"chess960_daily": lambda stats: stats.chess960_daily,
}
async def async_setup_entry(
hass: HomeAssistant,
@@ -71,13 +92,22 @@ async def async_setup_entry(
"""Initialize the entries."""
coordinator = entry.runtime_data
async_add_entities(
ChessPlayerSensor(coordinator, description) for description in SENSORS
)
entities: list[SensorEntity] = [
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
]
for game_mode, stats_fn in GAME_MODES.items():
if stats_fn(coordinator.data.stats) is not None:
entities.extend(
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
for description in GAME_MODE_SENSORS
)
async_add_entities(entities)
class ChessPlayerSensor(ChessEntity, SensorEntity):
"""Chess.com sensor."""
"""Chess.com player sensor."""
entity_description: ChessEntityDescription
@@ -95,3 +125,33 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
def native_value(self) -> float:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)
class ChessGameModeSensor(ChessEntity, SensorEntity):
"""Chess.com game mode sensor."""
entity_description: ChessModeEntityDescription
def __init__(
self,
coordinator: ChessCoordinator,
description: ChessModeEntityDescription,
game_mode: str,
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._stats_fn = stats_fn
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
)
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
@property
def native_value(self) -> float:
"""Return the state of the sensor."""
mode_data = self._stats_fn(self.coordinator.data.stats)
if TYPE_CHECKING:
assert mode_data is not None
return self.entity_description.value_fn(mode_data)

View File

@@ -23,24 +23,84 @@
},
"entity": {
"sensor": {
"chess960_daily_draw": {
"name": "Total daily Chess960 games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess960_daily_lost": {
"name": "Total daily Chess960 games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess960_daily_rating": {
"name": "Daily Chess960 rating"
},
"chess960_daily_won": {
"name": "Total daily Chess960 games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_draw": {
"name": "Total blitz chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_lost": {
"name": "Total blitz chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_blitz_rating": {
"name": "Blitz chess rating"
},
"chess_blitz_won": {
"name": "Total blitz chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_draw": {
"name": "Total bullet chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_lost": {
"name": "Total bullet chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_bullet_rating": {
"name": "Bullet chess rating"
},
"chess_bullet_won": {
"name": "Total bullet chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_draw": {
"name": "Total daily chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_lost": {
"name": "Total daily chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_daily_rating": {
"name": "Daily chess rating"
},
"chess_daily_won": {
"name": "Total daily chess games won",
"unit_of_measurement": "games"
},
"chess_rapid_draw": {
"name": "Total rapid chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_rapid_lost": {
"name": "Total rapid chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"chess_rapid_rating": {
"name": "Rapid chess rating"
},
"chess_rapid_won": {
"name": "Total rapid chess games won",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
},
"followers": {
"name": "Followers",
"unit_of_measurement": "followers"
},
"total_daily_draw": {
"name": "Total chess games drawn",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
},
"total_daily_lost": {
"name": "Total chess games lost",
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
},
"total_daily_won": {
"name": "Total chess games won",
"unit_of_measurement": "games"
}
}
}

View File

@@ -66,6 +66,7 @@ class ClementineDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY
)
_attr_volume_step = 0.04
def __init__(self, client, name):
"""Initialize the Clementine device."""
@@ -124,16 +125,6 @@ class ClementineDevice(MediaPlayerEntity):
return None, None
def volume_up(self) -> None:
"""Volume up the media player."""
newvolume = min(self._client.volume + 4, 100)
self._client.set_volume(newvolume)
def volume_down(self) -> None:
"""Volume down media player."""
newvolume = max(self._client.volume - 4, 0)
self._client.set_volume(newvolume)
def mute_volume(self, mute: bool) -> None:
"""Send mute command."""
self._client.set_volume(0)

View File

@@ -1,10 +1,31 @@
"""Provides conditions for climates."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityNumericalConditionWithUnitBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
"""Mixin for climate target temperature conditions with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, entity_state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
return self._hass.config.units.temperature_unit
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
@@ -28,6 +49,11 @@ CONDITIONS: dict[str, type[Condition]] = {
"is_heating": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
"target_humidity": make_entity_numerical_condition(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_temperature": ClimateTargetTemperatureCondition,
}

View File

@@ -1,9 +1,9 @@
.condition_common: &condition_common
target:
target: &condition_climate_target
entity:
domain: climate
fields:
behavior:
behavior: &condition_behavior
required: true
default: any
selector:
@@ -13,8 +13,76 @@
- all
- any
.number_or_entity_humidity: &number_or_entity_humidity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
translation_key: number_or_entity
.number_or_entity_temperature: &number_or_entity_temperature
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.condition_unit_temperature: &condition_unit_temperature
required: false
selector:
select:
options:
- "°C"
- "°F"
is_off: *condition_common
is_on: *condition_common
is_cooling: *condition_common
is_drying: *condition_common
is_heating: *condition_common
target_humidity:
target: *condition_climate_target
fields:
behavior: *condition_behavior
above: *number_or_entity_humidity
below: *number_or_entity_humidity
target_temperature:
target: *condition_climate_target
fields:
behavior: *condition_behavior
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *condition_unit_temperature

View File

@@ -14,6 +14,12 @@
},
"is_on": {
"condition": "mdi:power-on"
},
"target_humidity": {
"condition": "mdi:water-percent"
},
"target_temperature": {
"condition": "mdi:thermometer"
}
},
"entity_component": {

View File

@@ -11,7 +11,8 @@ set_preset_mode:
required: true
example: "away"
selector:
text:
state:
attribute: preset_mode
set_temperature:
target:
@@ -55,16 +56,10 @@ set_temperature:
mode: box
hvac_mode:
selector:
select:
options:
- "off"
- "auto"
- "cool"
- "dry"
- "fan_only"
- "heat_cool"
- "heat"
translation_key: hvac_mode
state:
hide_states:
- unavailable
- unknown
set_humidity:
target:
entity:
@@ -91,7 +86,8 @@ set_fan_mode:
required: true
example: "low"
selector:
text:
state:
attribute: fan_mode
set_hvac_mode:
target:
@@ -115,7 +111,8 @@ set_swing_mode:
required: true
example: "on"
selector:
text:
state:
attribute: swing_mode
set_swing_horizontal_mode:
target:
@@ -128,7 +125,8 @@ set_swing_horizontal_mode:
required: true
example: "on"
selector:
text:
state:
attribute: swing_horizontal_mode
turn_on:
target:

View File

@@ -55,6 +55,46 @@
}
},
"name": "Climate-control device is on"
},
"target_humidity": {
"description": "Tests the humidity setpoint of one or more climate-control devices.",
"fields": {
"above": {
"description": "Require the target humidity to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::climate::common::condition_behavior_description%]",
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target humidity to be below this value.",
"name": "Below"
}
},
"name": "Climate-control device target humidity"
},
"target_temperature": {
"description": "Tests the temperature setpoint of one or more climate-control devices.",
"fields": {
"above": {
"description": "Require the target temperature to be above this value.",
"name": "Above"
},
"behavior": {
"description": "[%key:component::climate::common::condition_behavior_description%]",
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"below": {
"description": "Require the target temperature to be below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the condition.",
"name": "Unit of measurement"
}
},
"name": "Climate-control device target temperature"
}
},
"device_automation": {
@@ -241,17 +281,6 @@
"any": "Any"
}
},
"hvac_mode": {
"options": {
"auto": "[%key:common::state::auto%]",
"cool": "Cool",
"dry": "Dry",
"fan_only": "Fan only",
"heat": "Heat",
"heat_cool": "Heat/cool",
"off": "[%key:common::state::off%]"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
@@ -276,67 +305,67 @@
},
"services": {
"set_fan_mode": {
"description": "Sets fan operation mode.",
"description": "Sets the fan mode of a climate-control device.",
"fields": {
"fan_mode": {
"description": "Fan operation mode.",
"name": "Fan mode"
}
},
"name": "Set fan mode"
"name": "Set climate-control device fan mode"
},
"set_humidity": {
"description": "Sets target humidity.",
"description": "Sets the target humidity of a climate-control device.",
"fields": {
"humidity": {
"description": "Target humidity.",
"name": "Humidity"
}
},
"name": "Set target humidity"
"name": "Set climate-control device target humidity"
},
"set_hvac_mode": {
"description": "Sets HVAC operation mode.",
"description": "Sets the HVAC mode of a climate-control device.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
"name": "HVAC mode"
}
},
"name": "Set HVAC mode"
"name": "Set climate-control device HVAC mode"
},
"set_preset_mode": {
"description": "Sets preset mode.",
"description": "Sets the preset mode of a climate-control device.",
"fields": {
"preset_mode": {
"description": "Preset mode.",
"name": "Preset mode"
}
},
"name": "Set preset mode"
"name": "Set climate-control device preset mode"
},
"set_swing_horizontal_mode": {
"description": "Sets horizontal swing operation mode.",
"description": "Sets the horizontal swing mode of a climate-control device.",
"fields": {
"swing_horizontal_mode": {
"description": "Horizontal swing operation mode.",
"name": "Horizontal swing mode"
}
},
"name": "Set horizontal swing mode"
"name": "Set climate-control device horizontal swing mode"
},
"set_swing_mode": {
"description": "Sets swing operation mode.",
"description": "Sets the swing mode of a climate-control device.",
"fields": {
"swing_mode": {
"description": "Swing operation mode.",
"name": "Swing mode"
}
},
"name": "Set swing mode"
"name": "Set climate-control device swing mode"
},
"set_temperature": {
"description": "Sets the temperature setpoint.",
"description": "Sets the target temperature of a climate-control device.",
"fields": {
"hvac_mode": {
"description": "HVAC operation mode.",
@@ -355,19 +384,19 @@
"name": "Target temperature"
}
},
"name": "Set target temperature"
"name": "Set climate-control device target temperature"
},
"toggle": {
"description": "Toggles climate device, from on to off, or off to on.",
"name": "[%key:common::action::toggle%]"
"description": "Toggles a climate-control device on/off.",
"name": "Toggle climate-control device"
},
"turn_off": {
"description": "Turns climate device off.",
"name": "[%key:common::action::turn_off%]"
"description": "Turns off a climate-control device.",
"name": "Turn off climate-control device"
},
"turn_on": {
"description": "Turns climate device on.",
"name": "[%key:common::action::turn_on%]"
"description": "Turns on a climate-control device.",
"name": "Turn on climate-control device"
}
},
"title": "Climate",
@@ -462,6 +491,10 @@
"below": {
"description": "Trigger when the target temperature is below this value.",
"name": "Below"
},
"unit": {
"description": "All values will be converted to this unit when evaluating the trigger.",
"name": "Unit of measurement"
}
},
"name": "Climate-control device target temperature changed"
@@ -481,6 +514,10 @@
"description": "Type of threshold crossing to trigger on.",
"name": "Threshold type"
},
"unit": {
"description": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::description%]",
"name": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::name%]"
},
"upper_limit": {
"description": "Upper threshold limit.",
"name": "Upper threshold"

View File

@@ -2,12 +2,15 @@
import voluptuous as vol
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
from homeassistant.core import HomeAssistant
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
EntityNumericalStateTriggerWithUnitBase,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
@@ -16,6 +19,7 @@ from homeassistant.helpers.trigger import (
make_entity_target_state_trigger,
make_entity_transition_trigger,
)
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
@@ -44,6 +48,33 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
self._to_states = set(self._options[CONF_HVAC_MODE])
class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
"""Mixin for climate target temperature triggers with unit conversion."""
_base_unit = UnitOfTemperature.CELSIUS
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _get_entity_unit(self, state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
return self._hass.config.units.temperature_unit
class ClimateTargetTemperatureChangedTrigger(
_ClimateTargetTemperatureTriggerMixin,
EntityNumericalStateChangedTriggerWithUnitBase,
):
"""Trigger for climate target temperature value changes."""
class ClimateTargetTemperatureCrossedThresholdTrigger(
_ClimateTargetTemperatureTriggerMixin,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
):
"""Trigger for climate target temperature value crossing a threshold."""
TRIGGERS: dict[str, type[Trigger]] = {
"hvac_mode_changed": HVACModeChangedTrigger,
"started_cooling": make_entity_target_state_trigger(
@@ -53,17 +84,15 @@ TRIGGERS: dict[str, type[Trigger]] = {
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
),
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
),
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_entity_transition_trigger(
DOMAIN,

View File

@@ -14,7 +14,29 @@
- last
- any
.number_or_entity: &number_or_entity
.number_or_entity_humidity: &number_or_entity_humidity
required: false
selector:
choose:
choices:
number:
selector:
number:
mode: box
unit_of_measurement: "%"
entity:
selector:
entity:
filter:
- domain: input_number
unit_of_measurement: "%"
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
translation_key: number_or_entity
.number_or_entity_temperature: &number_or_entity_temperature
required: false
selector:
choose:
@@ -27,12 +49,24 @@
selector:
entity:
filter:
domain:
- input_number
- number
- sensor
- domain: input_number
unit_of_measurement:
- "°C"
- "°F"
- domain: sensor
device_class: temperature
- domain: number
device_class: temperature
translation_key: number_or_entity
.trigger_unit_temperature: &trigger_unit_temperature
required: false
selector:
select:
options:
- "°C"
- "°F"
.trigger_threshold_type: &trigger_threshold_type
required: true
default: above
@@ -69,27 +103,29 @@ hvac_mode_changed:
target_humidity_changed:
target: *trigger_climate_target
fields:
above: *number_or_entity
below: *number_or_entity
above: *number_or_entity_humidity
below: *number_or_entity_humidity
target_humidity_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity
upper_limit: *number_or_entity
lower_limit: *number_or_entity_humidity
upper_limit: *number_or_entity_humidity
target_temperature_changed:
target: *trigger_climate_target
fields:
above: *number_or_entity
below: *number_or_entity
above: *number_or_entity_temperature
below: *number_or_entity_temperature
unit: *trigger_unit_temperature
target_temperature_crossed_threshold:
target: *trigger_climate_target
fields:
behavior: *trigger_behavior
threshold_type: *trigger_threshold_type
lower_limit: *number_or_entity
upper_limit: *number_or_entity
lower_limit: *number_or_entity_temperature
upper_limit: *number_or_entity_temperature
unit: *trigger_unit_temperature

View File

@@ -138,6 +138,7 @@ class CloudBackupAgent(BackupAgent):
base64md5hash=base64md5hash,
metadata=metadata,
size=size,
on_progress=on_progress,
)
break
except CloudApiNonRetryableError as err:

View File

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

View File

@@ -144,7 +144,7 @@ class R2BackupAgent(BackupAgent):
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
await self._upload_simple(tar_filename, open_stream)
else:
await self._upload_multipart(tar_filename, open_stream)
await self._upload_multipart(tar_filename, open_stream, on_progress)
# Upload the metadata file
metadata_content = json.dumps(backup.as_dict())
@@ -185,11 +185,13 @@ class R2BackupAgent(BackupAgent):
self,
tar_filename: str,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
):
on_progress: OnProgressCallback,
) -> None:
"""Upload a large file using multipart upload.
:param tar_filename: The target filename for the backup.
:param open_stream: A function returning an async iterator that yields bytes.
:param on_progress: A callback to report the number of uploaded bytes.
"""
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
key = self._with_prefix(tar_filename)
@@ -203,6 +205,7 @@ class R2BackupAgent(BackupAgent):
part_number = 1
buffer = bytearray() # bytes buffer to store the data
offset = 0 # start index of unread data inside buffer
bytes_uploaded = 0
stream = await open_stream()
async for chunk in stream:
@@ -231,6 +234,8 @@ class R2BackupAgent(BackupAgent):
Body=part_data.tobytes(),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
bytes_uploaded += len(part_data)
on_progress(bytes_uploaded=bytes_uploaded)
part_number += 1
finally:
view.release()
@@ -259,6 +264,8 @@ class R2BackupAgent(BackupAgent):
Body=remaining_data.tobytes(),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
bytes_uploaded += len(remaining_data)
on_progress(bytes_uploaded=bytes_uploaded)
await cast(Any, self._client).complete_multipart_upload(
Bucket=self._bucket,

View File

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

View File

@@ -12,5 +12,22 @@
"set_value": {
"service": "mdi:counter"
}
},
"triggers": {
"decremented": {
"trigger": "mdi:numeric-negative-1"
},
"incremented": {
"trigger": "mdi:numeric-positive-1"
},
"maximum_reached": {
"trigger": "mdi:sort-numeric-ascending-variant"
},
"minimum_reached": {
"trigger": "mdi:sort-numeric-descending-variant"
},
"reset": {
"trigger": "mdi:refresh"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted counters to trigger on.",
"trigger_behavior_name": "Behavior"
},
"entity_component": {
"_": {
"name": "[%key:component::counter::title%]",
@@ -25,6 +29,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"decrement": {
"description": "Decrements a counter by its step size.",
@@ -49,5 +62,45 @@
"name": "Set"
}
},
"title": "Counter"
"title": "Counter",
"triggers": {
"decremented": {
"description": "Triggers after one or more counters decrement.",
"name": "Counter decremented"
},
"incremented": {
"description": "Triggers after one or more counters increment.",
"name": "Counter incremented"
},
"maximum_reached": {
"description": "Triggers after one or more counters reach their maximum value.",
"fields": {
"behavior": {
"description": "[%key:component::counter::common::trigger_behavior_description%]",
"name": "[%key:component::counter::common::trigger_behavior_name%]"
}
},
"name": "Counter reached maximum"
},
"minimum_reached": {
"description": "Triggers after one or more counters reach their minimum value.",
"fields": {
"behavior": {
"description": "[%key:component::counter::common::trigger_behavior_description%]",
"name": "[%key:component::counter::common::trigger_behavior_name%]"
}
},
"name": "Counter reached minimum"
},
"reset": {
"description": "Triggers after one or more counters are reset.",
"fields": {
"behavior": {
"description": "[%key:component::counter::common::trigger_behavior_description%]",
"name": "[%key:component::counter::common::trigger_behavior_name%]"
}
},
"name": "Counter reset"
}
}
}

View File

@@ -0,0 +1,113 @@
"""Provides triggers for counters."""
from homeassistant.const import (
CONF_MAXIMUM,
CONF_MINIMUM,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
)
from . import CONF_INITIAL, DOMAIN
def _is_integer_state(state: State) -> bool:
"""Return True if the state's value can be interpreted as an integer."""
try:
int(state.state)
except TypeError, ValueError:
return False
return True
class CounterBaseIntegerTrigger(EntityTriggerBase):
"""Base trigger for valid counter integer states."""
_domain_specs = {DOMAIN: DomainSpec()}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_state(self, state: State) -> bool:
"""Check if the new state is valid."""
return _is_integer_state(state)
class CounterDecrementedTrigger(CounterBaseIntegerTrigger):
"""Trigger for when a counter is decremented."""
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return int(from_state.state) > int(to_state.state)
class CounterIncrementedTrigger(CounterBaseIntegerTrigger):
"""Trigger for when a counter is incremented."""
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return int(from_state.state) < int(to_state.state)
class CounterValueBaseTrigger(EntityTriggerBase):
"""Base trigger for counter value changes."""
_domain_specs = {DOMAIN: DomainSpec()}
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
return from_state.state != to_state.state
class CounterMaxReachedTrigger(CounterValueBaseTrigger):
"""Trigger for when a counter reaches its maximum value."""
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
return False
return state.state == str(max_value)
class CounterMinReachedTrigger(CounterValueBaseTrigger):
"""Trigger for when a counter reaches its minimum value."""
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
return False
return state.state == str(min_value)
class CounterResetTrigger(CounterValueBaseTrigger):
"""Trigger for reset of counter entities."""
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state(s)."""
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
return False
return state.state == str(init_state)
TRIGGERS: dict[str, type[Trigger]] = {
"decremented": CounterDecrementedTrigger,
"incremented": CounterIncrementedTrigger,
"maximum_reached": CounterMaxReachedTrigger,
"minimum_reached": CounterMinReachedTrigger,
"reset": CounterResetTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for counters."""
return TRIGGERS

View File

@@ -1,18 +1,27 @@
.trigger_common: &trigger_common
target:
entity:
domain: input_boolean
domain: counter
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
translation_key: trigger_behavior
turned_off: *trigger_common
turned_on: *trigger_common
incremented:
target:
entity:
domain: counter
decremented:
target:
entity:
domain: counter
maximum_reached: *trigger_common
minimum_reached: *trigger_common
reset: *trigger_common

View File

@@ -32,6 +32,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.hass_dict import HassKey
from .condition import make_cover_is_closed_condition, make_cover_is_open_condition
from .const import (
ATTR_CURRENT_POSITION,
ATTR_CURRENT_TILT_POSITION,
@@ -80,6 +81,8 @@ __all__ = [
"CoverEntityFeature",
"CoverState",
"make_cover_closed_trigger",
"make_cover_is_closed_condition",
"make_cover_is_open_condition",
"make_cover_opened_trigger",
]

View File

@@ -1,7 +1,7 @@
"""Provides conditions for covers."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.condition import Condition, EntityConditionBase
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
@@ -13,7 +13,7 @@ class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches the expected cover state."""
domain_spec = self._domain_specs[split_entity_id(entity_state.entity_id)[0]]
domain_spec = self._domain_specs[entity_state.domain]
if domain_spec.value_source is not None:
return (
entity_state.attributes.get(domain_spec.value_source)

View File

@@ -208,19 +208,19 @@
"services": {
"close_cover": {
"description": "Closes a cover.",
"name": "[%key:common::action::close%]"
"name": "Close cover"
},
"close_cover_tilt": {
"description": "Tilts a cover to close.",
"name": "Close tilt"
"name": "Close cover tilt"
},
"open_cover": {
"description": "Opens a cover.",
"name": "[%key:common::action::open%]"
"name": "Open cover"
},
"open_cover_tilt": {
"description": "Tilts a cover open.",
"name": "Open tilt"
"name": "Open cover tilt"
},
"set_cover_position": {
"description": "Moves a cover to a specific position.",
@@ -230,7 +230,7 @@
"name": "Position"
}
},
"name": "Set position"
"name": "Set cover position"
},
"set_cover_tilt_position": {
"description": "Moves a cover tilt to a specific position.",
@@ -240,23 +240,23 @@
"name": "Tilt position"
}
},
"name": "Set tilt position"
"name": "Set cover tilt position"
},
"stop_cover": {
"description": "Stops the cover movement.",
"name": "[%key:common::action::stop%]"
"description": "Stops a cover's movement.",
"name": "Stop cover"
},
"stop_cover_tilt": {
"description": "Stops a tilting cover movement.",
"name": "Stop tilt"
"name": "Stop cover tilt"
},
"toggle": {
"description": "Toggles a cover open/closed.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle cover"
},
"toggle_cover_tilt": {
"description": "Toggles a cover tilt open/closed.",
"name": "Toggle tilt"
"name": "Toggle cover tilt"
}
},
"title": "Cover",

View File

@@ -1,7 +1,7 @@
"""Provides triggers for covers."""
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
@@ -13,14 +13,14 @@ class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
def _get_value(self, state: State) -> str | bool | None:
"""Extract the relevant value from state based on domain spec."""
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is not None:
return state.attributes.get(domain_spec.value_source)
return state.state
def is_valid_state(self, state: State) -> bool:
"""Check if the state matches the target cover state."""
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
domain_spec = self._domain_specs[state.domain]
return self._get_value(state) == domain_spec.target_value
def is_valid_transition(self, from_state: State, to_state: State) -> bool:

View File

@@ -9,9 +9,12 @@ from typing import Any
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_utc_time_change
from . import DOMAIN
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
@@ -23,10 +26,10 @@ async def async_setup_entry(
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
DemoValve("Back Garden", ValveState.CLOSED, position=70),
DemoValve("Trees", ValveState.CLOSED, position=30),
DemoValve("valve_1", "Front Garden", ValveState.OPEN),
DemoValve("valve_2", "Orchard", ValveState.CLOSED),
DemoValve("valve_3", "Back Garden", ValveState.CLOSED, position=70),
DemoValve("valve_4", "Trees", ValveState.CLOSED, position=30),
]
)
@@ -34,17 +37,24 @@ async def async_setup_entry(
class DemoValve(ValveEntity):
"""Representation of a Demo valve."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
def __init__(
self,
unique_id: str,
name: str,
state: str,
moveable: bool = True,
position: int | None = None,
) -> None:
"""Initialize the valve."""
self._attr_name = name
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE

View File

@@ -61,7 +61,7 @@
"services": {
"reload": {
"description": "Reloads derivative sensors from the YAML-configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload derivative sensors"
}
},
"title": "Derivative sensor"

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