Compare commits

..

385 Commits

Author SHA1 Message Date
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
Franck Nijhof
c1bd83c9c0 2026.3.3 (#166076) 2026-03-20 23:01:26 +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
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
737 changed files with 42590 additions and 5115 deletions

View File

@@ -224,7 +224,6 @@ jobs:
matrix:
machine:
- generic-x86-64
- intel-nuc
- khadas-vim3
- odroid-c2
- odroid-c4
@@ -248,10 +247,6 @@ 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

View File

@@ -274,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.*
@@ -327,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.*

16
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
@@ -737,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
@@ -784,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
@@ -943,6 +949,8 @@ 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
@@ -1071,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
@@ -1309,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
@@ -1594,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
@@ -1750,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

2
Dockerfile generated
View File

@@ -29,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,17 @@ 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",
}

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

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

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

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

@@ -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."""
@@ -1680,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,34 +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",
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"lawn_mower",
"light",
"lock",
"media_player",
"motion",
"occupancy",
"person",
"schedule",
"siren",
"switch",
"vacuum",
"window",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"button",
"battery",
"climate",
"cover",
"device_tracker",
@@ -155,6 +131,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"gate",
"humidifier",
"humidity",
"illuminance",
"lawn_mower",
"light",
"lock",
@@ -162,6 +139,42 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"motion",
"occupancy",
"person",
"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",
@@ -172,6 +185,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"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

@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
"home-assistant_v2.db-wal",
]
SECURETAR_CREATE_VERSION = 3
SECURETAR_CREATE_VERSION = 2

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

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

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

@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [Platform.LIGHT]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:

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,17 @@
{
"entity": {
"binary_sensor": {
"paused": {
"default": "mdi:timer-pause"
}
},
"button": {
"pause": {
"default": "mdi:pause"
},
"resume": {
"default": "mdi:play"
}
}
}
}

View File

@@ -14,6 +14,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pycasperglow"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pycasperglow==1.1.0"]
}

View File

@@ -32,7 +32,9 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow:
status: exempt
comment: Bluetooth device with no authentication credentials.
test-coverage: done
# Gold
@@ -53,15 +55,9 @@ rules:
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations:
status: exempt
comment: No entity translations needed.
exception-translations:
status: exempt
comment: No custom services that raise exceptions.
icon-translations:
status: exempt
comment: No icon translations needed.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

View File

@@ -26,6 +26,21 @@
}
}
},
"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

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

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

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

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

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

View File

@@ -48,7 +48,12 @@ def async_redact_data[_T](data: _T, to_redact: Iterable[Any]) -> _T:
def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in ("_cache", "compat_aliases", "compat_name")
return a.name not in (
"_cache",
"compat_aliases",
"compat_name",
"original_name_unprefixed",
)
@callback

View File

@@ -28,6 +28,7 @@ from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigEntry,
ConfigEntryState,
ConfigFlow,
ConfigFlowResult,
FlowType,
@@ -363,6 +364,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Don't probe to verify the mac is correct since
# the host matches (and port matches if provided).
raise AbortFlow("already_configured")
# If the entry is loaded and the device is currently connected,
# don't update the host. This prevents transient mDNS announcements
# (e.g., during WiFi mesh roaming) from overwriting a working connection.
if entry.state is ConfigEntryState.LOADED and entry.runtime_data.available:
raise AbortFlow("already_configured")
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
await self._fetch_device_info(host, port or configured_port, configured_psk)
updates: dict[str, Any] = {}

View File

@@ -1,10 +1,12 @@
"""HTTP view that converts audio from a URL to a preferred format."""
import asyncio
from collections import defaultdict
from collections import defaultdict, deque
import contextlib
from dataclasses import dataclass, field
from http import HTTPStatus
import logging
import re
import secrets
from typing import Final
@@ -22,6 +24,12 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
_MAX_STDERR_LINES: Final[int] = 64
_PROC_WAIT_TIMEOUT: Final[int] = 5
_STDERR_DRAIN_TIMEOUT: Final[int] = 1
_SENSITIVE_QUERY_PARAMS: Final[re.Pattern[str]] = re.compile(
r"(?<=[?&])(authSig|token|key|password|secret)=[^&\s]+", re.IGNORECASE
)
@callback
@@ -215,8 +223,10 @@ class FFmpegConvertResponse(web.StreamResponse):
assert proc.stdout is not None
assert proc.stderr is not None
stderr_lines: deque[str] = deque(maxlen=_MAX_STDERR_LINES)
stderr_task = self.hass.async_create_background_task(
self._dump_ffmpeg_stderr(proc), "ESPHome media proxy dump stderr"
self._collect_ffmpeg_stderr(proc, stderr_lines),
"ESPHome media proxy dump stderr",
)
try:
@@ -235,33 +245,80 @@ class FFmpegConvertResponse(web.StreamResponse):
if request.transport:
request.transport.abort()
raise # don't log error
except:
except Exception:
_LOGGER.exception("Unexpected error during ffmpeg conversion")
raise
finally:
# Allow conversion info to be removed
self.convert_info.is_finished = True
# stop dumping ffmpeg stderr task
stderr_task.cancel()
# Ensure subprocess and stderr cleanup run even if this task
# is cancelled (e.g., during shutdown)
try:
# Terminate hangs, so kill is used
if proc.returncode is None:
proc.kill()
# Terminate hangs, so kill is used
if proc.returncode is None:
proc.kill()
# Wait for process to exit so returncode is set
await asyncio.wait_for(proc.wait(), timeout=_PROC_WAIT_TIMEOUT)
# Let stderr collector finish draining
if not stderr_task.done():
try:
await asyncio.wait_for(
stderr_task, timeout=_STDERR_DRAIN_TIMEOUT
)
except TimeoutError:
stderr_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await stderr_task
except TimeoutError:
_LOGGER.warning(
"Timed out waiting for ffmpeg process to exit for device %s",
self.device_id,
)
stderr_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await stderr_task
except asyncio.CancelledError:
# Kill the process if we were interrupted
if proc.returncode is None:
proc.kill()
stderr_task.cancel()
raise
if proc.returncode is not None and proc.returncode > 0:
_LOGGER.error(
"FFmpeg conversion failed for device %s (return code %s):\n%s",
self.device_id,
proc.returncode,
"\n".join(
_SENSITIVE_QUERY_PARAMS.sub(r"\1=REDACTED", line)
for line in stderr_lines
),
)
# Close connection by writing EOF unless already closing
if request.transport and not request.transport.is_closing():
await writer.write_eof()
with contextlib.suppress(ConnectionResetError, RuntimeError, OSError):
await writer.write_eof()
async def _dump_ffmpeg_stderr(
async def _collect_ffmpeg_stderr(
self,
proc: asyncio.subprocess.Process,
stderr_lines: deque[str],
) -> None:
assert proc.stdout is not None
"""Collect stderr output from ffmpeg for error reporting."""
assert proc.stderr is not None
while self.hass.is_running and (chunk := await proc.stderr.readline()):
_LOGGER.debug("ffmpeg[%s] output: %s", proc.pid, chunk.decode().rstrip())
line = chunk.decode(errors="replace").rstrip()
stderr_lines.append(line)
_LOGGER.debug(
"ffmpeg[%s] output: %s",
proc.pid,
_SENSITIVE_QUERY_PARAMS.sub(r"\1=REDACTED", line),
)
class FFmpegProxyView(HomeAssistantView):

View File

@@ -12,5 +12,10 @@
"motion": {
"default": "mdi:motion-sensor"
}
},
"triggers": {
"received": {
"trigger": "mdi:eye-check"
}
}
}

View File

@@ -21,5 +21,17 @@
"name": "Motion"
}
},
"title": "Event"
"title": "Event",
"triggers": {
"received": {
"description": "Triggers after one or more event entities receive a matching event.",
"fields": {
"event_type": {
"description": "The event types to trigger on.",
"name": "Event type"
}
},
"name": "Event received"
}
}
}

View File

@@ -0,0 +1,67 @@
"""Provides triggers for events."""
import voluptuous as vol
from homeassistant.const import CONF_OPTIONS, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA,
EntityTriggerBase,
Trigger,
TriggerConfig,
)
from .const import ATTR_EVENT_TYPE, DOMAIN
CONF_EVENT_TYPE = "event_type"
EVENT_RECEIVED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_EVENT_TYPE): vol.All(
cv.ensure_list, vol.Length(min=1), [cv.string]
),
},
}
)
class EventReceivedTrigger(EntityTriggerBase):
"""Trigger for event entity when it receives a matching event."""
_domain_specs = {DOMAIN: DomainSpec()}
_schema = EVENT_RECEIVED_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize the event received trigger."""
super().__init__(hass, config)
self._event_types = set(self._options[CONF_EVENT_TYPE])
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and different from the current state."""
# UNKNOWN is a valid from_state, otherwise the first time the event is received
# would not trigger
if from_state.state == STATE_UNAVAILABLE:
return False
return from_state.state != to_state.state
def is_valid_state(self, state: State) -> bool:
"""Check if the event type is valid and matches one of the configured types."""
return (
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
and state.attributes.get(ATTR_EVENT_TYPE) in self._event_types
)
TRIGGERS: dict[str, type[Trigger]] = {
"received": EventReceivedTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for events."""
return TRIGGERS

