Compare commits

...

316 Commits

Author SHA1 Message Date
prana-dev-official
0a2fc97696 Import improvement for Prana integration (#165805) 2026-03-17 16:28:53 +01:00
Joost Lekkerkerker
447d616097 Add select for SmartThings RVC sound mode (#164519) 2026-03-17 15:57:59 +01:00
Norbert Rittel
d3102e718d Consistenly sentence-case "API token" in habitica (#165369)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-17 14:30:39 +00:00
Josef Zweck
69ee49735a Remove support for homeassistant.update_entity from mold_indicator (#165797) 2026-03-17 15:26:22 +01:00
Daniel Hjelseth Høyer
35a99dd4a4 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-17 15:11:51 +01:00
Ariel Ebersberger
51c3397be8 Refactor wemo integration to use async service action handlers (#165794)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 15:07:00 +01:00
Brett Adams
57f0fd2ed2 Tesla Fleet: fix malformed energy live response handling (#165101) 2026-03-17 15:04:35 +01:00
Erik Montnemery
fa7a216afe Use return value from target_entities directly in condition tests (#165791) 2026-03-17 14:55:17 +01:00
Josef Zweck
20f4426e1d Fix mold_indicator sensor update (#158996) 2026-03-17 14:28:50 +01:00
Erik Montnemery
ba30563772 Deduplicate tests testing triggers in mode last (#165789) 2026-03-17 14:28:10 +01:00
A. Gideonse
b807c104a3 Add button platform to Indevolt integration (#165283) 2026-03-17 13:59:18 +01:00
epenet
9e6abb719a Add fixture for Kerui/Tuya video doorbell (#165786) 2026-03-17 13:57:28 +01:00
jvmahon
ed2083a60d Limit color temperature to maximum Matter MIREDs value (#163892)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:49:18 +00:00
Kornel
94db0d5eab Handle timeout in HKDevice.async_update (#162071)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-17 12:42:43 +00:00
dckiller51
06eed998b9 Add platform attribute to Xbox sensors (#161661) 2026-03-17 12:40:42 +00:00
Andrej Walilko
fb5c2f2566 Add shuffle service and enqueue support to jellyfin media player (#161632)
Co-authored-by: Andrej Walilko <awalilko@liquidweb.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:17:55 +00:00
Dominik
4f7d065230 Fix fritz target selector for dial and set_guest_wifi_password (#165396) 2026-03-17 13:15:51 +01:00
Erwin Douna
d034df9b93 Add Portainer request timeout (#165785) 2026-03-17 12:58:55 +01:00
Erik Montnemery
6c9fc7c7a1 Deduplicate tests testing triggers in mode first (#165779)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 12:44:21 +01:00
Leon Grave
ba58ef23d8 Add reauthentication-flow to freshr (#165545)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-17 12:22:54 +01:00
johanzander
0a0fa96ac1 Add silver quality scale for growatt_server (#165500) 2026-03-17 12:18:11 +01:00
Erik Montnemery
9cc7ef75b0 Move cover.trigger.CoverDomainSpec to cover.models (#165774) 2026-03-17 12:11:11 +01:00
Carlos Sánchez López
2e0d6d2bbf Add fixture for Tuya wg2 alarm panel (Duosmart C30) (#165701)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-17 11:34:53 +01:00
Artur Pragacz
bafef2065f Rework user-given entity name logic (#162763) 2026-03-17 11:09:20 +01:00
Erik Montnemery
fdfe87de4c Move condition/trigger test helpers to test.components.common (#165777) 2026-03-17 11:08:38 +01:00
epenet
933d123db3 Move xiaomi_miio coordinator to separate module (#165766) 2026-03-17 11:04:31 +01:00
Brett Adams
1f9946a1b8 Fix sensor reset handling in Tesla Fleet (#165744) 2026-03-17 10:59:35 +01:00
Josef Zweck
403e30b56e Add upload progress tracking to hassio (#165664) 2026-03-17 10:47:58 +01:00
Robert Resch
e4524d9b68 Run split tests in the same stage with mypy (#165738) 2026-03-17 10:45:55 +01:00
Ariel Ebersberger
738100c897 Fix wemo tests for Python 3.14.3 (#165768) 2026-03-17 10:40:54 +01:00
Erik Montnemery
67356de21b Deduplicate tests testing triggers in mode any (#165772) 2026-03-17 10:27:12 +01:00
Robert Resch
80c5bd1843 Bump pyOpenSSL to 26.0.0 (#165770) 2026-03-17 10:13:37 +01:00
Erik Montnemery
492883de57 Add cover conditions (#165661) 2026-03-17 10:11:16 +01:00
Samuel Xiao
45f1247237 Switchbot Cloud: Add new supported device(Standing Fan) (#165755) 2026-03-17 09:59:18 +01:00
Ville Skyttä
0e76d927cf Switch to actions/attest for build provenance (#165350) 2026-03-17 09:57:48 +01:00
Erik Montnemery
4769a769e0 Use return value from target_entities directly in all trigger tests (#165761) 2026-03-17 09:55:08 +01:00
J. Nick Koston
f2d62049ec Fix ESPHome cold/warm white brightness applied twice (#165405) 2026-03-17 09:24:21 +01:00
dependabot[bot]
751b2638ce Bump sigstore/cosign-installer from 4.0.0 to 4.1.0 (#165758)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 09:23:42 +01:00
Joakim Plate
120d3ee85a Add support for aqua contour/precise line of gardena products (#165326) 2026-03-17 08:32:17 +01:00
Joost Lekkerkerker
2d273a86ba Add more connection info to SmartThings (#165472) 2026-03-17 08:30:57 +01:00
Erik Montnemery
9bbd9d8bcd Deduplicate trigger tests checking labs flag (#165760)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 08:19:10 +01:00
mettolen
5ff2cac077 Set parallel updates for Huum integration (#165749) 2026-03-17 07:54:37 +01:00
mettolen
74b0d058ec Fix issues in Huum unit test (#165753) 2026-03-17 07:54:01 +01:00
mettolen
29f96e3f9c Move _async_abort_entries_match before the try block in Huum (#165752) 2026-03-17 07:47:47 +01:00
Mike Degatano
39b44445ec Use aiohasupervisor for all calls from hassio/coordinator (#164413)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-17 01:06:56 +01:00
Robert Resch
589622c05a Fix pterodactyl tests (#165745) 2026-03-16 23:44:26 +01:00
Brett Adams
6abe576ec9 Platinum quality for Teslemetry (#165727) 2026-03-16 22:31:17 +00:00
Robert Resch
75978d8837 Fix demo tests for Python 3.14.3 (#165724) 2026-03-16 22:52:04 +01:00
Robert Resch
a2da13a0b3 Fix kitchen_sink tests for Python 3.14.3 (#165730) 2026-03-16 22:45:36 +01:00
Robert Resch
ce081d7e71 Fix local_file tests for Python 3.14.3 (#165731) 2026-03-16 22:45:15 +01:00
Robert Resch
037e123e11 Fix media_player tests for Python 3.14.3 (#165732) 2026-03-16 22:44:52 +01:00
Robert Resch
592b7e5594 Fix wake_on_lan tests for Python 3.14.3 (#165733) 2026-03-16 22:44:23 +01:00
Cyril MARIN
a963eed3a7 Add bearer token as optional setting to Ollama (#165325)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-16 22:14:33 +01:00
Devin Slick
2042f2e2bd Add Lojack integration (#162047)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-16 22:09:10 +01:00
mettolen
3580fab26e Initialize quality scale for Huum integration (#164902) 2026-03-16 22:08:43 +01:00
Matt Zimmerman
1817522107 Clean up SmartTub integration and tests (#165517)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 22:06:23 +01:00
Matt Zimmerman
98a9ce3a64 Add quality scale file for SmartTub integration (#162376)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:48:09 +01:00
johanzander
163bfb0fdd Add SPH inverter support to Growatt Server integration (#165314)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:46:48 +01:00
Jeff Terrace
66f04c702c Update onvif parsers library to latest parsing multiple (#165571)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 21:40:37 +01:00
Khole
41c497c49e Hive: Fix bug in config flow for authentication and device registration (#165061) 2026-03-16 21:07:34 +01:00
Ludovic BOUÉ
c25a664365 Fix Matter firmware update detection when version strings are identical (#165509) 2026-03-16 21:07:03 +01:00
Raj Laud
3dec70abce Add AC charger sensor support to victron_ble (#165497)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 20:59:30 +01:00
Robert Resch
3c2f696a23 Improve type hints for pilight (#165719) 2026-03-16 20:55:04 +01:00
Nathan Spencer
54745dc1f2 Remove stale devices at setup in Whisker (#165721) 2026-03-16 20:54:02 +01:00
Raj Laud
e4345c72d9 Fix SmartLithium 8-cell support in victron_ble (#165496)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 20:49:43 +01:00
J. Diego Rodríguez Royo
7acb253ae2 Add bread baking and dough proving programs to Home Connect (#165717) 2026-03-16 20:47:20 +01:00
J. Diego Rodríguez Royo
812c63eeb7 Bump aiohomeconnect to 0.32.0 (#165716) 2026-03-16 20:46:22 +01:00
Erwin Douna
7f13731035 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-16 20:45:33 +01:00
Christian Lackas
879178e8a2 Add light support for HmIP-MP3P (Combination Signalling Device) (#162825) 2026-03-16 20:43:36 +01:00
Brett Adams
4d8cedb061 Add dynamic device discovery for Teslemetry (#162143)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:31:05 +01:00
Christian Lackas
e9f0d8a550 vicare: Remove heating type config, defaulting to auto-detection (#165649) 2026-03-16 20:26:02 +01:00
Joost Lekkerkerker
c5a04deb28 Add integration type to Orvibo (#165706) 2026-03-16 20:04:59 +01:00
Bouwe Westerdijk
f2a205e8d7 Improve Plugwise DataUpdateCoordinator (#165715)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:04:55 +01:00
prana-dev-official
254aa30ad8 Add sensor platform to prana (#165632) 2026-03-16 20:03:36 +01:00
J. Diego Rodríguez Royo
de4025634a Add start selected program action to Home Connect (#165362)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 20:03:20 +01:00
Artur Pragacz
db4af890f4 Use standard syrupy serialisation for registries in homekit controller (#165693) 2026-03-16 18:17:22 +00:00
cdheiser
501c8fecec Bump pylutron to 0.4.0 and maintain switch compatibility (#165592) 2026-03-16 19:13:23 +01:00
Andres Ruiz
03edee1335 Enable support for multiple Waterfurnace devices (#162692)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 19:04:14 +01:00
Nathan Spencer
00b0da7d26 Add auto device removal handling to Whisker (#165709) 2026-03-16 18:01:37 +00:00
J. Nick Koston
bf23fc5887 Fix choppy HomeKit camera audio with SRTP audio proxy (#165185) 2026-03-16 07:36:08 -10:00
Artur Pragacz
6f746c4375 Add common entity_entry_as_dict util to diagnostics (#165692) 2026-03-16 18:16:13 +01:00
Nathan Spencer
e7c3a62569 Add dynamic devices support for Whisker (#165704) 2026-03-16 18:11:10 +01:00
Joost Lekkerkerker
b1578a0c8c Add hassfest check to make sure new integrations have an integration type (#164001) 2026-03-16 18:10:30 +01:00
Martin Ecker
56b4d2c015 Add correct speed fan mapping for Z-Wave GE/Jasco Enbrighten ZWA4013 (#164500) 2026-03-16 17:53:58 +01:00
Erwin Douna
d5ee99c450 Proxmox re-use sanitize UserID (#164303) 2026-03-16 17:50:51 +01:00
hanwg
7d2a305996 Suggest chat_id for subentry flow for Telegram bot (#165515)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 17:33:00 +01:00
Josef Zweck
6945418805 Refactor mold_indicator sensor (#165696) 2026-03-16 17:10:05 +01:00
Ariel Ebersberger
ccecbcb389 Refactor condition helpers (#165662) 2026-03-16 16:57:53 +01:00
epenet
8bb51c0662 Move meteo_france coordinators to separate module (#164558)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 16:27:54 +01:00
Raj Laud
f66edf6b86 Bump victron-ble-ha-parser to 0.6.1 (#165473)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 15:27:06 +00:00
Joost Lekkerkerker
70e469366b Finish test coverage in TRMNL (#165611) 2026-03-16 16:18:20 +01:00
epenet
4a9ba865be Fix HVACMode mappings in Tuya climate (#165691) 2026-03-16 16:15:12 +01:00
Denis Shulyaka
0167182e2e Add support for service tier for OpenAI integration (#165379)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 15:38:29 +01:00
Ariel Ebersberger
11411a880d Refactor trigger helpers (#165455)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 15:26:57 +01:00
J. Diego Rodríguez Royo
ce47abe1d3 Add climate entity for air conditioner to Home Connect (#155981)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: emontnemery <erik@montnemery.com>
2026-03-16 15:19:57 +01:00
epenet
b58513c19a Use TuyaCoverAction enum in Tuya cover (#165690) 2026-03-16 15:08:49 +01:00
epenet
4e1dab6d8b Migrate remaining vacuum wrappers to Tuya library (#165688) 2026-03-16 15:06:03 +01:00
epenet
5ae8e1c319 Migrate remaining climate wrappers to Tuya library (#165687) 2026-03-16 15:03:15 +01:00
epenet
17bf6ca591 Migrate remaining alarm control panel wrappers to Tuya library (#165686) 2026-03-16 14:59:10 +01:00
epenet
256d30c38d Migrate remaining fan wrappers to Tuya library (#165685) 2026-03-16 14:56:26 +01:00
Jan Čermák
5d182394c2 Update zizmor to v1.23.1 (#165467) 2026-03-16 14:30:13 +01:00
epenet
011e6863d8 Bump tuya-device-handlers to 0.0.13 (#165684) 2026-03-16 14:11:26 +01:00
Anis Kadri
b902b590b1 Add UniFi Access binary sensors (#165569)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 14:03:46 +01:00
peteS-UK
960666e15b Improve discovery flow for Squeezebox (#153958) 2026-03-16 13:50:33 +01:00
Mike Degatano
1fb59c9f11 Remove code notary related unsupported reasons (#165417)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-16 13:45:58 +01:00
Mike Degatano
332bf95e16 Bump aiohasupervisor to 0.4.1 (#165489)
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-03-16 13:11:48 +01:00
Joost Lekkerkerker
e35fc8267e Fix typing in nsw_fuel_station (#165679) 2026-03-16 12:41:53 +01:00
Joost Lekkerkerker
f8b4ffc0d7 Fix translation placeholders in Assist pipeline (#165676) 2026-03-16 12:37:47 +01:00
Mike Degatano
003ee5a699 Remove aiohasupervisor from pyproject.toml (#165512) 2026-03-16 11:56:10 +01:00
epenet
c91d805174 Use external library wrapper in Tuya vacuum (#165673) 2026-03-16 11:52:34 +01:00
epenet
c478d19ae3 Use external library wrapper in Tuya climate (#165672) 2026-03-16 11:46:59 +01:00
Samuel Xiao
09169b0f06 Switchbot Cloud: Fixed Circulator Fan on start error (#165241)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-16 11:45:21 +01:00
epenet
aa1dbee315 Use external library wrapper in Tuya cover (#165656) 2026-03-16 11:37:18 +01:00
TimL
daf89e5673 Bump Pysmlight to 0.3.0 (#165658) 2026-03-16 11:35:25 +01:00
Joshua Monta
85dc81c147 Update uhoo IQS to silver (#165665) 2026-03-16 11:31:53 +01:00
epenet
5acf24cb53 Use external library wrapper in Tuya alarm control panel (#165671) 2026-03-16 11:30:51 +01:00
Martin Hjelmare
79829a311c Fix emulated_kasa tests for Python 3.14.3 (#165667) 2026-03-16 11:19:06 +01:00
Martin Hjelmare
ce2c62ae28 Fix numato tests for Python 3.14.3 (#165669) 2026-03-16 11:17:29 +01:00
Martin Hjelmare
1cda3f47d6 Fix valve tests for Python 3.14.3 (#165668) 2026-03-16 11:16:27 +01:00
Nathan Spencer
e254716615 Remove deprecated entity creation code for Litter-Robot 4 devices (#165636) 2026-03-16 10:40:31 +01:00
epenet
1d410f4cbd Use external library wrapper in Tuya humidifer (#165654) 2026-03-16 10:39:54 +01:00
epenet
6616793e2b Use external library wrapper in Tuya light (#165653) 2026-03-16 10:39:43 +01:00
Joost Lekkerkerker
6766961327 Finish TRMNL docs (#165612) 2026-03-16 10:38:11 +01:00
Denis Shulyaka
dd6fc11d28 Bump python-telegram-bot to 22.6 (#165508)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 10:34:54 +01:00
Simone Chemelli
cb5b8b212c Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:32:55 +01:00
epenet
66b96d096e Use external library wrapper in Tuya event (#165655) 2026-03-16 10:32:31 +01:00
epenet
e86160de36 Use external library wrapper in Tuya fan (#165464) 2026-03-16 10:24:00 +01:00
Simone Chemelli
7617007edd Update IQS to silver for Fritz (#162280) 2026-03-16 10:19:35 +01:00
epenet
3e065b31b3 Simplify Prana entity descriptions (#165660)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-16 10:12:16 +01:00
Simone Chemelli
5f909a6f3a Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 10:05:42 +01:00
Jan Bouwhuis
6117a20ec6 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:03:35 +01:00
Simone Chemelli
93bc05bb3f Fix switch set for Vodafone Station (#165273) 2026-03-16 10:00:52 +01:00
Thomas Kadauke
e7397ccaa7 fix: Increase WebSocket message size limit to 16MB in Hass.io ingress proxy (#164442)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 09:48:06 +01:00
Joshua Monta
91a43873a2 feat: implement reauthentication requirement (#165641) 2026-03-16 09:03:01 +01:00
Ludovic BOUÉ
469e06fb8c Add Matter certified Silabs fan example to fixtures (#165622) 2026-03-16 09:02:23 +01:00
Joost Lekkerkerker
bac370e775 Add diagnostics to Zinvolt (#165623) 2026-03-16 08:22:46 +01:00
Mick Vleeshouwer
1a9da26286 Update Overkiz test fixtures and diagnostics to use more realistic fixture (#165615) 2026-03-16 08:21:01 +01:00
Lukas
f795707c53 Pooldose bump python-pooldose to 0.8.6 (#165616) 2026-03-16 07:27:57 +01:00
Allen Porter
9ad1356e4b Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 07:09:25 +01:00
J. Nick Koston
0f70d5fd39 Bump aioesphomeapi to 44.5.2 (#165644) 2026-03-16 07:07:49 +01:00
Josh Gustafson
f4c6724953 Add icons for Arcam sensors (#165637) 2026-03-16 06:56:30 +01:00
Sab44
82432d9ee7 Bump librehardwaremonitor-api to version 1.11.1 (#165629) 2026-03-16 00:10:51 +00:00
Joost Lekkerkerker
8db07f3ceb Add API key url to step description in TRMNL (#165614) 2026-03-15 23:41:22 +01:00
Joost Lekkerkerker
2fe9d1ef86 Add reconfigure flow to TRMNL (#165594) 2026-03-15 21:52:40 +01:00
David Bishop
cbb1f3726c Move coordinator tests and migrate test data to JSON fixtures (#165503)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:53:45 +01:00
Josh Gustafson
beb122bb1a Add binary sensor platform to Arcam FMJ (#165272)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 20:08:05 +01:00
J. Nick Koston
8d6099b055 Bump ulid-transform to 2.0.2 (#165585) 2026-03-15 20:07:19 +01:00
J. Nick Koston
7ebe11c0e6 Bump habluetooth to 5.10.2 (#165591) 2026-03-15 20:07:01 +01:00
Andrew Jackson
12b14b46c0 Bump aiomealie to 1.2.2 (#165610) 2026-03-15 19:56:48 +01:00
tronikos
cc45201f2d Redact utility account id in Opower diagnostics (#165145) 2026-03-15 11:56:36 -07:00
Erwin Douna
a433a163a3 Migrate unique ID of Portainer integration (#165123) 2026-03-15 18:00:41 +01:00
Joost Lekkerkerker
7fd86145d1 Add 2 more sensors to TRMNL (#165604) 2026-03-15 17:13:35 +01:00
Joost Lekkerkerker
f244af590e Handle action exceptions in TRMNL (#165607) 2026-03-15 17:00:14 +01:00
Joost Lekkerkerker
9a7dd98d89 Change initiate flow button text for TRMNL (#165606) 2026-03-15 16:46:57 +01:00
Joost Lekkerkerker
6c4beba465 Bump trmnl to 0.1.1 (#165605) 2026-03-15 16:45:43 +01:00
Joost Lekkerkerker
3a46beec76 Add dynamic device handling to TRMNL (#165548) 2026-03-15 16:43:52 +01:00
Josef Zweck
d7c2dfc4d4 Add backup progress callback to onedrive integrations (#165217) 2026-03-15 16:31:04 +01:00
Joost Lekkerkerker
4efbafb003 Add TRMNL time platform (#165537)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-15 14:29:20 +01:00
Simone Chemelli
1b10db28f1 Add 100% coverage of coordinator for Fritz (#164074) 2026-03-15 12:16:42 +01:00
Joost Lekkerkerker
1e988fbb04 Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-15 11:39:57 +01:00
J. Nick Koston
9ab577aad4 Bump fnv-hash-fast to 2.0.0 (#165586) 2026-03-15 09:55:54 +01:00
Olivier R.
ed53469eb6 Fix KeyError 'api_domain' in Freebox zeroconf discovery (#165288)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-15 09:07:28 +01:00
Andres Ruiz
56aa96a00c Add re-auth flow for Waterfurnace (#165406) 2026-03-15 07:09:35 +01:00
Anis Kadri
99c6cdbe44 Bump py-unifi-access to 1.1.0 (#165576) 2026-03-15 06:58:27 +01:00
J. Diego Rodríguez Royo
1fd30b73e7 Add fan speed percentage to service schema (#165557) 2026-03-15 06:57:38 +01:00
Joost Lekkerkerker
14aace0c00 Add stale device handling to TRMNL (#165550) 2026-03-15 06:56:05 +01:00
Joost Lekkerkerker
6eed18623b Add reauthentication to TRMNL (#165546) 2026-03-15 06:54:26 +01:00
Joost Lekkerkerker
66ca7d5782 Add switch platform to TRMNL (#165539) 2026-03-15 06:49:09 +01:00
Joost Lekkerkerker
a7436cbdc3 Add diagnostics to TRMNL (#165544) 2026-03-15 06:48:13 +01:00
Joost Lekkerkerker
5e57b0272d Add diagnostics to Chess.com (#165563) 2026-03-15 06:47:37 +01:00
Raphael Hehl
e16b6ab026 Add emergency switch platform for UniFi Access integration (#165536)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 20:43:12 +01:00
Joost Lekkerkerker
e21fb14b9a Discover Aeotec hub for SmartThings (#165469) 2026-03-14 19:56:53 +01:00
Simone Chemelli
8e099a874b Bump aioamazondevices to 13.0.1 (#165476) 2026-03-14 19:46:02 +01:00
Åke Strandberg
a5302a6219 Fix missing code for Miele dishwasher (#165553) 2026-03-14 19:45:47 +01:00
Nathan Spencer
f761ac5b49 Add coordinator exception translations and mark entity/exception-translations rules as done (#165551) 2026-03-14 19:27:11 +01:00
Josh Gustafson
6988e73ddc Add sensor platform to Arcam FMJ (#165271)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:18:17 +01:00
Norbert Rittel
a88374557b Make "Power-on behavior" in zha consistent with matter and tuya (#165549) 2026-03-14 18:04:55 +01:00
Nathan Spencer
f2456b2c3a Add reconfiguration flow to Whisker (#165513) 2026-03-14 17:30:29 +01:00
Raphael Hehl
c1a525b7aa Add unifi_access to Ubiquiti brand and regenerate integrations.json (#165538)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 17:09:16 +01:00
Joost Lekkerkerker
9d2febd24e Add TRMNL integration (#165499) 2026-03-14 16:17:19 +01:00
Raphael Hehl
54f96bcc33 Add event platform for UniFi Access integration (#165531)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 14:12:50 +01:00
Manu
5582d83f7b Remove duplicate sensor entity description for monitor port in Uptime Kuma integration (#165479) 2026-03-14 14:05:48 +01:00
Joost Lekkerkerker
2832456bcd Add binary sensor for cooktop in SmartThings (#165481) 2026-03-14 14:05:24 +01:00
Norbert Rittel
070c5821e4 Make start_up_current_level in zha consistent with matter (#165504) 2026-03-14 13:58:01 +01:00
Lukas
07caa8ed2d Bump python-pooldose to 0.8.5 (#165507) 2026-03-14 13:57:20 +01:00
Kevin Stillhammer
b02f447e4d Bump pywaze to 1.2.0 (#165526) 2026-03-14 13:56:15 +01:00
Nathan Spencer
4fbb22e861 Update Whisker quality scale docs rules (#165510) 2026-03-14 11:38:29 +01:00
hanwg
45199a341f Pass web session to download files for Telegram bot (#165424) 2026-03-14 09:57:39 +01:00
Jan-Philipp Benecke
de5f42d7a0 Add progress reporting to WebDAV upload (#165398) 2026-03-14 08:35:47 +01:00
Artur Pragacz
4459dce73a Reorder code to group intent errors (#165431) 2026-03-13 18:58:19 -05:00
Artur Pragacz
a465905467 Remove speech parameter from service intent handler (#165225) 2026-03-13 18:57:16 -05:00
Raphael Hehl
a47faa3ced Add UniFi Access integration (#165404)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-03-14 00:00:18 +01:00
Josh
7276403ab9 Allow deleting UniFi client devices (#165505) 2026-03-13 23:06:58 +01:00
Raj Laud
018717af4f Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 22:23:54 +01:00
Norbert Rittel
274c2b8092 Shorten "Power-on behavior" name in matter to be consistent (#165490) 2026-03-13 21:22:49 +01:00
David Bishop
bfe15a55c9 Add entity-unavailable and log-when-unavailable (#165486)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:20:55 +00:00
dvdinth
54ad67b810 Bump pyintelliclima dependency for IntelliClima integration (#165478)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-03-13 20:16:27 +00:00
Nathan Spencer
4d2732df6f Add diagnostics to Whisker (#165487) 2026-03-13 20:38:57 +01:00
Andres Ruiz
2be3291d8e Update brand name for Subaru integration (#165485) 2026-03-13 20:26:44 +01:00
Joost Lekkerkerker
4326cb96ea Add zigbee address to SmartThings devices (#165474)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 20:14:58 +01:00
Norbert Rittel
278894d4b4 Make "power-on behavior" states more consistent in tuya (#165344) 2026-03-13 18:53:32 +00:00
Ariel Ebersberger
eb17367229 Add DomainSpec to trigger and condition helpers (#165392) 2026-03-13 19:50:19 +01:00
Mike Degatano
d96191723f Improve error handling when addon unavailable for install/update (#165352) 2026-03-13 19:28:19 +01:00
mcisk
b6c7b2952e Add autoskope integration (#146772)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 19:19:00 +01:00
David Bishop
356de12bce Add parallel-updates and action-exceptions for Whisker (#165433)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:33:42 +01:00
epenet
57c49d0c48 Fix missing Tuya climate preset_mode (#165460) 2026-03-13 17:49:10 +01:00
Joost Lekkerkerker
af22b5fdbb Bump pySmartThings to 3.7.0 (#165468) 2026-03-13 17:12:15 +01:00
Joost Lekkerkerker
9c710961f0 Add Matter fixtures to SmartThings (#165466) 2026-03-13 17:09:38 +01:00
epenet
2a2da83173 Use external library wrapper in Tuya binary_sensor (#165465) 2026-03-13 17:05:52 +01:00
jvmahon
00a52245e3 Add Matter start-up Power-on level entity (#164775) 2026-03-13 17:04:12 +01:00
TheJulianJES
adb30e1ec1 Hide ZWA-2 adapter in Zigbee serial port selector (#155526) 2026-03-13 16:56:12 +01:00
TheJulianJES
34a7fcf8d3 Bump ZHA to 1.0.2 (#165423) 2026-03-13 16:15:51 +01:00
prana-dev-official
95a57a2984 Add fan platform for Prana Integration (#163379)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-13 16:05:37 +01:00
epenet
7f39cc0aeb Bump tuya-device-handlers to 0.0.12 (#165462) 2026-03-13 15:58:12 +01:00
Robin Lintermann
6962288e85 Add spring status sensor entity (#164332) 2026-03-13 14:29:37 +01:00
Eli Sand
fab4355cc8 Enhance generic_thermostat with min/max run time and cooldown time (#136298) 2026-03-13 14:22:33 +01:00
Robin Lintermann
e39d84e8fc Bump pysmarlaapi to 1.0.2 (#165454) 2026-03-13 12:46:09 +01:00
Christian Lackas
35f597223a Add DHW operating mode select entity to ViCare integration (#163832) 2026-03-13 12:44:24 +01:00
Galorhallen
9d61c8336d Update govee local api to 2.4.0 (#165418) 2026-03-13 12:43:41 +01:00
Robert Resch
6fd3603b7b Bump orjson to 3.11.7 (#165443) 2026-03-13 12:34:13 +01:00
epenet
49ac5c42ee Add base entity to arcam_fmj (#165447) 2026-03-13 12:27:52 +01:00
epenet
df0db5853c Fix device name in arcam_fmj (#165448) 2026-03-13 12:25:52 +01:00
dependabot[bot]
7afc5b777c Bump docker/metadata-action from 5.10.0 to 6.0.0 (#165438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:25:35 +01:00
dependabot[bot]
595aeea8cc Bump github/codeql-action from 4.32.4 to 4.32.6 (#165436)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:22:09 +01:00
dependabot[bot]
02abba02d1 Bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#165437)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:21:54 +01:00
dependabot[bot]
4ca1ad96f1 Bump docker/build-push-action from 6.19.2 to 7.0.0 (#165435)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 12:21:20 +01:00
Erik Montnemery
9f3beba97a Fix vera test opening sockets (#165439) 2026-03-13 11:00:17 +01:00
johanzander
9f86006328 Update Growatt quality scale: add config flow data descriptions (#165426)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:46:14 +01:00
Erik Montnemery
4ac651d0b4 Add occupancy triggers (#165374) 2026-03-13 08:41:48 +01:00
J. Nick Koston
9e54abbcb5 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-13 08:19:24 +01:00
Erik Montnemery
d5915c8811 Add motion triggers (#165373) 2026-03-13 07:54:51 +01:00
Erik Montnemery
0c2887df9e Fix numerical entity trigger schema (#165411) 2026-03-13 07:32:43 +01:00
Zach Feldman
3767bac850 August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-12 17:28:08 -10:00
J. Nick Koston
9d962d3815 Add missing ON_OFF support and target_temperature_step to ESPHome water heater (#165427)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-12 16:10:29 -10:00
Bram Kragten
786fd40ae8 Update frontend to 20260312.0 (#165420) 2026-03-12 23:07:04 +01:00
Joakim Plate
5ec65dbd58 Remove use of media player internals in arcam (#165359) 2026-03-12 21:55:39 +00:00
Josef Zweck
35878bb203 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-12 21:59:40 +01:00
Arie Catsman
e14d88ff55 Bump pyenphase to 2.4.6 (#165402) 2026-03-12 20:06:49 +00:00
Erwin Douna
d04efbfe48 Add platinum badge to Portainer (#165048)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-03-12 19:30:31 +01:00
AlCalzone
3f35cd5cd2 Remove Z-Wave Installer panel (#165388)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com>
2026-03-12 17:30:28 +01:00
AlCalzone
86ffd58665 Instruct AI to add type annotations to tests (#165386)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 17:10:30 +01:00
prana-dev-official
6206392b28 Bump prana-local-api to 0.12.0 (#165394) 2026-03-12 17:05:26 +01:00
dvdinth
b7c36c707f Add IntelliClima Sensor platform (#163901)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-12 16:33:34 +01:00
Joakim Sørensen
973c32b99d Add latency results if available to the support package (#165377) 2026-03-12 10:44:08 +01:00
Erik Montnemery
951775bea6 Add window triggers (#165230) 2026-03-12 10:18:42 +01:00
Artur Pragacz
0f2dbdf4f4 Fix logging of unavailable entities in entity call (#165370) 2026-03-12 09:53:30 +01:00
Jan-Philipp Benecke
443ff7efe1 Bump aiowebdav2 to 0.6.2 (#165353) 2026-03-12 08:17:41 +01:00
Jeef
0ee6b954df Bump intellifire4py to 4.4.0 (#165356) 2026-03-12 08:15:48 +01:00
Norbert Rittel
5681acf0e1 Sentence-case "API token" and "username/password" in growatt (#165368) 2026-03-12 07:49:35 +01:00
Andres Ruiz
a94458b8bc Bump waterfurnace version v1.6.2 (#165348) 2026-03-12 07:49:12 +01:00
Josef Zweck
f3c38ba2d3 Add "cleaning_up" stage to backup (#165349) 2026-03-12 07:28:17 +01:00
Jan Bouwhuis
c1acd1d860 Allow an MQTT entity to show as a group (#152270)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-11 22:25:28 +01:00
chli1
f4748aa63d fix #163316: FRITZ!SmartHome integration not showing boost status on … (#164574) 2026-03-11 22:19:43 +01:00
Brett Adams
31f4f618cc Fix duplicate energy remaining sensors in Tessie (#165102) 2026-03-11 21:39:35 +01:00
Oluwatobi Mustapha
30aec4d2ab Migrate OAuth helper token request exception handling in Google Sheets (#165000)
Signed-off-by: Oluwatobi Mustapha <oluwatobimustapha539@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 20:33:26 +01:00
AlCalzone
335abd7002 Support new Z-Wave JS "Opening state" notification variable (#165236) 2026-03-11 20:13:54 +01:00
Joakim Sørensen
3b3f0e9240 Bump hass-nabucasa from 1.15.0 to 2.0.0 (#165335) 2026-03-11 20:02:28 +01:00
Simone Chemelli
49586d1519 Fix dnd switch status for Alexa Devices (#164953) 2026-03-11 19:21:51 +01:00
Erwin Douna
c63ded3522 Add Swarm stack to Portainer (#164991) 2026-03-11 18:14:05 +01:00
Josef Zweck
2eb65ab314 Buffer backup upload progress events (#165249)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-11 17:29:35 +01:00
ams2990
402a37b435 Change light.toggle service call to invoke LightEntity.async_toggle (#156196)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-11 17:17:10 +01:00
Erik Montnemery
aa66e8ef0c Improve humidity triggers (#165323) 2026-03-11 17:11:27 +01:00
noambav
f1a1e284b7 Add support for Fish Audio s2-pro model (#165269) 2026-03-11 17:07:56 +01:00
hanwg
08594f4e0c Update migration message for Telegram bot (#165299) 2026-03-11 17:04:16 +01:00
Joakim Plate
8d810588f8 Move secondary zone of arcam to sub-device (#165336) 2026-03-11 16:57:47 +01:00
Sid
70faad15d5 Add binary_sensor to eheimdigital (#165035)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 16:21:16 +01:00
TheJulianJES
d447843687 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-11 16:15:35 +01:00
Steve Easley
83b64e29fa Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-11 16:13:26 +01:00
tronikos
4558a10e05 Improve test coverage in Opower to make it silver (#165124) 2026-03-11 15:56:31 +01:00
johanzander
5ad9e81082 Add reauthentication flow to growatt_server (silver quality scale) (#164993)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:51:25 +01:00
cdheiser
ba00a14772 Fix flakiness in lutron tests and isolate platforms per test file (#165328) 2026-03-11 15:08:00 +01:00
J. Diego Rodríguez Royo
49f4d07eeb Add fan entity for air conditioner to Home Connect (#155983)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-11 14:29:01 +01:00
Dan Raper
5d271a0d30 Bump ohme to 1.7.0 (#165318) 2026-03-11 12:49:07 +01:00
Joakim Plate
474b683d3c Update gardena to 2.1.0 (#165322) 2026-03-11 12:48:24 +01:00
Erik Montnemery
d37106a360 Add gate triggers (#165228) 2026-03-11 10:59:53 +01:00
epenet
e115c90719 Reduce internal testing in arcam_fmj tests (#165315) 2026-03-11 10:14:24 +01:00
epenet
6ad3adf0c3 Remove duplicate fixture in arcam_fmj tests (#165312) 2026-03-11 09:51:51 +01:00
dependabot[bot]
2a8d59be4c Bump docker/login-action from 3.7.0 to 4.0.0 (#165302) 2026-03-11 09:16:34 +01:00
dependabot[bot]
6e6e35bc3b Bump actions/dependency-review-action from 4.8.3 to 4.9.0 (#165304) 2026-03-11 09:15:36 +01:00
epenet
795b4c8414 Fix incorrect type annotations in tests (#165305) 2026-03-11 08:38:58 +01:00
Luke Lashley
16389dc18e Bump python-roborock to 4.20.0 (#165292) 2026-03-11 08:21:28 +01:00
Erik Montnemery
e7a1c8d001 Remove triggers binary_sensor.occupancy_cleared and occupancy_detected (#165181) 2026-03-11 07:37:40 +01:00
Luke Lashley
4efb10dae1 Remove an extra roborock trait from updating (#165297) 2026-03-11 02:31:10 +01:00
Erik Montnemery
f163576e78 Fail more tests when pytest_socket.SocketBlockedError is raised (#155398)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-11 00:00:00 +01:00
Erik Montnemery
cad8f97e97 Prevent network access in telegram_bot tests (#165284) 2026-03-10 21:53:35 +01:00
Jeef
4ae6099d84 Add local/cloud option to Intellifire (#162739)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 21:11:57 +01:00
epenet
60dc88fa15 Move NUT coordinator to separate module (#164848)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:13:00 -10:00
Josh Gustafson
2d2c6d676d Address Arcam FMJ post-merge feedback (#165277)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:09:54 +01:00
Matthias Alphart
f3879335ab KNX: add config for unit_of_measurement for yaml sensor entities (#165082) 2026-03-10 19:27:59 +01:00
Matthias Alphart
11bc00038e KNX: add config for device_class and unit_of_measurement for yaml number entities (#165083) 2026-03-10 19:27:48 +01:00
David Bonnes
6845e8b880 Extend RESET_SYSTEM action to all Evohome controller types (#164459) 2026-03-10 19:27:35 +01:00
cdheiser
5741016931 Bump pylutron version to 0.3.0 (#164707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 18:10:28 +01:00
WardZhou
6cbc4e7f62 Add support for Thread Integration to Display Icons for Aeotec SmartThings TBRs (#165275) 2026-03-10 18:07:50 +01:00
Joost Lekkerkerker
4064df0114 Create reset HEPA filter button for main component in SmartThings (#165262) 2026-03-10 18:00:55 +01:00
Troels Schwarz-Linnet
789f850691 Implement 2 new sensors in pyvicare (#164523) 2026-03-10 17:59:36 +01:00
Abílio Costa
efca71852b Implement exception-translations for whirlpool integration (#165017) 2026-03-10 17:56:59 +01:00
A. Gideonse
1967e9f309 Add reconfiguration flow to Indevolt integration (#165132) 2026-03-10 17:43:19 +01:00
Artur Pragacz
6ac0c163aa Improve group entities (#160860) 2026-03-10 17:34:52 +01:00
Norbert Rittel
bbe20fd698 Improve descriptions of bond actions (#164744) 2026-03-10 17:08:23 +01:00
hanwg
f576743340 Fix proxy settings not applied for Telegram bot (#165240) 2026-03-10 16:42:46 +01:00
John O'Nolan
3b4a1fba5f Update Ghost integration quality scale to gold (#165215) 2026-03-10 16:25:15 +01:00
Artur Pragacz
1677a9bfa6 Add clean area intent for vacuum (#165182) 2026-03-10 16:24:18 +01:00
Jordan Harvey
0d9c458705 Anglian Water: Add last meter reading processed sensor (#159144)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-10 16:18:11 +01:00
epenet
57026a862d Ensure actions have name and description translations (#158243)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-10 16:06:51 +01:00
Josh Gustafson
fd05be4c52 Refactor Arcam FMJ to use coordinator pattern (#165232)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:37:09 +01:00
Dave Love
b1f038849e Add Midea Smart Inverter Window AC to Matter Fan Only mode list (#165170) 2026-03-10 15:28:09 +01:00
Ariel Ebersberger
b46c9ccc65 Influxdb: Add reconfigure flow (#165186) 2026-03-10 15:06:31 +01:00
epenet
80601426cf Move spotify coordinator to separate module (#164927) 2026-03-10 15:01:04 +01:00
Michael
9519bd2428 Add turned off and turned on triggers to input boolean (#158824)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-03-10 14:26:15 +01:00
Manu
be0b7f06a8 Bump pyrate-limiter to 4.0.2, PSNAWP to 3.0.3, python-roborock to 4.17.2 (#164133)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-03-10 13:54:37 +01:00
Joost Lekkerkerker
d30c6de168 Add another air purifier fixture to SmartThings (#165261) 2026-03-10 12:30:12 +01:00
Sab44
0fa666518e Dynamically add new devices to Libre Hardware Monitor (#165250) 2026-03-10 09:19:50 +01:00
Josef Zweck
cf454a1fa3 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-10 09:13:07 +01:00
Panda-NZ
a36733c4dc Add ambient temperature range controls to ToGrill integration (#165235) 2026-03-09 23:40:30 +01:00
Bram Kragten
bf846e0756 Validate reorder is only used when multiple is true (#165216) 2026-03-09 22:32:02 +01:00
Erik Montnemery
c037dad093 Add humidity triggers (#165197) 2026-03-09 20:34:26 +01:00
1855 changed files with 100112 additions and 40763 deletions

View File

@@ -18,6 +18,11 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
## Testing
When writing or modifying tests, ensure all test function parameters have type annotations.
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
## Good practices
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.

View File

@@ -72,7 +72,7 @@ jobs:
- name: Download Translations
run: python3 -m script.translations download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
@@ -196,19 +196,19 @@ jobs:
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build variables
id: vars
@@ -242,7 +242,7 @@ jobs:
- name: Build base image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: ./Dockerfile
@@ -328,7 +328,7 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -400,19 +400,19 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -442,7 +442,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -456,7 +456,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -585,14 +585,14 @@ jobs:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -605,7 +605,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -614,7 +614,7 @@ jobs:
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}

View File

@@ -609,7 +609,7 @@ jobs:
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
license-check: false # We use our own license audit checks
@@ -852,10 +852,6 @@ jobs:
needs:
- info
- base
- gen-requirements-all
- hassfest
- prek
- mypy
steps:
- name: Restore apt cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
@@ -1400,7 +1396,7 @@ jobs:
with:
fail_ci_if_error: true
flags: full-suite
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
pytest-partial:
name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
@@ -1570,7 +1566,7 @@ jobs:
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
upload-test-results:
name: Upload test results to Codecov

View File

@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: "/language:python"

View File

@@ -58,8 +58,8 @@ jobs:
# v1.7.0
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
with:
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 90 day stale policy for issues
# Used for:

View File

@@ -33,6 +33,6 @@ jobs:
- name: Upload Translations
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
run: |
python3 -m script.translations upload

View File

@@ -142,7 +142,7 @@ jobs:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
@@ -200,7 +200,7 @@ jobs:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl

View File

@@ -18,7 +18,7 @@ repos:
exclude_types: [csv, json, html]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
rev: v1.23.1
hooks:
- id: zizmor
args:

View File

@@ -342,6 +342,7 @@ homeassistant.components.lookin.*
homeassistant.components.lovelace.*
homeassistant.components.luftdaten.*
homeassistant.components.lunatone.*
homeassistant.components.lutron.*
homeassistant.components.madvr.*
homeassistant.components.manual.*
homeassistant.components.mastodon.*
@@ -569,6 +570,7 @@ homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.transmission.*
homeassistant.components.trend.*
homeassistant.components.trmnl.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*

View File

@@ -15,6 +15,11 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
## Testing
When writing or modifying tests, ensure all test function parameters have type annotations.
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
## Good practices
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.

18
CODEOWNERS generated
View File

@@ -186,6 +186,8 @@ build.json @home-assistant/supervisor
/tests/components/auth/ @home-assistant/core
/homeassistant/components/automation/ @home-assistant/core
/tests/components/automation/ @home-assistant/core
/homeassistant/components/autoskope/ @mcisk
/tests/components/autoskope/ @mcisk
/homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @ricohageman
/tests/components/awair/ @ahayworth @ricohageman
@@ -577,6 +579,8 @@ build.json @home-assistant/supervisor
/tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus
/tests/components/gardena_bluetooth/ @elupus
/homeassistant/components/gate/ @home-assistant/core
/tests/components/gate/ @home-assistant/core
/homeassistant/components/gdacs/ @exxamalte
/tests/components/gdacs/ @exxamalte
/homeassistant/components/generic/ @davet2001
@@ -743,6 +747,8 @@ build.json @home-assistant/supervisor
/tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
/tests/components/humidifier/ @home-assistant/core @Shulyaka
/homeassistant/components/humidity/ @home-assistant/core
/tests/components/humidity/ @home-assistant/core
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555
@@ -968,6 +974,8 @@ build.json @home-assistant/supervisor
/tests/components/logbook/ @home-assistant/core
/homeassistant/components/logger/ @home-assistant/core
/tests/components/logger/ @home-assistant/core
/homeassistant/components/lojack/ @devinslick
/tests/components/lojack/ @devinslick
/homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco
@@ -1067,6 +1075,8 @@ build.json @home-assistant/supervisor
/tests/components/moon/ @fabaff @frenck
/homeassistant/components/mopeka/ @bdraco
/tests/components/mopeka/ @bdraco
/homeassistant/components/motion/ @home-assistant/core
/tests/components/motion/ @home-assistant/core
/homeassistant/components/motion_blinds/ @starkillerOG
/tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
@@ -1180,6 +1190,8 @@ build.json @home-assistant/supervisor
/tests/components/nzbget/ @chriscla
/homeassistant/components/obihai/ @dshokouhi @ejpenney
/tests/components/obihai/ @dshokouhi @ejpenney
/homeassistant/components/occupancy/ @home-assistant/core
/tests/components/occupancy/ @home-assistant/core
/homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480
@@ -1760,6 +1772,8 @@ build.json @home-assistant/supervisor
/tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
/tests/components/triggercmd/ @rvmey
/homeassistant/components/trmnl/ @joostlek
/tests/components/trmnl/ @joostlek
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver
@@ -1776,6 +1790,8 @@ build.json @home-assistant/supervisor
/tests/components/ukraine_alarm/ @PaulAnnekov
/homeassistant/components/unifi/ @Kane610
/tests/components/unifi/ @Kane610
/homeassistant/components/unifi_access/ @imhotep @RaHehl
/tests/components/unifi_access/ @imhotep @RaHehl
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl
@@ -1901,6 +1917,8 @@ build.json @home-assistant/supervisor
/tests/components/wiffi/ @mampfes
/homeassistant/components/wilight/ @leofig-rj
/tests/components/wilight/ @leofig-rj
/homeassistant/components/window/ @home-assistant/core
/tests/components/window/ @home-assistant/core
/homeassistant/components/wirelesstag/ @sergeymaysak
/homeassistant/components/withings/ @joostlek
/tests/components/withings/ @joostlek

View File

@@ -243,6 +243,11 @@ DEFAULT_INTEGRATIONS = {
# Integrations providing triggers and conditions for base platforms:
"door",
"garage_door",
"gate",
"humidity",
"motion",
"occupancy",
"window",
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
# These integrations are set up if recovery mode is activated.

View File

@@ -1,5 +1,12 @@
{
"domain": "ubiquiti",
"name": "Ubiquiti",
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
"integrations": [
"airos",
"unifi",
"unifi_access",
"unifi_direct",
"unifiled",
"unifiprotect"
]
}

View File

@@ -2,6 +2,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityStateConditionBase,
@@ -43,7 +44,7 @@ def make_entity_state_required_features_condition(
class CustomCondition(EntityStateRequiredFeaturesCondition):
"""Condition for entity state changes."""
_domain = domain
_domain_specs = {domain: DomainSpec()}
_states = {to_state}
_required_features = required_features

View File

@@ -2,6 +2,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import (
EntityTargetStateTriggerBase,
@@ -44,7 +45,7 @@ def make_entity_state_trigger_required_features(
class CustomTrigger(EntityStateTriggerRequiredFeatures):
"""Trigger for entity state changes."""
_domains = {domain}
_domain_specs = {domain: DomainSpec()}
_to_states = {to_state}
_required_features = required_features

View File

@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model=self.device.model,
model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
sw_version=self.device.software_version,
serial_number=serial_num,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"

View File

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

View File

@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""

View File

@@ -338,6 +338,7 @@ class Analytics:
hass = self._hass
supervisor_info = None
addons_info: dict[str, Any] | None = None
operating_system_info: dict[str, Any] = {}
if self._data.uuid is None:
@@ -347,6 +348,7 @@ class Analytics:
if self.supervisor:
supervisor_info = hassio.get_supervisor_info(hass)
operating_system_info = hassio.get_os_info(hass) or {}
addons_info = hassio.get_addons_info(hass) or {}
system_info = await async_get_system_info(hass)
integrations = []
@@ -419,13 +421,10 @@ class Analytics:
integrations.append(integration.domain)
if supervisor_info is not None:
if addons_info is not None:
supervisor_client = hassio.get_supervisor_client(hass)
installed_addons = await asyncio.gather(
*(
supervisor_client.addons.addon_info(addon[ATTR_SLUG])
for addon in supervisor_info[ATTR_ADDONS]
)
*(supervisor_client.addons.addon_info(slug) for slug in addons_info)
)
addons.extend(
{

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from pyanglianwater.meter import SmartMeter
@@ -32,13 +33,14 @@ class AnglianWaterSensor(StrEnum):
YESTERDAY_WATER_COST = "yesterday_water_cost"
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
LATEST_READING = "latest_reading"
LAST_UPDATED = "last_updated"
@dataclass(frozen=True, kw_only=True)
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
"""Describes AnglianWater sensor entity."""
value_fn: Callable[[SmartMeter], float]
value_fn: Callable[[SmartMeter], float | datetime | None]
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
@@ -76,6 +78,13 @@ ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
entity_category=EntityCategory.DIAGNOSTIC,
),
AnglianWaterSensorEntityDescription(
key=AnglianWaterSensor.LAST_UPDATED,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda entity: entity.last_updated,
translation_key=AnglianWaterSensor.LAST_UPDATED,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -112,6 +121,6 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
self.entity_description = description
@property
def native_value(self) -> float | None:
def native_value(self) -> float | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.smart_meter)

View File

@@ -34,6 +34,9 @@
},
"entity": {
"sensor": {
"last_updated": {
"name": "Last meter reading processed"
},
"latest_reading": {
"name": "Latest reading"
},

View File

@@ -8,46 +8,55 @@ from typing import Any
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
DEFAULT_SCAN_INTERVAL,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
type ArcamFmjConfigEntry = ConfigEntry[Client]
from .const import DEFAULT_SCAN_INTERVAL
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator, ArcamFmjRuntimeData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
"""Set up config entry."""
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
coordinators: dict[int, ArcamFmjCoordinator] = {}
for zone in (1, 2):
coordinator = ArcamFmjCoordinator(hass, entry, client, zone)
coordinators[zone] = coordinator
entry.runtime_data = ArcamFmjRuntimeData(client, coordinators)
entry.async_create_background_task(
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
hass,
_run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL),
"arcam_fmj",
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
"""Cleanup before removing config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:
async def _run_client(
hass: HomeAssistant,
runtime_data: ArcamFmjRuntimeData,
interval: float,
) -> None:
client = runtime_data.client
coordinators = runtime_data.coordinators
def _listen(_: Any) -> None:
async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host)
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
while True:
try:
@@ -55,16 +64,21 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N
await client.start()
_LOGGER.debug("Client connected %s", client.host)
async_dispatcher_send(hass, SIGNAL_CLIENT_STARTED, client.host)
try:
for coordinator in coordinators.values():
await coordinator.state.start()
with client.listen(_listen):
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await client.process()
finally:
await client.stop()
_LOGGER.debug("Client disconnected %s", client.host)
async_dispatcher_send(hass, SIGNAL_CLIENT_STOPPED, client.host)
for coordinator in coordinators.values():
coordinator.async_notify_disconnected()
except ConnectionFailed:
await asyncio.sleep(interval)

View File

@@ -0,0 +1,68 @@
"""Arcam binary sensors for incoming stream info."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from arcam.fmj.state import State
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
@dataclass(frozen=True, kw_only=True)
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes an Arcam FMJ binary sensor entity."""
value_fn: Callable[[State], bool | None]
BINARY_SENSORS: tuple[ArcamFmjBinarySensorEntityDescription, ...] = (
ArcamFmjBinarySensorEntityDescription(
key="incoming_video_interlaced",
translation_key="incoming_video_interlaced",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: (
vp.interlaced
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Arcam FMJ binary sensors from a config entry."""
coordinators = config_entry.runtime_data.coordinators
entities: list[ArcamFmjBinarySensorEntity] = []
for coordinator in coordinators.values():
entities.extend(
ArcamFmjBinarySensorEntity(coordinator, description)
for description in BINARY_SENSORS
)
async_add_entities(entities)
class ArcamFmjBinarySensorEntity(ArcamFmjEntity, BinarySensorEntity):
"""Representation of an Arcam FMJ binary sensor."""
entity_description: ArcamFmjBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return the binary sensor value."""
return self.entity_description.value_fn(self.coordinator.state)

View File

@@ -2,10 +2,6 @@
DOMAIN = "arcam_fmj"
SIGNAL_CLIENT_STARTED = "arcam.client_started"
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
SIGNAL_CLIENT_DATA = "arcam.client_data"
EVENT_TURN_ON = "arcam_fmj.turn_on"
DEFAULT_PORT = 50000

View File

@@ -0,0 +1,97 @@
"""Coordinator for Arcam FMJ integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.state import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class ArcamFmjRuntimeData:
"""Runtime data for Arcam FMJ integration."""
client: Client
coordinators: dict[int, ArcamFmjCoordinator]
type ArcamFmjConfigEntry = ConfigEntry[ArcamFmjRuntimeData]
class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
"""Coordinator for a single Arcam FMJ zone."""
config_entry: ArcamFmjConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
client: Client,
zone: int,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"Arcam FMJ zone {zone}",
)
self.client = client
self.state = State(client, zone)
self.last_update_success = False
name = config_entry.title
unique_id = config_entry.unique_id or config_entry.entry_id
unique_id_device = unique_id
if zone != 1:
unique_id_device += f"-{zone}"
name += f" Zone {zone}"
self.device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id_device)},
manufacturer="Arcam",
model="Arcam FMJ AVR",
name=name,
)
self.zone_unique_id = f"{unique_id}-{zone}"
if zone != 1:
self.device_info["via_device"] = (DOMAIN, unique_id)
async def _async_update_data(self) -> None:
"""Fetch data for manual refresh."""
try:
await self.state.update()
except ConnectionFailed as err:
raise UpdateFailed(
f"Connection failed during update for zone {self.state.zn}"
) from err
@callback
def async_notify_data_updated(self) -> None:
"""Notify that new data has been received from the device."""
self.async_set_updated_data(None)
@callback
def async_notify_connected(self) -> None:
"""Handle client connected."""
self.hass.async_create_task(self.async_refresh())
@callback
def async_notify_disconnected(self) -> None:
"""Handle client disconnected."""
self.last_update_success = False
self.async_update_listeners()

View File

@@ -0,0 +1,28 @@
"""Base entity for Arcam FMJ integration."""
from __future__ import annotations
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ArcamFmjCoordinator
class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
"""Base entity for Arcam FMJ."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: ArcamFmjCoordinator,
description: EntityDescription | None = None,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = coordinator.device_info
self._attr_entity_registry_enabled_default = coordinator.state.zn == 1
self._attr_unique_id = coordinator.zone_unique_id
if description is not None:
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
self.entity_description = description

View File

@@ -0,0 +1,35 @@
{
"entity": {
"binary_sensor": {
"incoming_video_interlaced": {
"default": "mdi:reorder-horizontal"
}
},
"sensor": {
"incoming_audio_config": {
"default": "mdi:surround-sound"
},
"incoming_audio_format": {
"default": "mdi:dolby"
},
"incoming_audio_sample_rate": {
"default": "mdi:waveform"
},
"incoming_video_aspect_ratio": {
"default": "mdi:aspect-ratio"
},
"incoming_video_colorspace": {
"default": "mdi:palette"
},
"incoming_video_horizontal_resolution": {
"default": "mdi:arrow-expand-horizontal"
},
"incoming_video_refresh_rate": {
"default": "mdi:animation"
},
"incoming_video_vertical_resolution": {
"default": "mdi:arrow-expand-vertical"
}
}
}
}

View File

@@ -8,7 +8,6 @@ import logging
from typing import Any
from arcam.fmj import ConnectionFailed, SourceCodes
from arcam.fmj.state import State
from homeassistant.components.media_player import (
BrowseError,
@@ -20,20 +19,13 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ArcamFmjConfigEntry
from .const import (
DOMAIN,
EVENT_TURN_ON,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)
from .const import EVENT_TURN_ON
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator
from .entity import ArcamFmjEntity
_LOGGER = logging.getLogger(__name__)
@@ -44,19 +36,10 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the configuration entry."""
client = config_entry.runtime_data
coordinators = config_entry.runtime_data.coordinators
async_add_entities(
[
ArcamFmj(
config_entry.title,
State(client, zone),
config_entry.unique_id or config_entry.entry_id,
)
for zone in (1, 2)
],
True,
[ArcamFmj(coordinators[zone]) for zone in (1, 2)],
)
@@ -77,21 +60,13 @@ def convert_exception[**_P, _R](
return _convert_exception
class ArcamFmj(MediaPlayerEntity):
class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
"""Representation of a media device."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
self,
device_name: str,
state: State,
uuid: str,
) -> None:
def __init__(self, coordinator: ArcamFmjCoordinator) -> None:
"""Initialize device."""
self._state = state
self._attr_name = f"Zone {state.zn}"
super().__init__(coordinator)
self._state = coordinator.state
self._attr_supported_features = (
MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY_MEDIA
@@ -102,18 +77,8 @@ class ArcamFmj(MediaPlayerEntity):
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.TURN_ON
)
if state.zn == 1:
if self._state.zn == 1:
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
self._attr_unique_id = f"{uuid}-{state.zn}"
self._attr_entity_registry_enabled_default = state.zn == 1
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, uuid),
},
manufacturer="Arcam",
model="Arcam FMJ AVR",
name=device_name,
)
@property
def state(self) -> MediaPlayerState:
@@ -122,49 +87,6 @@ class ArcamFmj(MediaPlayerEntity):
return MediaPlayerState.ON
return MediaPlayerState.OFF
async def async_added_to_hass(self) -> None:
"""Once registered, add listener for events."""
await self._state.start()
try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during addition: %s", connection)
@callback
def _data(host: str) -> None:
if host == self._state.client.host:
self.async_write_ha_state()
@callback
def _started(host: str) -> None:
if host == self._state.client.host:
self.async_schedule_update_ha_state(force_refresh=True)
@callback
def _stopped(host: str) -> None:
if host == self._state.client.host:
self.async_schedule_update_ha_state(force_refresh=True)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_DATA, _data)
)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STARTED, _started)
)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped)
)
async def async_update(self) -> None:
"""Force update of state."""
_LOGGER.debug("Update state %s", self.name)
try:
await self._state.update()
except ConnectionFailed as connection:
_LOGGER.debug("Connection lost during update: %s", connection)
@convert_exception
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""

View File

@@ -0,0 +1,162 @@
"""Arcam sensors for incoming stream info."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from arcam.fmj import IncomingVideoAspectRatio, IncomingVideoColorspace
from arcam.fmj.state import IncomingAudioConfig, IncomingAudioFormat, State
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory, UnitOfFrequency
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ArcamFmjConfigEntry
from .entity import ArcamFmjEntity
@dataclass(frozen=True, kw_only=True)
class ArcamFmjSensorEntityDescription(SensorEntityDescription):
"""Describes an Arcam FMJ sensor entity."""
value_fn: Callable[[State], int | float | str | None]
SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
ArcamFmjSensorEntityDescription(
key="incoming_video_horizontal_resolution",
translation_key="incoming_video_horizontal_resolution",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="px",
suggested_display_precision=0,
value_fn=lambda state: (
vp.horizontal_resolution
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_vertical_resolution",
translation_key="incoming_video_vertical_resolution",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="px",
suggested_display_precision=0,
value_fn=lambda state: (
vp.vertical_resolution
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_refresh_rate",
translation_key="incoming_video_refresh_rate",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0,
value_fn=lambda state: (
vp.refresh_rate
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_aspect_ratio",
translation_key="incoming_video_aspect_ratio",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingVideoAspectRatio],
value_fn=lambda state: (
vp.aspect_ratio.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_video_colorspace",
translation_key="incoming_video_colorspace",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingVideoColorspace],
value_fn=lambda state: (
vp.colorspace.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_format",
translation_key="incoming_audio_format",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingAudioFormat],
value_fn=lambda state: (
result.name.lower()
if (result := state.get_incoming_audio_format()[0]) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_config",
translation_key="incoming_audio_config",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[member.name.lower() for member in IncomingAudioConfig],
value_fn=lambda state: (
result.name.lower()
if (result := state.get_incoming_audio_format()[1]) is not None
else None
),
),
ArcamFmjSensorEntityDescription(
key="incoming_audio_sample_rate",
translation_key="incoming_audio_sample_rate",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
suggested_display_precision=0,
value_fn=lambda state: (
None
if (sample_rate := state.get_incoming_audio_sample_rate()) == 0
else sample_rate
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ArcamFmjConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Arcam FMJ sensors from a config entry."""
coordinators = config_entry.runtime_data.coordinators
entities: list[ArcamFmjSensorEntity] = []
for coordinator in coordinators.values():
entities.extend(
ArcamFmjSensorEntity(coordinator, description) for description in SENSORS
)
async_add_entities(entities)
class ArcamFmjSensorEntity(ArcamFmjEntity, SensorEntity):
"""Representation of an Arcam FMJ sensor."""
entity_description: ArcamFmjSensorEntityDescription
@property
def native_value(self) -> int | float | str | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.state)

View File

@@ -23,5 +23,121 @@
"trigger_type": {
"turn_on": "{entity_name} was requested to turn on"
}
},
"entity": {
"binary_sensor": {
"incoming_video_interlaced": {
"name": "Incoming video interlaced"
}
},
"sensor": {
"incoming_audio_config": {
"name": "Incoming audio configuration",
"state": {
"auro_10_1": "Auro 10.1",
"auro_11_1": "Auro 11.1",
"auro_13_1": "Auro 13.1",
"auro_2_2_2": "Auro 2.2.2",
"auro_5_0": "Auro 5.0",
"auro_5_1": "Auro 5.1",
"auro_8_0": "Auro 8.0",
"auro_9_1": "Auro 9.1",
"auro_quad": "Auro quad",
"dual_mono": "Dual mono",
"dual_mono_lfe": "Dual mono + LFE",
"mono": "Mono",
"mono_lfe": "Mono + LFE",
"stereo_center": "Stereo center",
"stereo_center_lfe": "Stereo center + LFE",
"stereo_center_surr_lr": "Stereo center surround L/R",
"stereo_center_surr_lr_back_lr": "Stereo center surround L/R back L/R",
"stereo_center_surr_lr_back_lr_lfe": "Stereo center surround L/R back L/R + LFE",
"stereo_center_surr_lr_back_matrix": "Stereo center surround L/R back matrix",
"stereo_center_surr_lr_back_matrix_lfe": "Stereo center surround L/R back matrix + LFE",
"stereo_center_surr_lr_back_mono": "Stereo center surround L/R back mono",
"stereo_center_surr_lr_back_mono_lfe": "Stereo center surround L/R back mono + LFE",
"stereo_center_surr_lr_lfe": "Stereo center surround L/R + LFE",
"stereo_center_surr_mono": "Stereo center surround mono",
"stereo_center_surr_mono_lfe": "Stereo center surround mono + LFE",
"stereo_downmix": "Stereo downmix",
"stereo_downmix_lfe": "Stereo downmix + LFE",
"stereo_lfe": "Stereo + LFE",
"stereo_only": "Stereo only",
"stereo_only_lo_ro": "Stereo only Lo/Ro",
"stereo_only_lo_ro_lfe": "Stereo only Lo/Ro + LFE",
"stereo_surr_lr": "Stereo surround L/R",
"stereo_surr_lr_back_lr": "Stereo surround L/R back L/R",
"stereo_surr_lr_back_lr_lfe": "Stereo surround L/R back L/R + LFE",
"stereo_surr_lr_back_matrix": "Stereo surround L/R back matrix",
"stereo_surr_lr_back_matrix_lfe": "Stereo surround L/R back matrix + LFE",
"stereo_surr_lr_back_mono": "Stereo surround L/R back mono",
"stereo_surr_lr_back_mono_lfe": "Stereo surround L/R back mono + LFE",
"stereo_surr_lr_lfe": "Stereo surround L/R + LFE",
"stereo_surr_mono": "Stereo surround mono",
"stereo_surr_mono_lfe": "Stereo surround mono + LFE",
"undetected": "Undetected",
"unknown": "Unknown"
}
},
"incoming_audio_format": {
"name": "Incoming audio format",
"state": {
"analogue_direct": "Analogue direct",
"auro_3d": "Auro-3D",
"dolby_atmos": "Dolby Atmos",
"dolby_digital": "Dolby Digital",
"dolby_digital_ex": "Dolby Digital EX",
"dolby_digital_plus": "Dolby Digital Plus",
"dolby_digital_surround": "Dolby Digital Surround",
"dolby_digital_true_hd": "Dolby TrueHD",
"dts": "DTS",
"dts_96_24": "DTS 96/24",
"dts_core": "DTS Core",
"dts_es_discrete": "DTS-ES Discrete",
"dts_es_discrete_96_24": "DTS-ES Discrete 96/24",
"dts_es_matrix": "DTS-ES Matrix",
"dts_es_matrix_96_24": "DTS-ES Matrix 96/24",
"dts_hd_high_res_audio": "DTS-HD High Resolution Audio",
"dts_hd_master_audio": "DTS-HD Master Audio",
"dts_low_bit_rate": "DTS Low Bit Rate",
"dts_x": "DTS:X",
"imax_enhanced": "IMAX Enhanced",
"pcm": "PCM",
"pcm_zero": "PCM zero",
"undetected": "Undetected",
"unsupported": "Unsupported"
}
},
"incoming_audio_sample_rate": {
"name": "Incoming audio sample rate"
},
"incoming_video_aspect_ratio": {
"name": "Incoming video aspect ratio",
"state": {
"aspect_16_9": "16:9",
"aspect_4_3": "4:3",
"undefined": "Undefined"
}
},
"incoming_video_colorspace": {
"name": "Incoming video colorspace",
"state": {
"dolby_vision": "Dolby Vision",
"hdr10": "HDR10",
"hdr10_plus": "HDR10+",
"hlg": "HLG",
"normal": "Normal"
}
},
"incoming_video_horizontal_resolution": {
"name": "Incoming video horizontal resolution"
},
"incoming_video_refresh_rate": {
"name": "Incoming video refresh rate"
},
"incoming_video_vertical_resolution": {
"name": "Incoming video vertical resolution"
}
}
}
}

View File

@@ -78,19 +78,13 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
index: int = 0,
) -> None:
"""Initialize a pipeline selector."""
if index < 1:
# Keep compatibility
key_suffix = ""
placeholder = ""
else:
key_suffix = f"_{index + 1}"
placeholder = f" {index + 1}"
self.entity_description = replace(
self.entity_description,
key=f"pipeline{key_suffix}",
translation_placeholders={"index": placeholder},
)
if index >= 1:
self.entity_description = replace(
self.entity_description,
key=f"pipeline_{index + 1}",
translation_key="pipeline_n",
translation_placeholders={"index": str(index + 1)},
)
self._domain = domain
self._unique_id_prefix = unique_id_prefix

View File

@@ -7,11 +7,17 @@
},
"select": {
"pipeline": {
"name": "Assistant{index}",
"name": "Assistant",
"state": {
"preferred": "Preferred"
}
},
"pipeline_n": {
"name": "Assistant {index}",
"state": {
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
}
},
"vad_sensitivity": {
"name": "Finished speaking detection",
"state": {

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -121,6 +121,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"climate",
"cover",
"device_tracker",
"fan",
"humidifier",
@@ -137,7 +138,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"binary_sensor",
"button",
"climate",
"cover",
@@ -145,11 +145,16 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"door",
"fan",
"garage_door",
"gate",
"humidifier",
"humidity",
"input_boolean",
"lawn_mower",
"light",
"lock",
"media_player",
"motion",
"occupancy",
"person",
"remote",
"scene",
@@ -159,6 +164,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"text",
"update",
"vacuum",
"window",
}

View File

@@ -0,0 +1,53 @@
"""The Autoskope integration."""
from __future__ import annotations
import aiohttp
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DEFAULT_HOST
from .coordinator import AutoskopeConfigEntry, AutoskopeDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]
async def async_setup_entry(hass: HomeAssistant, entry: AutoskopeConfigEntry) -> bool:
"""Set up Autoskope from a config entry."""
session = async_create_clientsession(hass, cookie_jar=aiohttp.CookieJar())
api = AutoskopeApi(
host=entry.data.get(CONF_HOST, DEFAULT_HOST),
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
)
try:
await api.connect()
except InvalidAuth as err:
# Raise ConfigEntryError until reauth flow is implemented (then ConfigEntryAuthFailed)
raise ConfigEntryError(
"Authentication failed, please check credentials"
) from err
except CannotConnect as err:
raise ConfigEntryNotReady("Could not connect to Autoskope API") from err
coordinator = AutoskopeDataUpdateCoordinator(hass, api, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AutoskopeConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,89 @@
"""Config flow for the Autoskope integration."""
from __future__ import annotations
from typing import Any
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import section
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADVANCED_SETTINGS
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): TextSelector(
TextSelectorConfig(type=TextSelectorType.URL)
),
}
),
{"collapsed": True},
),
}
)
class AutoskopeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Autoskope."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
username = user_input[CONF_USERNAME].lower()
host = user_input[SECTION_ADVANCED_SETTINGS][CONF_HOST].lower()
try:
cv.url(host)
except vol.Invalid:
errors["base"] = "invalid_url"
if not errors:
await self.async_set_unique_id(f"{username}@{host}")
self._abort_if_unique_id_configured()
try:
async with AutoskopeApi(
host=host,
username=username,
password=user_input[CONF_PASSWORD],
):
pass
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
return self.async_create_entry(
title=f"Autoskope ({username})",
data={
CONF_USERNAME: username,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_HOST: host,
},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -0,0 +1,9 @@
"""Constants for the Autoskope integration."""
from datetime import timedelta
DOMAIN = "autoskope"
DEFAULT_HOST = "https://portal.autoskope.de"
SECTION_ADVANCED_SETTINGS = "advanced_settings"
UPDATE_INTERVAL = timedelta(seconds=60)

View File

@@ -0,0 +1,60 @@
"""Data update coordinator for the Autoskope integration."""
from __future__ import annotations
import logging
from autoskope_client.api import AutoskopeApi
from autoskope_client.models import CannotConnect, InvalidAuth, Vehicle
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
type AutoskopeConfigEntry = ConfigEntry[AutoskopeDataUpdateCoordinator]
class AutoskopeDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Vehicle]]):
"""Class to manage fetching Autoskope data."""
config_entry: AutoskopeConfigEntry
def __init__(
self, hass: HomeAssistant, api: AutoskopeApi, entry: AutoskopeConfigEntry
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=entry,
)
self.api = api
async def _async_update_data(self) -> dict[str, Vehicle]:
"""Fetch data from API endpoint."""
try:
vehicles = await self.api.get_vehicles()
return {vehicle.id: vehicle for vehicle in vehicles}
except InvalidAuth:
# Attempt to re-authenticate using stored credentials
try:
await self.api.authenticate()
# Retry the request after successful re-authentication
vehicles = await self.api.get_vehicles()
return {vehicle.id: vehicle for vehicle in vehicles}
except InvalidAuth as reauth_err:
raise ConfigEntryAuthFailed(
f"Authentication failed: {reauth_err}"
) from reauth_err
except CannotConnect as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err

View File

@@ -0,0 +1,145 @@
"""Support for Autoskope device tracking."""
from __future__ import annotations
from autoskope_client.constants import MANUFACTURER
from autoskope_client.models import Vehicle
from homeassistant.components.device_tracker import SourceType, TrackerEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AutoskopeConfigEntry, AutoskopeDataUpdateCoordinator
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: AutoskopeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Autoskope device tracker entities."""
coordinator: AutoskopeDataUpdateCoordinator = entry.runtime_data
tracked_vehicles: set[str] = set()
@callback
def update_entities() -> None:
"""Update entities based on coordinator data."""
current_vehicles = set(coordinator.data.keys())
vehicles_to_add = current_vehicles - tracked_vehicles
if vehicles_to_add:
new_entities = [
AutoskopeDeviceTracker(coordinator, vehicle_id)
for vehicle_id in vehicles_to_add
]
tracked_vehicles.update(vehicles_to_add)
async_add_entities(new_entities)
entry.async_on_unload(coordinator.async_add_listener(update_entities))
update_entities()
class AutoskopeDeviceTracker(
CoordinatorEntity[AutoskopeDataUpdateCoordinator], TrackerEntity
):
"""Representation of an Autoskope tracked device."""
_attr_has_entity_name = True
_attr_name: str | None = None
def __init__(
self, coordinator: AutoskopeDataUpdateCoordinator, vehicle_id: str
) -> None:
"""Initialize the TrackerEntity."""
super().__init__(coordinator)
self._vehicle_id = vehicle_id
self._attr_unique_id = vehicle_id
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if (
self._vehicle_id in self.coordinator.data
and (device_entry := self.device_entry) is not None
and device_entry.name != self._vehicle_data.name
):
device_registry = dr.async_get(self.hass)
device_registry.async_update_device(
device_entry.id, name=self._vehicle_data.name
)
super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for the vehicle."""
vehicle = self.coordinator.data[self._vehicle_id]
return DeviceInfo(
identifiers={(DOMAIN, str(vehicle.id))},
name=vehicle.name,
manufacturer=MANUFACTURER,
model=vehicle.model,
serial_number=vehicle.imei,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available
and self.coordinator.data is not None
and self._vehicle_id in self.coordinator.data
)
@property
def _vehicle_data(self) -> Vehicle:
"""Return the vehicle data for the current entity."""
return self.coordinator.data[self._vehicle_id]
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""
if (vehicle := self._vehicle_data) and vehicle.position:
return float(vehicle.position.latitude)
return None
@property
def longitude(self) -> float | None:
"""Return longitude value of the device."""
if (vehicle := self._vehicle_data) and vehicle.position:
return float(vehicle.position.longitude)
return None
@property
def source_type(self) -> SourceType:
"""Return the source type of the device."""
return SourceType.GPS
@property
def location_accuracy(self) -> float:
"""Return the location accuracy of the device in meters."""
if (vehicle := self._vehicle_data) and vehicle.gps_quality:
if vehicle.gps_quality > 0:
# HDOP to estimated accuracy in meters
# HDOP of 1-2 = good (5-10m), 2-5 = moderate (10-25m), >5 = poor (>25m)
return float(max(5, int(vehicle.gps_quality * 5.0)))
return 0.0
@property
def icon(self) -> str:
"""Return the icon based on the vehicle's activity."""
if self._vehicle_id not in self.coordinator.data:
return "mdi:car-clock"
vehicle = self._vehicle_data
if vehicle.position:
if vehicle.position.park_mode:
return "mdi:car-brake-parking"
if vehicle.position.speed > 5: # Moving threshold: 5 km/h
return "mdi:car-arrow-right"
return "mdi:car"
return "mdi:car-clock"

View File

@@ -0,0 +1,11 @@
{
"domain": "autoskope",
"name": "Autoskope",
"codeowners": ["@mcisk"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autoskope",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["autoskope_client==1.4.1"]
}

View File

@@ -0,0 +1,88 @@
# + in comment indicates requirement for quality scale
# - in comment indicates issue to be fixed, not impacting quality scale
rules:
# Bronze
action-setup:
status: exempt
comment: |
Integration does not provide custom services.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
Integration does not provide custom services.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
Integration does not provide custom services.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow:
status: todo
comment: |
Reauthentication flow removed for initial PR, will be added in follow-up.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
Integration does not use discovery. Autoskope devices use NB-IoT/LTE-M (via IoT SIMs) and LoRaWAN.
discovery:
status: exempt
comment: |
Integration does not use discovery. Autoskope devices use NB-IoT/LTE-M (via IoT SIMs) and LoRaWAN.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
Only one entity type (device_tracker) is created, making this not applicable.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: todo
comment: |
Reconfiguration flow removed for initial PR, will be added in follow-up.
repair-issues: todo
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing:
status: todo
comment: |
Integration needs to be added to .strict-typing file for full compliance.

View File

@@ -0,0 +1,52 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_url": "Invalid URL",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "The password for your Autoskope account.",
"username": "The username for your Autoskope account."
},
"description": "Enter your Autoskope credentials.",
"sections": {
"advanced_settings": {
"data": {
"host": "API endpoint"
},
"data_description": {
"host": "The URL of your Autoskope API endpoint. Only change this if you use a white-label portal."
},
"name": "Advanced settings"
}
},
"title": "Connect to Autoskope"
}
}
},
"issues": {
"cannot_connect": {
"description": "Home Assistant could not connect to the Autoskope API at {host}. Please check the connection details and ensure the API endpoint is reachable.\n\nError: {error}",
"title": "Failed to connect to Autoskope"
},
"invalid_auth": {
"description": "Authentication with Autoskope failed for user {username}. Please re-authenticate the integration with the correct password.",
"title": "Invalid Autoskope authentication"
},
"low_battery": {
"description": "The battery voltage for vehicle {vehicle_name} ({vehicle_id}) is low ({value}V). Consider checking or replacing the battery.",
"title": "Low vehicle battery ({vehicle_name})"
}
}
}

View File

@@ -32,6 +32,7 @@ from homeassistant.helpers import (
issue_registry as ir,
start,
)
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
from homeassistant.util.async_iterator import AsyncIteratorReader
@@ -78,6 +79,8 @@ from .util import (
validate_password_stream,
)
UPLOAD_PROGRESS_DEBOUNCE_SECONDS = 1
@dataclass(frozen=True, kw_only=True, slots=True)
class NewBackup:
@@ -141,6 +144,7 @@ class CreateBackupStage(StrEnum):
ADDONS = "addons"
AWAIT_ADDON_RESTARTS = "await_addon_restarts"
DOCKER_CONFIG = "docker_config"
CLEANING_UP = "cleaning_up"
FINISHING_FILE = "finishing_file"
FOLDERS = "folders"
HOME_ASSISTANT = "home_assistant"
@@ -590,23 +594,49 @@ class BackupManager:
)
agent = self.backup_agents[agent_id]
latest_uploaded_bytes = 0
@callback
def on_upload_progress(*, bytes_uploaded: int, **kwargs: Any) -> None:
"""Handle upload progress."""
def _emit_upload_progress() -> None:
"""Emit the latest upload progress event."""
self.async_on_backup_event(
UploadBackupEvent(
manager_state=self.state,
agent_id=agent_id,
uploaded_bytes=bytes_uploaded,
uploaded_bytes=latest_uploaded_bytes,
total_bytes=_backup.size,
)
)
upload_progress_debouncer: Debouncer[None] = Debouncer(
self.hass,
LOGGER,
cooldown=UPLOAD_PROGRESS_DEBOUNCE_SECONDS,
immediate=True,
function=_emit_upload_progress,
)
@callback
def on_upload_progress(*, bytes_uploaded: int, **kwargs: Any) -> None:
"""Handle upload progress."""
nonlocal latest_uploaded_bytes
latest_uploaded_bytes = bytes_uploaded
upload_progress_debouncer.async_schedule_call()
await agent.async_upload_backup(
open_stream=open_stream_func,
backup=_backup,
on_progress=on_upload_progress,
)
upload_progress_debouncer.async_cancel()
self.async_on_backup_event(
UploadBackupEvent(
manager_state=self.state,
agent_id=agent_id,
uploaded_bytes=_backup.size,
total_bytes=_backup.size,
)
)
if streamer:
await streamer.wait()
@@ -1261,6 +1291,13 @@ class BackupManager:
)
# delete old backups more numerous than copies
# try this regardless of agent errors above
self.async_on_backup_event(
CreateBackupEvent(
reason=None,
stage=CreateBackupStage.CLEANING_UP,
state=CreateBackupState.IN_PROGRESS,
)
)
await delete_backups_exceeding_configured_count(self)
finally:

View File

@@ -174,13 +174,5 @@
"on": "mdi:window-open"
}
}
},
"triggers": {
"occupancy_cleared": {
"trigger": "mdi:home-outline"
},
"occupancy_detected": {
"trigger": "mdi:home"
}
}
}

View File

@@ -1,8 +1,4 @@
{
"common": {
"trigger_behavior_description_occupancy": "The behavior of the targeted occupancy sensors to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"condition_type": {
"is_bat_low": "{entity_name} battery is low",
@@ -321,36 +317,5 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Binary sensor",
"triggers": {
"occupancy_cleared": {
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy cleared"
},
"occupancy_detected": {
"description": "Triggers after one or more occupancy sensors start detecting occupancy.",
"fields": {
"behavior": {
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
}
},
"name": "Occupancy detected"
}
}
"title": "Binary sensor"
}

View File

@@ -1,67 +0,0 @@
"""Provides triggers for binary sensors."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
"""Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None
_domains = {DOMAIN}
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_class
}
def make_binary_sensor_trigger(
device_class: BinarySensorDeviceClass | None,
to_state: str,
) -> type[BinarySensorOnOffTrigger]:
"""Create an entity state trigger class."""
class CustomTrigger(BinarySensorOnOffTrigger):
"""Trigger for entity state changes."""
_device_class = device_class
_to_states = {to_state}
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"occupancy_detected": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
),
"occupancy_cleared": make_binary_sensor_trigger(
BinarySensorDeviceClass.OCCUPANCY, STATE_OFF
),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for binary sensors."""
return TRIGGERS

View File

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

View File

@@ -37,8 +37,8 @@
"name": "Entity"
},
"speed": {
"description": "Fan Speed as %.",
"name": "Fan Speed"
"description": "The fan speed as a percentage.",
"name": "Fan speed"
}
},
"name": "Set fan speed tracked state"
@@ -47,7 +47,7 @@
"description": "Sets the tracked brightness state of a Bond light.",
"fields": {
"brightness": {
"description": "Brightness.",
"description": "The tracked brightness of the light.",
"name": "Brightness"
},
"entity_id": {
@@ -79,22 +79,22 @@
"name": "Entity"
},
"power_state": {
"description": "Power state.",
"description": "The tracked power state.",
"name": "Power state"
}
},
"name": "Set switch power tracked state"
},
"start_decreasing_brightness": {
"description": "Starts decreasing the brightness of the light (deprecated).",
"description": "Starts decreasing the brightness of a light (deprecated).",
"name": "Start decreasing brightness"
},
"start_increasing_brightness": {
"description": "Starts increasing the brightness of the light (deprecated).",
"description": "Starts increasing the brightness of a light (deprecated).",
"name": "Start increasing brightness"
},
"stop": {
"description": "Stops any in-progress action and empty the queue (deprecated).",
"description": "Stops any in-progress action and empties the queue (deprecated).",
"name": "[%key:common::action::stop%]"
}
}

View File

@@ -2,6 +2,7 @@
from homeassistant.const import 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,
@@ -14,7 +15,7 @@ from . import DOMAIN
class ButtonPressedTrigger(EntityTriggerBase):
"""Trigger for button entity presses."""
_domains = {DOMAIN}
_domain_specs = {DOMAIN: DomainSpec()}
_schema = ENTITY_STATE_TRIGGER_SCHEMA
def is_valid_transition(self, from_state: State, to_state: State) -> bool:

View File

@@ -0,0 +1,22 @@
"""Diagnostics support for Chess.com."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.core import HomeAssistant
from .coordinator import ChessConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ChessConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
return {
"player": asdict(coordinator.data.player),
"stats": asdict(coordinator.data.stats),
}

View File

@@ -41,7 +41,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: Can't detect a game

View File

@@ -1,11 +1,8 @@
"""Provides conditions for climates."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import (
Condition,
make_entity_state_attribute_condition,
make_entity_state_condition,
)
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
@@ -22,14 +19,14 @@ CONDITIONS: dict[str, type[Condition]] = {
HVACMode.HEAT_COOL,
},
),
"is_cooling": make_entity_state_attribute_condition(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
"is_cooling": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.COOLING
),
"is_drying": make_entity_state_attribute_condition(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
"is_drying": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"is_heating": make_entity_state_attribute_condition(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.HEATING
"is_heating": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
}

View File

@@ -5,14 +5,14 @@ import voluptuous as vol
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
make_entity_numerical_state_attribute_changed_trigger,
make_entity_numerical_state_attribute_crossed_threshold_trigger,
make_entity_target_state_attribute_trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_target_state_trigger,
make_entity_transition_trigger,
)
@@ -35,7 +35,7 @@ HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend
class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
"""Trigger for entity state changes."""
_domains = {DOMAIN}
_domain_specs = {DOMAIN: DomainSpec()}
_schema = HVAC_MODE_CHANGED_TRIGGER_SCHEMA
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
@@ -46,23 +46,23 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
TRIGGERS: dict[str, type[Trigger]] = {
"hvac_mode_changed": HVACModeChangedTrigger,
"started_cooling": make_entity_target_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
"started_cooling": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.COOLING
),
"started_drying": make_entity_target_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
"started_drying": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"target_humidity_changed": make_entity_numerical_state_attribute_changed_trigger(
DOMAIN, ATTR_HUMIDITY
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
),
"target_humidity_crossed_threshold": make_entity_numerical_state_attribute_crossed_threshold_trigger(
DOMAIN, ATTR_HUMIDITY
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
),
"target_temperature_changed": make_entity_numerical_state_attribute_changed_trigger(
DOMAIN, ATTR_TEMPERATURE
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
),
"target_temperature_crossed_threshold": make_entity_numerical_state_attribute_crossed_threshold_trigger(
DOMAIN, ATTR_TEMPERATURE
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
),
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
"turned_on": make_entity_transition_trigger(
@@ -79,8 +79,8 @@ TRIGGERS: dict[str, type[Trigger]] = {
HVACMode.HEAT_COOL,
},
),
"started_heating": make_entity_target_state_attribute_trigger(
DOMAIN, ATTR_HVAC_ACTION, HVACAction.HEATING
"started_heating": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
}

View File

@@ -516,6 +516,8 @@ class DownloadSupportPackageView(HomeAssistantView):
hass_info: dict[str, Any],
domains_info: dict[str, dict[str, str]],
) -> str:
cloud = hass.data[DATA_CLOUD]
def get_domain_table_markdown(domain_info: dict[str, Any]) -> str:
if len(domain_info) == 0:
return "No information available\n"
@@ -572,6 +574,15 @@ class DownloadSupportPackageView(HomeAssistantView):
"</details>\n\n"
)
# Add stored latency response if available
if locations := cloud.remote.latency_by_location:
markdown += "## Latency by location\n\n"
markdown += "Location | Latency (ms)\n"
markdown += "--- | ---\n"
for location in sorted(locations):
markdown += f"{location} | {locations[location]['avg'] or 'N/A'}\n"
markdown += "\n"
# Add installed packages section
try:
installed_packages = await async_get_installed_packages()

View File

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

View File

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

View File

@@ -153,8 +153,8 @@ def websocket_get_entities(
{
vol.Required("type"): "config/entity_registry/update",
vol.Required("entity_id"): cv.entity_id,
vol.Optional("aliases"): [vol.Any(str, None)],
# If passed in, we update value. Passing None will remove old value.
vol.Optional("aliases"): list,
vol.Optional("area_id"): vol.Any(str, None),
# Categories is a mapping of key/value (scope/category_id) pairs.
# If passed in, we update/adjust only the provided scope(s).
@@ -225,10 +225,15 @@ def websocket_update_entity(
changes[key] = msg[key]
if "aliases" in msg:
# Create a set for the aliases without:
# - Empty strings
# Sanitize aliases by removing:
# - Trailing and leading whitespace characters in the individual aliases
changes["aliases"] = {s_strip for s in msg["aliases"] if (s_strip := s.strip())}
# - Empty strings
changes["aliases"] = aliases = []
for alias in msg["aliases"]:
if alias is None:
aliases.append(er.COMPUTED_NAME)
elif alias := alias.strip():
aliases.append(alias)
if "labels" in msg:
# Convert labels to a set

View File

@@ -992,18 +992,11 @@ class DefaultAgent(ConversationEntity):
continue
context[attr] = state.attributes[attr]
if (
entity := entity_registry.async_get(state.entity_id)
) and entity.aliases:
for alias in entity.aliases:
alias = alias.strip()
if not alias:
continue
yield (alias, alias, context)
# Default name
yield (state.name, state.name, context)
entity_entry = entity_registry.async_get(state.entity_id)
for name in intent.async_get_entity_aliases(
self.hass, entity_entry, state=state
):
yield (name, name, context)
def _recognize_strict(
self,

View File

@@ -0,0 +1,103 @@
"""Provides conditions for covers."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.helpers.condition import Condition, EntityConditionBase
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
"""Base condition for cover state checks."""
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]]
if domain_spec.value_source is not None:
return (
entity_state.attributes.get(domain_spec.value_source)
== domain_spec.target_value
)
return entity_state.state == domain_spec.target_value
def make_cover_is_open_condition(
*, device_classes: dict[str, str]
) -> type[CoverConditionBase]:
"""Create a condition for cover is open."""
class CoverIsOpenCondition(CoverConditionBase):
"""Condition for cover is open."""
_domain_specs = {
domain: CoverDomainSpec(
device_class=dc,
value_source=ATTR_IS_CLOSED if domain == DOMAIN else None,
target_value=False if domain == DOMAIN else STATE_ON,
)
for domain, dc in device_classes.items()
}
return CoverIsOpenCondition
def make_cover_is_closed_condition(
*, device_classes: dict[str, str]
) -> type[CoverConditionBase]:
"""Create a condition for cover is closed."""
class CoverIsClosedCondition(CoverConditionBase):
"""Condition for cover is closed."""
_domain_specs = {
domain: CoverDomainSpec(
device_class=dc,
value_source=ATTR_IS_CLOSED if domain == DOMAIN else None,
target_value=True if domain == DOMAIN else STATE_OFF,
)
for domain, dc in device_classes.items()
}
return CoverIsClosedCondition
DEVICE_CLASSES_AWNING: dict[str, str] = {DOMAIN: CoverDeviceClass.AWNING}
DEVICE_CLASSES_BLIND: dict[str, str] = {DOMAIN: CoverDeviceClass.BLIND}
DEVICE_CLASSES_CURTAIN: dict[str, str] = {DOMAIN: CoverDeviceClass.CURTAIN}
DEVICE_CLASSES_SHADE: dict[str, str] = {DOMAIN: CoverDeviceClass.SHADE}
DEVICE_CLASSES_SHUTTER: dict[str, str] = {DOMAIN: CoverDeviceClass.SHUTTER}
CONDITIONS: dict[str, type[Condition]] = {
"awning_is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_AWNING
),
"awning_is_open": make_cover_is_open_condition(
device_classes=DEVICE_CLASSES_AWNING
),
"blind_is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_BLIND
),
"blind_is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_BLIND),
"curtain_is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_CURTAIN
),
"curtain_is_open": make_cover_is_open_condition(
device_classes=DEVICE_CLASSES_CURTAIN
),
"shade_is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_SHADE
),
"shade_is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_SHADE),
"shutter_is_closed": make_cover_is_closed_condition(
device_classes=DEVICE_CLASSES_SHUTTER
),
"shutter_is_open": make_cover_is_open_condition(
device_classes=DEVICE_CLASSES_SHUTTER
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for covers."""
return CONDITIONS

View File

@@ -0,0 +1,80 @@
.condition_common_fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
awning_is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: awning
awning_is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: awning
blind_is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: blind
blind_is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: blind
curtain_is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: curtain
curtain_is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: curtain
shade_is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: shade
shade_is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: shade
shutter_is_closed:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: shutter
shutter_is_open:
fields: *condition_common_fields
target:
entity:
- domain: cover
device_class: shutter

View File

@@ -1,4 +1,36 @@
{
"conditions": {
"awning_is_closed": {
"condition": "mdi:storefront-outline"
},
"awning_is_open": {
"condition": "mdi:storefront-outline"
},
"blind_is_closed": {
"condition": "mdi:blinds-horizontal-closed"
},
"blind_is_open": {
"condition": "mdi:blinds-horizontal"
},
"curtain_is_closed": {
"condition": "mdi:curtains-closed"
},
"curtain_is_open": {
"condition": "mdi:curtains"
},
"shade_is_closed": {
"condition": "mdi:roller-shade-closed"
},
"shade_is_open": {
"condition": "mdi:roller-shade"
},
"shutter_is_closed": {
"condition": "mdi:window-shutter"
},
"shutter_is_open": {
"condition": "mdi:window-shutter-open"
}
},
"entity_component": {
"_": {
"default": "mdi:window-open",

View File

@@ -15,7 +15,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
INTENT_OPEN_COVER,
DOMAIN,
SERVICE_OPEN_COVER,
"Opening {}",
description="Opens a cover",
platforms={DOMAIN},
device_classes={CoverDeviceClass},
@@ -27,7 +26,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
INTENT_CLOSE_COVER,
DOMAIN,
SERVICE_CLOSE_COVER,
"Closing {}",
description="Closes a cover",
platforms={DOMAIN},
device_classes={CoverDeviceClass},

View File

@@ -0,0 +1,12 @@
"""Data models for the cover integration."""
from dataclasses import dataclass
from homeassistant.helpers.automation import DomainSpec
@dataclass(frozen=True, slots=True)
class CoverDomainSpec(DomainSpec):
"""DomainSpec with a target value for comparison."""
target_value: str | bool | None = None

View File

@@ -1,8 +1,112 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted covers.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted covers to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"awning_is_closed": {
"description": "Tests if one or more awnings are closed.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Awning is closed"
},
"awning_is_open": {
"description": "Tests if one or more awnings are open.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Awning is open"
},
"blind_is_closed": {
"description": "Tests if one or more blinds are closed.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Blind is closed"
},
"blind_is_open": {
"description": "Tests if one or more blinds are open.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Blind is open"
},
"curtain_is_closed": {
"description": "Tests if one or more curtains are closed.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Curtain is closed"
},
"curtain_is_open": {
"description": "Tests if one or more curtains are open.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Curtain is open"
},
"shade_is_closed": {
"description": "Tests if one or more shades are closed.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Shade is closed"
},
"shade_is_open": {
"description": "Tests if one or more shades are open.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Shade is open"
},
"shutter_is_closed": {
"description": "Tests if one or more shutters are closed.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Shutter is closed"
},
"shutter_is_open": {
"description": "Tests if one or more shutters are open.",
"fields": {
"behavior": {
"description": "[%key:component::cover::common::condition_behavior_description%]",
"name": "[%key:component::cover::common::condition_behavior_name%]"
}
},
"name": "Shutter is open"
}
},
"device_automation": {
"action_type": {
"close": "Close {entity_name}",
@@ -87,6 +191,12 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -2,89 +2,72 @@
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
def get_device_class_or_undefined(
hass: HomeAssistant, entity_id: str
) -> str | None | UndefinedType:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return get_device_class(hass, entity_id)
except HomeAssistantError:
return UNDEFINED
class CoverTriggerBase(EntityTriggerBase):
class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
"""Base trigger for cover state changes."""
_binary_sensor_target_state: str
_cover_is_closed_target_value: bool
_device_classes: dict[str, str]
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities by cover device class."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if get_device_class_or_undefined(self._hass, entity_id)
== self._device_classes[split_entity_id(entity_id)[0]]
}
def _get_value(self, state: State) -> str | bool | None:
"""Extract the relevant value from state based on domain spec."""
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
if domain_spec.value_source is not None:
return state.attributes.get(domain_spec.value_source)
return state.state
def is_valid_state(self, state: State) -> bool:
"""Check if the state matches the target cover state."""
if split_entity_id(state.entity_id)[0] == DOMAIN:
return (
state.attributes.get(ATTR_IS_CLOSED)
== self._cover_is_closed_target_value
)
return state.state == self._binary_sensor_target_state
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
return self._get_value(state) == domain_spec.target_value
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the transition is valid for a cover state change."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
if split_entity_id(from_state.entity_id)[0] == DOMAIN:
if (from_is_closed := from_state.attributes.get(ATTR_IS_CLOSED)) is None:
return False
return from_is_closed != to_state.attributes.get(ATTR_IS_CLOSED) # type: ignore[no-any-return]
return from_state.state != to_state.state
if (from_value := self._get_value(from_state)) is None:
return False
return from_value != self._get_value(to_state)
def make_cover_opened_trigger(
*, device_classes: dict[str, str], domains: set[str] | None = None
*, device_classes: dict[str, str]
) -> type[CoverTriggerBase]:
"""Create a trigger cover_opened."""
class CoverOpenedTrigger(CoverTriggerBase):
"""Trigger for cover opened state changes."""
_binary_sensor_target_state = STATE_ON
_cover_is_closed_target_value = False
_domains = domains or {DOMAIN}
_device_classes = device_classes
_domain_specs = {
domain: CoverDomainSpec(
device_class=dc,
value_source=ATTR_IS_CLOSED if domain == DOMAIN else None,
target_value=False if domain == DOMAIN else STATE_ON,
)
for domain, dc in device_classes.items()
}
return CoverOpenedTrigger
def make_cover_closed_trigger(
*, device_classes: dict[str, str], domains: set[str] | None = None
*, device_classes: dict[str, str]
) -> type[CoverTriggerBase]:
"""Create a trigger cover_closed."""
class CoverClosedTrigger(CoverTriggerBase):
"""Trigger for cover closed state changes."""
_binary_sensor_target_state = STATE_OFF
_cover_is_closed_target_value = True
_domains = domains or {DOMAIN}
_device_classes = device_classes
_domain_specs = {
domain: CoverDomainSpec(
device_class=dc,
value_source=ATTR_IS_CLOSED if domain == DOMAIN else None,
target_value=True if domain == DOMAIN else STATE_OFF,
)
for domain, dc in device_classes.items()
}
return CoverClosedTrigger

View File

@@ -38,9 +38,9 @@ from homeassistant.util.hass_dict import HassKey
from homeassistant.util.json import format_unserializable_data
from .const import DOMAIN, REDACTED, DiagnosticsSubType, DiagnosticsType
from .util import async_redact_data
from .util import async_redact_data, entity_entry_as_dict
__all__ = ["REDACTED", "async_redact_data"]
__all__ = ["REDACTED", "async_redact_data", "entity_entry_as_dict"]
_LOGGER = logging.getLogger(__name__)

View File

@@ -5,7 +5,10 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any, cast, overload
import attr
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import RegistryEntry
from .const import REDACTED
@@ -42,3 +45,16 @@ def async_redact_data[_T](data: _T, to_redact: Iterable[Any]) -> _T:
redacted[key] = [async_redact_data(item, to_redact) for item in value]
return cast(_T, redacted)
def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in ("_cache", "compat_aliases", "compat_name")
@callback
def entity_entry_as_dict(entry: RegistryEntry) -> dict[str, Any]:
"""Convert an entity registry entry to a dict for diagnostics.
This excludes internal fields that should not be exposed in diagnostics.
"""
return attr.asdict(entry, filter=_entity_entry_filter)

View File

@@ -20,14 +20,8 @@ DEVICE_CLASSES_DOOR: dict[str, str] = {
TRIGGERS: dict[str, type[Trigger]] = {
"opened": make_cover_opened_trigger(
device_classes=DEVICE_CLASSES_DOOR,
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
),
"closed": make_cover_closed_trigger(
device_classes=DEVICE_CLASSES_DOOR,
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
),
"opened": make_cover_opened_trigger(device_classes=DEVICE_CLASSES_DOOR),
"closed": make_cover_closed_trigger(device_classes=DEVICE_CLASSES_DOOR),
}

View File

@@ -10,6 +10,7 @@ from .const import DOMAIN
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.LIGHT,
Platform.NUMBER,

View File

@@ -0,0 +1,101 @@
"""EHEIM Digital binary sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.reeflex import EheimDigitalReeflexUV
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class EheimDigitalBinarySensorDescription[_DeviceT: EheimDigitalDevice](
BinarySensorEntityDescription
):
"""Class describing EHEIM Digital binary sensor entities."""
value_fn: Callable[[_DeviceT], bool | None]
REEFLEX_DESCRIPTIONS: tuple[
EheimDigitalBinarySensorDescription[EheimDigitalReeflexUV], ...
] = (
EheimDigitalBinarySensorDescription[EheimDigitalReeflexUV](
key="is_lighting",
translation_key="is_lighting",
value_fn=lambda device: device.is_lighting,
device_class=BinarySensorDeviceClass.LIGHT,
),
EheimDigitalBinarySensorDescription[EheimDigitalReeflexUV](
key="is_uvc_connected",
translation_key="is_uvc_connected",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda device: device.is_uvc_connected,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so binary sensors can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the binary sensor entities for one or multiple devices."""
entities: list[EheimDigitalBinarySensor[Any]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalReeflexUV):
entities += [
EheimDigitalBinarySensor[EheimDigitalReeflexUV](
coordinator, device, description
)
for description in REEFLEX_DESCRIPTIONS
]
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalBinarySensor[_DeviceT: EheimDigitalDevice](
EheimDigitalEntity[_DeviceT], BinarySensorEntity
):
"""Represent an EHEIM Digital binary sensor entity."""
entity_description: EheimDigitalBinarySensorDescription[_DeviceT]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT,
description: EheimDigitalBinarySensorDescription[_DeviceT],
) -> None:
"""Initialize an EHEIM Digital binary sensor entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
def _async_update_attrs(self) -> None:
self._attr_is_on = self.entity_description.value_fn(self._device)

View File

@@ -1,5 +1,19 @@
{
"entity": {
"binary_sensor": {
"is_lighting": {
"default": "mdi:lightbulb-outline",
"state": {
"on": "mdi:lightbulb-on"
}
},
"is_uvc_connected": {
"default": "mdi:lightbulb-off",
"state": {
"on": "mdi:lightbulb-outline"
}
}
},
"number": {
"day_speed": {
"default": "mdi:weather-sunny"

View File

@@ -33,6 +33,17 @@
}
},
"entity": {
"binary_sensor": {
"is_lighting": {
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"is_uvc_connected": {
"name": "UVC lamp connected"
}
},
"climate": {
"heater": {
"state_attributes": {

View File

@@ -11,7 +11,7 @@ from attr import asdict
from pyenphase.envoy import Envoy
from pyenphase.exceptions import EnvoyError
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.diagnostics import async_redact_data, entity_entry_as_dict
from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
@@ -111,8 +111,7 @@ async def async_get_config_entry_diagnostics(
if state := hass.states.get(entity.entity_id):
state_dict = dict(state.as_dict())
state_dict.pop("context", None)
entity_dict = asdict(entity)
entity_dict.pop("_cache", None)
entity_dict = entity_entry_as_dict(entity)
entities.append({"entity": entity_dict, "state": state_dict})
device_dict = asdict(device)
device_dict.pop("_cache", None)

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.5"],
"requirements": ["pyenphase==2.4.6"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -160,6 +160,23 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
_native_supported_color_modes: tuple[ESPHomeColorMode, ...]
_supports_color_mode = False
def _color_temp_to_cold_warm(self, color_temp_mired: float) -> tuple[float, float]:
"""Convert a color temperature in mireds to cold/warm white fractions.
Returns (cold_white, warm_white) normalized so the brighter channel
is 1.0.
"""
static_info = self._static_info
min_mireds = static_info.min_mireds
max_mireds = static_info.max_mireds
if max_mireds <= min_mireds:
return 1.0, 1.0
color_temp_clamped = min(max(color_temp_mired, min_mireds), max_mireds)
ww_frac = (color_temp_clamped - min_mireds) / (max_mireds - min_mireds)
cw_frac = 1 - ww_frac
max_frac = max(cw_frac, ww_frac)
return cw_frac / max_frac, ww_frac / max_frac
@property
@esphome_state_property
def is_on(self) -> bool:
@@ -241,12 +258,19 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
if (color_temp_k := kwargs.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
# Do not use kelvin_to_mired here to prevent precision loss
data["color_temperature"] = 1_000_000.0 / color_temp_k
color_temp_mired = 1_000_000.0 / color_temp_k
if color_temp_modes := _filter_color_modes(
color_modes, LightColorCapability.COLOR_TEMPERATURE
):
data["color_temperature"] = color_temp_mired
color_modes = color_temp_modes
else:
# Convert color temperature to explicit cold/warm white
# values to avoid ESPHome applying brightness to both
# master brightness and white channels (b² effect).
data["cold_white"], data["warm_white"] = self._color_temp_to_cold_warm(
color_temp_mired
)
color_modes = _filter_color_modes(
color_modes, LightColorCapability.COLD_WARM_WHITE
)
@@ -345,19 +369,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
self._native_supported_color_modes, LightColorCapability.COLD_WARM_WHITE
):
# Try to reverse white + color temp to cwww
static_info = self._static_info
min_ct = static_info.min_mireds
max_ct = static_info.max_mireds
color_temp = min(max(state.color_temperature, min_ct), max_ct)
white = state.white
ww_frac = (color_temp - min_ct) / (max_ct - min_ct)
cw_frac = 1 - ww_frac
cw, ww = self._color_temp_to_cold_warm(state.color_temperature)
return (
*rgb,
round(white * cw_frac / max(cw_frac, ww_frac) * 255),
round(white * ww_frac / max(cw_frac, ww_frac) * 255),
round(white * cw * 255),
round(white * ww * 255),
)
return (
*rgb,

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==44.3.1",
"aioesphomeapi==44.5.2",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.7.1"
],

View File

@@ -123,19 +123,13 @@ class EsphomeAssistSatelliteWakeWordSelect(
def __init__(self, entry_data: RuntimeEntryData, index: int = 0) -> None:
"""Initialize a wake word selector."""
if index < 1:
# Keep compatibility
key_suffix = ""
placeholder = ""
else:
key_suffix = f"_{index + 1}"
placeholder = f" {index + 1}"
self.entity_description = replace(
self.entity_description,
key=f"wake_word{key_suffix}",
translation_placeholders={"index": placeholder},
)
if index >= 1:
self.entity_description = replace(
self.entity_description,
key=f"wake_word_{index + 1}",
translation_key="wake_word_n",
translation_placeholders={"index": str(index + 1)},
)
EsphomeAssistEntity.__init__(self, entry_data)

View File

@@ -107,6 +107,12 @@
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
}
},
"pipeline_n": {
"name": "[%key:component::assist_pipeline::entity::select::pipeline_n::name%]",
"state": {
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
}
},
"vad_sensitivity": {
"name": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::name%]",
"state": {
@@ -116,11 +122,18 @@
}
},
"wake_word": {
"name": "Wake word{index}",
"name": "Wake word",
"state": {
"no_wake_word": "No wake word",
"okay_nabu": "Okay Nabu"
}
},
"wake_word_n": {
"name": "Wake word {index}",
"state": {
"no_wake_word": "[%key:component::esphome::entity::select::wake_word::state::no_wake_word%]",
"okay_nabu": "[%key:component::esphome::entity::select::wake_word::state::okay_nabu%]"
}
}
}
},

View File

@@ -5,7 +5,13 @@ from __future__ import annotations
from functools import partial
from typing import Any
from aioesphomeapi import EntityInfo, WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
from aioesphomeapi import (
EntityInfo,
WaterHeaterFeature,
WaterHeaterInfo,
WaterHeaterMode,
WaterHeaterState,
)
from homeassistant.components.water_heater import (
WaterHeaterEntity,
@@ -54,6 +60,7 @@ class EsphomeWaterHeater(
static_info = self._static_info
self._attr_min_temp = static_info.min_temperature
self._attr_max_temp = static_info.max_temperature
self._attr_target_temperature_step = static_info.target_temperature_step
features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
if static_info.supported_modes:
features |= WaterHeaterEntityFeature.OPERATION_MODE
@@ -63,6 +70,8 @@ class EsphomeWaterHeater(
]
else:
self._attr_operation_list = None
if static_info.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF:
features |= WaterHeaterEntityFeature.ON_OFF
self._attr_supported_features = features
@property
@@ -101,6 +110,24 @@ class EsphomeWaterHeater(
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the water heater on."""
self._client.water_heater_command(
key=self._key,
on=True,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
self._client.water_heater_command(
key=self._key,
on=False,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
platform_async_setup_entry,

View File

@@ -363,10 +363,12 @@ class EvoController(EvoClimateEntity):
Data validation is not required, it will have been done upstream.
"""
if service == EvoService.SET_SYSTEM_MODE:
mode = data[ATTR_MODE]
else: # otherwise it is EvoService.RESET_SYSTEM
mode = EvoSystemMode.AUTO_WITH_RESET
if service == EvoService.RESET_SYSTEM:
await self.coordinator.call_client_api(self._evo_device.reset())
return
mode = data[ATTR_MODE] # otherwise it is EvoService.SET_SYSTEM_MODE
if ATTR_PERIOD in data:
until = dt_util.start_of_local_day()

View File

@@ -27,7 +27,6 @@ from .coordinator import EvoDataUpdateCoordinator
# because supported modes can vary for edge-case systems
# Zone service schemas (registered as entity services)
CLEAR_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {}
SET_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
vol.Required(ATTR_SETPOINT): vol.All(
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
@@ -47,7 +46,7 @@ def _register_zone_entity_services(hass: HomeAssistant) -> None:
DOMAIN,
EvoService.CLEAR_ZONE_OVERRIDE,
entity_domain=CLIMATE_DOMAIN,
schema=CLEAR_ZONE_OVERRIDE_SCHEMA,
schema=None,
func="async_clear_zone_override",
)
service.async_register_platform_entity_service(
@@ -79,7 +78,6 @@ def setup_service_functions(
@verify_domain_control(DOMAIN)
async def set_system_mode(call: ServiceCall) -> None:
"""Set the system mode."""
assert coordinator.tcs is not None # mypy
payload = {
"unique_id": coordinator.tcs.id,
@@ -91,18 +89,11 @@ def setup_service_functions(
assert coordinator.tcs is not None # mypy
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode)
# Enumerate which operating modes are supported by this system
modes = list(coordinator.tcs.allowed_system_modes)
# Not all systems support "AutoWithReset": register this handler only if required
if any(
m[SZ_SYSTEM_MODE]
for m in modes
if m[SZ_SYSTEM_MODE] == EvoSystemMode.AUTO_WITH_RESET
):
hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode)
system_mode_schemas = []
modes = [m for m in modes if m[SZ_SYSTEM_MODE] != EvoSystemMode.AUTO_WITH_RESET]

View File

@@ -111,7 +111,7 @@ def get_model_selection_schema(
),
vol.Required(
CONF_BACKEND,
default=options.get(CONF_BACKEND, "s1"),
default=options.get(CONF_BACKEND, "s2-pro"),
): SelectSelector(
SelectSelectorConfig(
options=[

View File

@@ -31,7 +31,7 @@ TTS_SUPPORTED_LANGUAGES = [
]
BACKEND_MODELS = ["s1", "speech-1.5", "speech-1.6"]
BACKEND_MODELS = ["s2-pro", "s1", "speech-1.5", "speech-1.6"]
SORT_BY_OPTIONS = ["task_count", "score", "created_at"]
LATENCY_OPTIONS = ["normal", "balanced"]

View File

@@ -103,6 +103,8 @@ class FreeboxFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Initialize flow from zeroconf."""
zeroconf_properties = discovery_info.properties
host = zeroconf_properties["api_domain"]
port = zeroconf_properties["https_port"]
host = zeroconf_properties.get("api_domain")
if not host:
return self.async_abort(reason="missing_api_domain")
port = zeroconf_properties.get("https_port") or discovery_info.port
return await self.async_step_user({CONF_HOST: host, CONF_PORT: port})

View File

@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"missing_api_domain": "The discovered Freebox service did not provide the required API domain. Try again later or configure the Freebox manually."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from aiohttp import ClientError
@@ -56,3 +57,42 @@ class FreshrFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, _user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthentication confirmation."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
client = FreshrClient(session=async_get_clientsession(self.hass))
try:
await client.login(
reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
)
except LoginError:
errors["base"] = "invalid_auth"
except ClientError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
errors=errors,
)

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/freshr",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pyfreshr==1.2.0"]
}

View File

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

View File

@@ -2,9 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_account": "Cannot change the account username."
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -12,6 +10,15 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::freshr::config::step::user::data_description::password%]"
},
"description": "Re-enter the password for your Fresh-r account `{username}`."
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",

View File

@@ -8,7 +8,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["fritzconnection[qr]==1.15.1", "xmltodict==1.0.2"],
"ssdp": [
{

View File

@@ -29,9 +29,7 @@ rules:
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage:
status: todo
comment: we are close to the goal of 95%
test-coverage: done
# Gold
devices: done

View File

@@ -4,9 +4,9 @@ set_guest_wifi_password:
required: true
selector:
device:
integration: fritz
entity:
device_class: connectivity
integration: fritz
domain: update
password:
required: false
selector:
@@ -23,9 +23,9 @@ dial:
required: true
selector:
device:
integration: fritz
entity:
device_class: connectivity
integration: fritz
domain: update
number:
required: true
selector:

View File

@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {}
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = {
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
networks[i] = network_info
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["ssid"]
networks[i]["switch_name"] = network["NewSSID"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["ssid"]) == slugify(network["ssid"])
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key]
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
resp = await self._avm_wrapper.async_add_port_mapping(
await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping
)
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
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
self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper,
device_friendly_name: str,
network_num: int,
network_data: dict,
network_data: dict[str, Any],
) -> None:
"""Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor,
init_state=network_data["enabled"],
init_state=network_data["NewEnable"],
)
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled"
]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)

View File

@@ -179,7 +179,9 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
return PRESET_HOLIDAY
if self.data.summer_active:
return PRESET_SUMMER
if self.data.target_temperature == ON_API_TEMPERATURE:
if self.data.target_temperature == ON_API_TEMPERATURE or getattr(
self.data, "boost_active", False
):
return PRESET_BOOST
if self.data.target_temperature == self.data.comfort_temperature:
return PRESET_COMFORT

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260304.0"]
"requirements": ["home-assistant-frontend==20260312.0"]
}

View File

@@ -20,14 +20,8 @@ DEVICE_CLASSES_GARAGE_DOOR: dict[str, str] = {
TRIGGERS: dict[str, type[Trigger]] = {
"opened": make_cover_opened_trigger(
device_classes=DEVICE_CLASSES_GARAGE_DOOR,
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
),
"closed": make_cover_closed_trigger(
device_classes=DEVICE_CLASSES_GARAGE_DOOR,
domains={BINARY_SENSOR_DOMAIN, COVER_DOMAIN},
),
"opened": make_cover_opened_trigger(device_classes=DEVICE_CLASSES_GARAGE_DOOR),
"closed": make_cover_closed_trigger(device_classes=DEVICE_CLASSES_GARAGE_DOOR),
}

View File

@@ -2,12 +2,18 @@
from __future__ import annotations
import asyncio
import logging
from bleak.backends.device import BLEDevice
from gardena_bluetooth.client import CachedConnection, Client
from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation
from gardena_bluetooth.exceptions import CommunicationFailure
from gardena_bluetooth.const import AquaContour, DeviceConfiguration, DeviceInformation
from gardena_bluetooth.exceptions import (
CharacteristicNoAccess,
CharacteristicNotFound,
CommunicationFailure,
)
from gardena_bluetooth.parse import CharacteristicTime
from homeassistant.components import bluetooth
from homeassistant.const import CONF_ADDRESS, Platform
@@ -23,11 +29,13 @@ from .coordinator import (
GardenaBluetoothConfigEntry,
GardenaBluetoothCoordinator,
)
from .util import async_get_product_type
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
@@ -51,22 +59,43 @@ def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
return CachedConnection(DISCONNECT_DELAY, _device_lookup)
async def _update_timestamp(client: Client, characteristics: CharacteristicTime):
try:
await client.update_timestamp(characteristics, dt_util.now())
except CharacteristicNotFound:
pass
except CharacteristicNoAccess:
LOGGER.debug("No access to update internal time")
async def async_setup_entry(
hass: HomeAssistant, entry: GardenaBluetoothConfigEntry
) -> bool:
"""Set up Gardena Bluetooth from a config entry."""
address = entry.data[CONF_ADDRESS]
client = Client(get_connection(hass, address))
try:
async with asyncio.timeout(TIMEOUT):
product_type = await async_get_product_type(hass, address)
except TimeoutError as exception:
raise ConfigEntryNotReady("Unable to find product type") from exception
client = Client(get_connection(hass, address), product_type)
try:
chars = await client.get_all_characteristics()
sw_version = await client.read_char(DeviceInformation.firmware_version, None)
manufacturer = await client.read_char(DeviceInformation.manufacturer_name, None)
model = await client.read_char(DeviceInformation.model_number, None)
name = await client.read_char(
DeviceConfiguration.custom_device_name, entry.title
)
uuids = await client.get_all_characteristics_uuid()
await client.update_timestamp(dt_util.now())
name = entry.title
name = await client.read_char(DeviceConfiguration.custom_device_name, name)
name = await client.read_char(AquaContour.custom_device_name, name)
await _update_timestamp(client, DeviceConfiguration.unix_timestamp)
await _update_timestamp(client, AquaContour.unix_timestamp)
except (TimeoutError, CommunicationFailure, DeviceUnavailable) as exception:
await client.disconnect()
raise ConfigEntryNotReady(
@@ -83,7 +112,7 @@ async def async_setup_entry(
)
coordinator = GardenaBluetoothCoordinator(
hass, entry, LOGGER, client, uuids, device, address
hass, entry, LOGGER, client, set(chars.keys()), device, address
)
entry.runtime_data = coordinator

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from gardena_bluetooth.const import Sensor, Valve
from gardena_bluetooth.const import AquaContour, Sensor, Valve
from gardena_bluetooth.parse import CharacteristicBool
from homeassistant.components.binary_sensor import (
@@ -34,19 +34,26 @@ class GardenaBluetoothBinarySensorEntityDescription(BinarySensorEntityDescriptio
DESCRIPTIONS = (
GardenaBluetoothBinarySensorEntityDescription(
key=Valve.connected_state.uuid,
key=Valve.connected_state.unique_id,
translation_key="valve_connected_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
char=Valve.connected_state,
),
GardenaBluetoothBinarySensorEntityDescription(
key=Sensor.connected_state.uuid,
key=Sensor.connected_state.unique_id,
translation_key="sensor_connected_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.connected_state,
),
GardenaBluetoothBinarySensorEntityDescription(
key=AquaContour.frost_warning.unique_id,
translation_key="frost_warning",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
char=AquaContour.frost_warning,
),
)
@@ -60,7 +67,7 @@ async def async_setup_entry(
entities = [
GardenaBluetoothBinarySensor(coordinator, description, description.context)
for description in DESCRIPTIONS
if description.key in coordinator.characteristics
if description.char.unique_id in coordinator.characteristics
]
async_add_entities(entities)

View File

@@ -30,7 +30,7 @@ class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription):
DESCRIPTIONS = (
GardenaBluetoothButtonEntityDescription(
key=Reset.factory_reset.uuid,
key=Reset.factory_reset.unique_id,
translation_key="factory_reset",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
@@ -49,7 +49,7 @@ async def async_setup_entry(
entities = [
GardenaBluetoothButton(coordinator, description, description.context)
for description in DESCRIPTIONS
if description.key in coordinator.characteristics
if description.char.unique_id in coordinator.characteristics
]
async_add_entities(entities)

View File

@@ -43,6 +43,7 @@ def _is_supported(discovery_info: BluetoothServiceInfo):
ProductType.WATER_COMPUTER,
ProductType.AUTOMATS,
ProductType.PRESSURE_TANKS,
ProductType.AQUA_CONTOURS,
):
_LOGGER.debug("Unsupported device: %s", manufacturer_data)
return False
@@ -70,6 +71,7 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_read_data(self):
"""Try to connect to device and extract information."""
assert self.address
client = Client(get_connection(self.hass, self.address))
try:
model = await client.read_char(DeviceInformation.model_number)

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