View File

@@ -0,0 +1,16 @@
received:
target:
entity:
domain: event
fields:
event_type:
context:
filter_target: target
required: true
selector:
state:
attribute: event_type
hide_states:
- unavailable
- unknown
multiple: true

View File

@@ -10,7 +10,8 @@ set_preset_mode:
required: true
example: "auto"
selector:
text:
state:
attribute: preset_mode
set_percentage:
target:
@@ -49,7 +50,8 @@ turn_on:
supported_features:
- fan.FanEntityFeature.PRESET_MODE
selector:
text:
state:
attribute: preset_mode
turn_off:
target:

View File

@@ -118,7 +118,7 @@
"name": "Decrement"
}
},
"name": "Decrease speed"
"name": "Decrease fan speed"
},
"increase_speed": {
"description": "Increases the speed of a fan.",
@@ -128,7 +128,7 @@
"name": "Increment"
}
},
"name": "Increase speed"
"name": "Increase fan speed"
},
"oscillate": {
"description": "Controls the oscillation of a fan.",
@@ -138,7 +138,7 @@
"name": "Oscillating"
}
},
"name": "Oscillate"
"name": "Oscillate fan"
},
"set_direction": {
"description": "Sets a fan's rotation direction.",
@@ -148,7 +148,7 @@
"name": "Direction"
}
},
"name": "Set direction"
"name": "Set fan direction"
},
"set_percentage": {
"description": "Sets the speed of a fan.",
@@ -158,28 +158,28 @@
"name": "Percentage"
}
},
"name": "Set speed"
"name": "Set fan speed"
},
"set_preset_mode": {
"description": "Sets preset fan mode.",
"description": "Sets the preset mode of a fan.",
"fields": {
"preset_mode": {
"description": "Preset fan mode.",
"name": "Preset mode"
}
},
"name": "Set preset mode"
"name": "Set fan preset mode"
},
"toggle": {
"description": "Toggles a fan on/off.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle fan"
},
"turn_off": {
"description": "Turns fan off.",
"name": "[%key:common::action::turn_off%]"
"description": "Turns off a fan.",
"name": "Turn off fan"
},
"turn_on": {
"description": "Turns fan on.",
"description": "Turns on a fan.",
"fields": {
"percentage": {
"description": "[%key:component::fan::services::set_percentage::fields::percentage::description%]",
@@ -190,7 +190,7 @@
"name": "[%key:component::fan::services::set_preset_mode::fields::preset_mode::name%]"
}
},
"name": "[%key:common::action::turn_on%]"
"name": "Turn on fan"
}
},
"title": "Fan",

View File

@@ -242,7 +242,7 @@
"services": {
"reload": {
"description": "Reloads filters from the YAML-configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload filters"
}
},
"title": "Filter"

View File

@@ -36,10 +36,10 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=5)
class FireflyCoordinatorData:
"""Data structure for Firefly III coordinator data."""
accounts: list[Account]
accounts: dict[str, Account]
categories: list[Category]
category_details: list[Category]
budgets: list[Budget]
category_details: dict[str, Category]
budgets: dict[str, Budget]
bills: list[Bill]
primary_currency: Currency
@@ -142,10 +142,10 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
) from err
return FireflyCoordinatorData(
accounts=accounts,
accounts={account.id: account for account in accounts},
categories=categories,
category_details=category_details,
budgets=budgets,
category_details={category.id: category for category in category_details},
budgets={budget.id: budget for budget in budgets},
bills=bills,
primary_currency=primary_currency,
)

View File

@@ -44,7 +44,7 @@ class FireflyAccountBaseEntity(FireflyBaseEntity):
) -> None:
"""Initialize a Firefly account entity."""
super().__init__(coordinator)
self._account = account
self._account_id = account.id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
@@ -58,6 +58,10 @@ class FireflyAccountBaseEntity(FireflyBaseEntity):
f"{coordinator.config_entry.unique_id}_account_{account.id}_{key}"
)
@property
def _account(self) -> Account:
return self.coordinator.data.accounts[self._account_id]
class FireflyCategoryBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III category entity."""
@@ -70,7 +74,7 @@ class FireflyCategoryBaseEntity(FireflyBaseEntity):
) -> None:
"""Initialize a Firefly category entity."""
super().__init__(coordinator)
self._category = category
self._category_id = category.id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
@@ -84,6 +88,10 @@ class FireflyCategoryBaseEntity(FireflyBaseEntity):
f"{coordinator.config_entry.unique_id}_category_{category.id}_{key}"
)
@property
def _category(self) -> Category:
return self.coordinator.data.category_details[self._category_id]
class FireflyBudgetBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III budget entity."""
@@ -96,7 +104,7 @@ class FireflyBudgetBaseEntity(FireflyBaseEntity):
) -> None:
"""Initialize a Firefly budget entity."""
super().__init__(coordinator)
self._budget = budget
self._budget_id = budget.id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
@@ -109,3 +117,7 @@ class FireflyBudgetBaseEntity(FireflyBaseEntity):
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_budget_{budget.id}_{key}"
)
@property
def _budget(self) -> Budget:
return self.coordinator.data.budgets[self._budget_id]

View File

@@ -51,7 +51,7 @@ async def async_setup_entry(
coordinator = entry.runtime_data
entities: list[SensorEntity] = []
for account in coordinator.data.accounts:
for account in coordinator.data.accounts.values():
entities.append(
FireflyAccountBalanceSensor(coordinator, account, ACCOUNT_BALANCE)
)
@@ -61,14 +61,14 @@ async def async_setup_entry(
entities.extend(
[
FireflyCategorySensor(coordinator, category, CATEGORY)
for category in coordinator.data.category_details
for category in coordinator.data.category_details.values()
]
)
entities.extend(
[
FireflyBudgetSensor(coordinator, budget, BUDGET)
for budget in coordinator.data.budgets
for budget in coordinator.data.budgets.values()
]
)
@@ -90,7 +90,6 @@ class FireflyAccountBalanceSensor(FireflyAccountBaseEntity, SensorEntity):
) -> None:
"""Initialize the account balance sensor."""
super().__init__(coordinator, account, key)
self._account = account
self._attr_native_unit_of_measurement = (
coordinator.data.primary_currency.attributes.code
)
@@ -108,16 +107,6 @@ class FireflyAccountRoleSensor(FireflyAccountBaseEntity, SensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = True
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
account: Account,
key: str,
) -> None:
"""Initialize the account role sensor."""
super().__init__(coordinator, account, key)
self._account = account
@property
def native_value(self) -> StateType:
"""Return account role."""
@@ -173,7 +162,6 @@ class FireflyCategorySensor(FireflyCategoryBaseEntity, SensorEntity):
) -> None:
"""Initialize the category sensor."""
super().__init__(coordinator, category, key)
self._category = category
self._attr_native_unit_of_measurement = (
coordinator.data.primary_currency.attributes.code
)
@@ -205,7 +193,6 @@ class FireflyBudgetSensor(FireflyBudgetBaseEntity, SensorEntity):
) -> None:
"""Initialize the budget sensor."""
super().__init__(coordinator, budget, key)
self._budget = budget
self._attr_native_unit_of_measurement = (
coordinator.data.primary_currency.attributes.code
)

View File

@@ -20,5 +20,7 @@ async def async_get_solar_forecast(
"wh_hours": {
timestamp.isoformat(): val
for timestamp, val in entry.runtime_data.data.wh_period.items()
if val != 0
or (timestamp.hour, timestamp.minute, timestamp.second) != (0, 0, 0)
}
}

View File

@@ -26,7 +26,7 @@ from .const import (
MeshRoles,
)
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
from .entity import FritzBoxBaseEntity, FritzDeviceBase
from .entity import FritzBoxBaseEntity
from .helpers import device_filter_out_from_trackers
from .models import FritzDevice, SwitchInfo
@@ -332,7 +332,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity):
@property
def icon(self) -> str:
"""Return name."""
"""Return icon."""
return self._icon
@property
@@ -485,42 +485,51 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
self.async_write_ha_state()
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
class FritzBoxProfileSwitch(FritzBoxBaseCoordinatorSwitch):
"""Defines a FRITZ!Box Tools DeviceProfile switch."""
_attr_translation_key = "internet_access"
_attr_entity_category = EntityCategory.CONFIG
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
"""Init Fritz profile."""
super().__init__(avm_wrapper, device)
self._attr_is_on: bool = False
self._mac = device.mac_address
description = SwitchEntityDescription(
key=f"{self._mac}_internet_access",
)
super().__init__(avm_wrapper, device.hostname, description)
self._attr_unique_id = f"{self._mac}_internet_access"
self._attr_entity_category = EntityCategory.CONFIG
@property
def is_on(self) -> bool | None:
"""Switch status."""
return self._avm_wrapper.devices[self._mac].wan_access
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, self._mac)},
)
@property
def _device(self) -> FritzDevice:
"""Return the device for this profile switch."""
return self.coordinator.devices[self._mac]
@property
def available(self) -> bool:
"""Return availability of the switch."""
if self._avm_wrapper.devices[self._mac].wan_access is None:
if self._device.wan_access is None:
return False
return super().available
return self.coordinator.last_update_success
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on switch."""
await self._async_handle_turn_on_off(turn_on=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
@property
def is_on(self) -> bool | None:
"""Switch status."""
return self._device.wan_access
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
"""Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
self._avm_wrapper.devices[self._mac].wan_access = turn_on
await self.coordinator.async_set_allow_wan_access(
self._device.ip_address, turn_on
)
self._device.wan_access = turn_on
self.async_write_ha_state()

View File

@@ -13,5 +13,5 @@
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",
"requirements": ["PyFronius==0.8.0"]
"requirements": ["PyFronius==0.8.2"]
}

View File

@@ -93,7 +93,7 @@
"services": {
"reload": {
"description": "Reloads generic thermostats from the YAML-configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload generic thermostats"
}
},
"title": "Generic thermostat"

View File

@@ -113,7 +113,7 @@ class GoogleTravelTimeSensor(SensorEntity):
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, api_key)},
name=DOMAIN,
name=DEFAULT_NAME,
)
self._config_entry = config_entry

View File

@@ -98,7 +98,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
"""Set light color in kelvin."""
await device.set_temperature(temperature)
async def set_scene(self, device: GoveeController, scene: str) -> None:
async def set_scene(self, device: GoveeDevice, scene: str) -> None:
"""Set light scene."""
await device.set_scene(scene)

View File

@@ -80,7 +80,6 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
super().__init__(coordinator)
self._device = device
device.set_update_callback(self._update_callback)
self._attr_unique_id = device.fingerprint
@@ -194,9 +193,20 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
await self.coordinator.turn_off(self._device)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register update callback when entity is added."""
await super().async_added_to_hass()
self._device.set_update_callback(self._update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Unregister update callback when entity is removed."""
self._device.set_update_callback(None)
await super().async_will_remove_from_hass()
@callback
def _update_callback(self, device: GoveeDevice) -> None:
self.async_write_ha_state()
if self.hass:
self.async_write_ha_state()
def _save_last_color_state(self) -> None:
color_mode = self.color_mode

View File

@@ -666,7 +666,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
# Init add-on ingress panels
panels_task = hass.async_create_task(
async_setup_addon_panel(hass, hassio), eager_start=True
async_setup_addon_panel(hass), eager_start=True
)
# Make sure to await the update_info task before

View File

@@ -2,24 +2,23 @@
from http import HTTPStatus
import logging
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import IngressPanel
from aiohttp import web
from homeassistant.components import frontend
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_ICON
from homeassistant.core import HomeAssistant
from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE
from .handler import HassIO, HassioAPIError
from .handler import get_supervisor_client
_LOGGER = logging.getLogger(__name__)
async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
async def async_setup_addon_panel(hass: HomeAssistant) -> None:
"""Add-on Ingress Panel setup."""
hassio_addon_panel = HassIOAddonPanel(hass, hassio)
hassio_addon_panel = HassIOAddonPanel(hass)
hass.http.register_view(hassio_addon_panel)
# If panels are exists
@@ -28,11 +27,8 @@ async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
# Register available panels
for addon, data in panels.items():
if not data[ATTR_ENABLE]:
if not data.enable:
continue
# _register_panel never suspends and is only
# a coroutine because it would be a breaking change
# to make it a normal function
_register_panel(hass, addon, data)
@@ -42,23 +38,22 @@ class HassIOAddonPanel(HomeAssistantView):
name = "api:hassio_push:panel"
url = "/api/hassio_push/panel/{addon}"
def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize WebView."""
self.hass = hass
self.hassio = hassio
self.client = get_supervisor_client(hass)
async def post(self, request: web.Request, addon: str) -> web.Response:
"""Handle new add-on panel requests."""
panels = await self.get_panels()
# Panel exists for add-on slug
if addon not in panels or not panels[addon][ATTR_ENABLE]:
_LOGGER.error("Panel is not enable for %s", addon)
if addon not in panels or not panels[addon].enable:
_LOGGER.error("Panel is not enabled for %s", addon)
return web.Response(status=HTTPStatus.BAD_REQUEST)
data = panels[addon]
# Register panel
_register_panel(self.hass, addon, data)
_register_panel(self.hass, addon, panels[addon])
return web.Response()
async def delete(self, request: web.Request, addon: str) -> web.Response:
@@ -66,24 +61,23 @@ class HassIOAddonPanel(HomeAssistantView):
frontend.async_remove_panel(self.hass, addon)
return web.Response()
async def get_panels(self) -> dict:
async def get_panels(self) -> dict[str, IngressPanel]:
"""Return panels add-on info data."""
try:
data = await self.hassio.get_ingress_panels()
return data[ATTR_PANELS]
except HassioAPIError as err:
return await self.client.ingress.panels()
except SupervisorError as err:
_LOGGER.error("Can't read panel info: %s", err)
return {}
def _register_panel(hass: HomeAssistant, addon: str, data: dict[str, Any]):
"""Init coroutine to register the panel."""
def _register_panel(hass: HomeAssistant, addon: str, data: IngressPanel):
"""Helper to register the panel."""
frontend.async_register_built_in_panel(
hass,
"app",
frontend_url_path=addon,
sidebar_title=data[ATTR_TITLE],
sidebar_icon=data[ATTR_ICON],
require_admin=data[ATTR_ADMIN],
sidebar_title=data.title,
sidebar_icon=data.icon,
require_admin=data.admin,
config={"addon": addon},
)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from http import HTTPStatus
import logging
import os
@@ -28,21 +27,6 @@ class HassioAPIError(RuntimeError):
"""Return if a API trow a error."""
def api_data[**_P](
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
) -> Callable[_P, Coroutine[Any, Any, Any]]:
"""Return data of an api."""
async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> Any:
"""Wrap function."""
data = await funct(*argv, **kwargs)
if data["result"] == "ok":
return data["data"]
raise HassioAPIError(data["message"])
return _wrapper
class HassIO:
"""Small API wrapper for Hass.io."""
@@ -64,14 +48,6 @@ class HassIO:
"""Return base url for Supervisor."""
return self._base_url
@api_data
def get_ingress_panels(self) -> Coroutine:
"""Return data for Add-on ingress panels.
This method returns a coroutine.
"""
return self.send_command("/ingress/panels", method="get")
async def send_command(
self,
command: str,

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["aiohasupervisor==0.4.2"],
"requirements": ["aiohasupervisor==0.4.3"],
"single_config_entry": true
}

View File

@@ -211,6 +211,10 @@
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Docker misconfigured"
},
"unhealthy_docker_gateway_unprotected": {
"description": "System is currently unhealthy because Supervisor was not able to apply firewall protection for the Docker gateway IP. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Docker gateway unprotected"
},
"unhealthy_duplicate_os_installation": {
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Duplicate Home Assistant OS installation"

View File

@@ -124,7 +124,7 @@
"services": {
"reload": {
"description": "Reloads history stats sensors from the YAML-configuration.",
"name": "[%key:common::action::reload%]"
"name": "Reload history stats sensors"
}
},
"title": "History Stats"

View File

@@ -312,6 +312,7 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
case EventType.NOTIFY:
settings = self.data.settings
events = self.data.events
program_update_event_value = None
for event in event_message.data.items:
event_key = event.key
if event_key in SettingKey.__members__.values(): # type: ignore[comparison-overlap]
@@ -330,11 +331,13 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
) and isinstance(event_value, str):
await self.update_options(
event_key,
ProgramKey(event_value),
)
program_update_event_value = ProgramKey(event_value)
events[event_key] = event
# Process program update after all events to ensure
# BSH_COMMON_OPTION_BASE_PROGRAM event is available for
# favorite program resolution
if program_update_event_value:
await self.update_options(program_update_event_value)
self._call_event_listener(event_message)
case EventType.EVENT:
@@ -493,7 +496,7 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
programs = []
events = {}
options = {}
if appliance.type in APPLIANCES_WITH_PROGRAMS:
if appliance.type in APPLIANCES_WITH_PROGRAMS: # pylint: disable=too-many-nested-blocks
try:
all_programs = await self.client.get_all_programs(appliance.ha_id)
except TooManyRequestsError:
@@ -529,6 +532,17 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
)
current_program_key = program.key
program_options = program.options
if (
current_program_key == ProgramKey.BSH_COMMON_FAVORITE_001
and program_options
):
# The API doesn't allow to fetch the options from the favorite program.
# We can attempt to get the base program and get the options
for option in program_options:
if option.key == OptionKey.BSH_COMMON_BASE_PROGRAM:
current_program_key = ProgramKey(option.value)
break
if current_program_key:
options = await self.get_options_definitions(current_program_key)
for option in program_options or []:
@@ -595,15 +609,24 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
)
return {}
async def update_options(
self, event_key: EventKey, program_key: ProgramKey
) -> None:
async def update_options(self, program_key: ProgramKey) -> None:
"""Update options for appliance."""
options = self.data.options
events = self.data.events
options_to_notify = options.copy()
options.clear()
options.update(await self.get_options_definitions(program_key))
if (
program_key == ProgramKey.BSH_COMMON_FAVORITE_001
and (event := events.get(EventKey.BSH_COMMON_OPTION_BASE_PROGRAM))
and isinstance(event.value, str)
):
# The API doesn't allow to fetch the options from the favorite program.
# We can attempt to get the base program and get the options
resolved_program_key = ProgramKey(event.value)
else:
resolved_program_key = program_key
options.update(await self.get_options_definitions(resolved_program_key))
for option in options.values():
option_value = option.constraints.default if option.constraints else None

View File

@@ -430,11 +430,24 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
def update_native_value(self) -> None:
"""Set the program value."""
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
self._attr_current_option = (
PROGRAMS_TRANSLATION_KEYS_MAP.get(ProgramKey(event_value))
program_key = (
ProgramKey(event_value)
if event and isinstance(event_value := event.value, str)
else None
)
if (
program_key == ProgramKey.BSH_COMMON_FAVORITE_001
and (
base_program_event := self.appliance.events.get(
EventKey.BSH_COMMON_OPTION_BASE_PROGRAM
)
)
and isinstance(base_program_event.value, str)
):
program_key = ProgramKey(base_program_event.value)
self._attr_current_option = (
PROGRAMS_TRANSLATION_KEYS_MAP.get(program_key) if program_key else None
)
async def async_select_option(self, option: str) -> None:
"""Select new program."""

View File

@@ -208,11 +208,11 @@
"services": {
"check_config": {
"description": "Checks the Home Assistant YAML-configuration files for errors. Errors will be shown in the Home Assistant logs.",
"name": "Check configuration"
"name": "Check Home Assistant configuration"
},
"reload_all": {
"description": "Reloads all YAML configuration that can be reloaded without restarting Home Assistant.",
"name": "Reload all"
"name": "Reload all Home Assistant configuration"
},
"reload_config_entry": {
"description": "Reloads the specified config entry.",
@@ -240,7 +240,7 @@
"name": "Safe mode"
}
},
"name": "[%key:common::action::restart%]"
"name": "Restart Home Assistant"
},
"save_persistent_states": {
"description": "Saves the persistent states immediately. Maintains the normal periodic saving interval.",
@@ -262,11 +262,11 @@
"name": "[%key:common::config_flow::data::longitude%]"
}
},
"name": "Set location"
"name": "Set Home Assistant location"
},
"stop": {
"description": "Stops Home Assistant.",
"name": "[%key:common::action::stop%]"
"name": "Stop Home Assistant"
},
"toggle": {
"description": "Generic action to toggle devices on/off under any domain.",

View File

@@ -8,7 +8,7 @@
"integration_type": "system",
"requirements": [
"serialx==0.6.2",
"universal-silabs-flasher==1.0.2",
"universal-silabs-flasher==1.0.3",
"ha-silabs-firmware-client==0.3.0"
]
}

View File

@@ -0,0 +1,67 @@
"""The Qube Heat Pump integration."""
from __future__ import annotations
from dataclasses import dataclass
from python_qube_heatpump import QubeClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import PLATFORMS
from .coordinator import QubeCoordinator
@dataclass
class QubeData:
"""Runtime data for Qube Heat Pump."""
coordinator: QubeCoordinator
client: QubeClient
sw_version: str | None
type QubeConfigEntry = ConfigEntry[QubeData]
async def async_setup_entry(hass: HomeAssistant, entry: QubeConfigEntry) -> bool:
"""Set up Qube Heat Pump from a config entry."""
client = QubeClient(entry.data[CONF_HOST], entry.data[CONF_PORT])
# Connect and read software version for device info
sw_version: str | None = None
try:
connected = await client.connect()
if not connected:
await client.close()
raise ConfigEntryNotReady(
f"Unable to connect to Qube heat pump at {entry.data[CONF_HOST]}"
)
sw_version = await client.async_get_software_version()
except (OSError, TimeoutError) as err:
await client.close()
raise ConfigEntryNotReady(
f"Unable to connect to Qube heat pump at {entry.data[CONF_HOST]}"
) from err
coordinator = QubeCoordinator(hass, client, entry)
entry.runtime_data = QubeData(
coordinator=coordinator,
client=client,
sw_version=sw_version,
)
await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: QubeConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.client.close()
return unload_ok

View File

@@ -0,0 +1,61 @@
"""Config flow for Qube Heat Pump integration."""
from __future__ import annotations
from typing import Any
from python_qube_heatpump import QubeClient
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DEFAULT_PORT, DOMAIN
class QubeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Qube Heat Pump."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
self._async_abort_entries_match({CONF_HOST: host})
# Connect and verify it's a Qube by reading software version
client = QubeClient(host, DEFAULT_PORT)
try:
connected = await client.connect()
if not connected:
errors["base"] = "cannot_connect"
else:
version = await client.async_get_software_version()
if version is None:
errors["base"] = "not_qube_device"
except OSError, TimeoutError:
errors["base"] = "cannot_connect"
finally:
await client.close()
if not errors:
return self.async_create_entry(
title="Qube heat pump",
data={
CONF_HOST: host,
CONF_PORT: DEFAULT_PORT,
},
)
schema = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)

View File

@@ -0,0 +1,9 @@
"""Constants for the Qube Heat Pump integration."""
from homeassistant.const import Platform
DOMAIN = "hr_energy_qube"
PLATFORMS = (Platform.SENSOR,)
DEFAULT_PORT = 502
DEFAULT_SCAN_INTERVAL = 15

View File

@@ -0,0 +1,51 @@
"""DataUpdateCoordinator for Qube Heat Pump."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from python_qube_heatpump import QubeClient
from python_qube_heatpump.models import QubeState
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__)
class QubeCoordinator(DataUpdateCoordinator[QubeState]):
"""Qube Heat Pump data coordinator."""
def __init__(
self, hass: HomeAssistant, client: QubeClient, entry: ConfigEntry
) -> None:
"""Initialize the coordinator."""
self.client = client
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
config_entry=entry,
)
async def _async_update_data(self) -> QubeState:
"""Fetch data from the device."""
try:
data = await self.client.get_all_data()
except (ConnectionError, TimeoutError, OSError) as exc:
raise UpdateFailed(
f"Error communicating with Qube heat pump: {exc}"
) from exc
if data is None:
raise UpdateFailed("No data received from Qube heat pump")
return data

View File

@@ -0,0 +1,34 @@
"""Base entity for Qube Heat Pump."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import QubeCoordinator
if TYPE_CHECKING:
from . import QubeConfigEntry
class QubeEntity(CoordinatorEntity[QubeCoordinator]):
"""Base entity for Qube Heat Pump."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: QubeCoordinator,
entry: QubeConfigEntry,
) -> None:
"""Initialize the base entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Qube",
model="Heat Pump",
sw_version=entry.runtime_data.sw_version,
)

View File

@@ -0,0 +1,12 @@
{
"domain": "hr_energy_qube",
"name": "Qube heat pump",
"codeowners": ["@MattieGit"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hr_energy_qube",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["python_qube_heatpump"],
"quality_scale": "bronze",
"requirements": ["python-qube-heatpump==1.7.0"]
}

View File

@@ -0,0 +1,78 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Entities do not subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No configuration options beyond initial setup.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow:
status: exempt
comment: No authentication required for Modbus TCP.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Single device per config entry.
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices:
status: exempt
comment: Single device per config entry.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: Uses Modbus TCP, not HTTP.
strict-typing: done

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