Compare commits

...

489 Commits

Author SHA1 Message Date
Josef Zweck
aeebf67575 Revert "Add device trackers to enabled_by_default fixture (#134446)"
This reverts commit a47fa08a9b.
2025-01-09 13:30:23 +01:00
Kerey Roper
6a4160bcc4 add support for dimming/brightening X10 lamps (#130196)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-09 13:07:24 +01:00
Norbert Rittel
411d14c2ce Update title and description for setup dialog of thethingsnetwork (#134954) 2025-01-09 13:07:03 +01:00
Joost Lekkerkerker
d7315f4500 Add event entities to Overseerr (#134975) 2025-01-09 12:48:09 +01:00
epenet
c4ac648a2b Add select platform to onewire (#135181)
* Add select platform to onewire

* Add tests

* Apply suggestions from code review
2025-01-09 12:45:49 +01:00
epenet
e9616f38d8 Update scaffold to use internal _PLATFORM constant (#135177) 2025-01-09 12:41:29 +01:00
Steven B.
1550086dd6 Fix stale docstrings in tplink integration (#135183) 2025-01-09 12:37:32 +01:00
beginner2047
8e28b7b49b Add yue language support to Google Translate TTS (#134480) 2025-01-09 12:16:54 +01:00
epenet
4a33b1d936 Set PARALLEL_UPDATES to 0 in onewire (#135178) 2025-01-09 12:15:32 +01:00
epenet
8bfdbc173a Use snapshot_platform helper in onewire tests (#135176)
* Use snapshot_platform helper in onewire tests

* Snapshot device registry
2025-01-09 11:45:29 +01:00
Cyrill Raccaud
3ce4c47cfc Add uuid as unique_id to config entries for Cookidoo (#134831) 2025-01-09 11:28:28 +01:00
Steven B.
0d9ac25257 Add and cleanup tplink translations (#135120) 2025-01-09 11:28:10 +01:00
epenet
15e785b974 Move OneWire PLATFORM constant back to init (#135172) 2025-01-09 11:22:08 +01:00
Antoine Reversat
13527768cc Add outside temperature sensor to fujitsu_fglair (#130717) 2025-01-09 11:21:27 +01:00
Jan-Philipp Benecke
071e675d9d Mark docs-installation-parameters and docs-removal-instructions for inexogy as done (#135126) 2025-01-09 11:21:07 +01:00
epenet
316a61fcde Deprecate raw_value attribute in onewire entity (#135171)
* Drop raw_value attribute in onewire entity

* Deprecate only
2025-01-09 11:20:08 +01:00
Erik Montnemery
9901f3c3dd Add jitter to backup start time to avoid thundering herd (#135065) 2025-01-09 10:53:33 +01:00
J. Nick Koston
c9d8c59b45 Bump zeroconf to 0.138.1 (#135148) 2025-01-09 10:33:24 +01:00
Nikolay Vasilchuk
0184d8e954 Deprecate StarLine engine switch attributes (#133958)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-09 10:24:04 +01:00
Brynley McDonald
2f892678f6 Fix Flick Electric Pricing (#135154) 2025-01-09 10:09:04 +01:00
G Johansson
fe8cae8eb5 Make devices dynamic in Sensibo (#134935) 2025-01-09 09:02:14 +01:00
G Johansson
64752af4c2 Change minimum SQLite version to 3.40.1 (#135042)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-08 16:34:36 -10:00
G Johansson
c5f80dd01d Render select entity unavailable when active feature is missing in Sensibo (#135031) 2025-01-08 22:55:31 +01:00
Quentame
2704090418 Fix Météo-France setup in non French cities (because of failed next rain sensor) (#134782) 2025-01-08 22:51:37 +01:00
Tomer Shemesh
f01c860c44 Add support for Lutron Wood Tilt Blinds (#135057) 2025-01-08 22:40:13 +01:00
Shay Levy
bb4a497247 Impove LG webOS TV tests quality (#135130)
* Impove LG webOS TV tests quality

* Review comments
2025-01-08 23:12:09 +02:00
Joris Pelgröm
488c5a6b9f Use is in FlowResultType enum comparison in integration scaffold tests (#135133) 2025-01-08 22:10:29 +01:00
Louis Christ
acbd501ede Add DataUpdateCoordinator to bluesound integration (#135125) 2025-01-08 22:09:59 +01:00
Shay Levy
d06cd1ad3b Set PARALLEL_UPDATES in LG webOS TV (#135135) 2025-01-08 22:08:13 +01:00
Joris Pelgröm
4129697dd9 Add LetPot integration (#134925) 2025-01-08 21:38:52 +01:00
Ståle Storø Hauknes
4086d092ff Add suggested precision for Airthings BLE integration (#134985)
Add suggested precision
2025-01-08 21:05:42 +01:00
Arie Catsman
988a0639f4 Remove enphase_envoy config flow tests that make no sense (#133833) 2025-01-08 20:09:06 +01:00
Steven B.
c9c553047c Add quality scale file to tplink integration (#135017) 2025-01-08 20:08:04 +01:00
Arie Catsman
f05cffea17 Update enphase_envoy test_init to use str for unique_id and test for loaded config entry (#133810) 2025-01-08 20:06:51 +01:00
Hervé Cauwelier
d2a188ad3c Improve holidays config form and naming (#133663)
The holidays library is improving over time, let's make use of their
data for a more user-friendly experience.

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-01-08 17:19:28 +01:00
epenet
02e30edc6c Improve onewire options flow tests (#135109) 2025-01-08 17:00:35 +01:00
G Johansson
0e52ea482f Fix hvac_modes never empty in Sensibo (#135029) 2025-01-08 15:27:26 +01:00
epenet
d46be61b6f Split simple and recovery in onewire config-flow user tests (#135102) 2025-01-08 15:25:39 +01:00
epenet
f05e234c30 Refactor patching in onewire tests (#135070) 2025-01-08 15:14:51 +01:00
jb101010-2
bc09e825a9 Bump pysuezV2 to 2.0.3 (#135080) 2025-01-08 15:12:56 +01:00
Steven B.
6f6d485530 Raise HomeAssistantError from tplink light effect service (#135081) 2025-01-08 15:12:21 +01:00
Steven B.
63eb27df7b Add PARALLEL_UPDATES constant to tplink integration platforms (#135083) 2025-01-08 15:11:06 +01:00
elmurato
da29b2f711 Add quality_scale.yaml to Minecraft Server (#132551) 2025-01-08 15:00:33 +01:00
farkasdi
c2f6f93f1d Update addition logger string in fan.py (#135098) 2025-01-08 14:58:50 +01:00
Dawid Pietryga
39143a2e79 Add satel integra switches and alarm control panels unique_id (#129636) 2025-01-08 14:49:43 +01:00
dontinelli
99e65c38b0 Add binary sensors to fyta (#134900)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-08 14:41:33 +01:00
epenet
ec7d2f3731 Add quality_scale file to onewire (#134951) 2025-01-08 14:41:20 +01:00
epenet
d43187327f Remove rounding from onewire sensors (#135095) 2025-01-08 14:25:05 +01:00
Austin Mroczek
8be01ac9d6 TotalConnect improved config flow and test before setup (#133852)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-08 12:37:04 +01:00
Erik Montnemery
e052ab27f2 Fix DSMR migration (#135068) 2025-01-08 11:20:35 +00:00
Shay Levy
43ec63eabc Cleanup LG webOS TV name (#135028) 2025-01-08 12:06:02 +01:00
starkillerOG
7a2a6cf7d8 Add Reolink unexpected error translation (#134807) 2025-01-08 10:58:28 +01:00
puddly
eff440d2a8 Fix ZHA "referencing a non existing via_device" warning (#135008) 2025-01-08 10:51:57 +01:00
Andrew Sayre
3fea4efb9f Update pyheos to 0.9.0 (#134947)
Bump pyheos
2025-01-08 10:36:02 +02:00
Matthias Alphart
dc1928f3eb Delete KNX config storage when removing the integration (#135071) 2025-01-08 09:35:44 +01:00
epenet
f8618e65f6 Improve type hints in onewire tests (#134993) 2025-01-08 09:33:04 +01:00
G Johansson
e99aaed7fa Fix climate react type (#135030) 2025-01-08 10:30:14 +02:00
starkillerOG
d000558227 Fix channel retrieval for Reolink DUO V1 connected to a NVR (#135035)
fix channel retrieval for DUO V1 connected to a NVR
2025-01-08 10:28:01 +02:00
Thomas55555
7daf442271 Bump aioautomower to 2025.1.0 (#135039) 2025-01-08 10:26:48 +02:00
Cyrill Raccaud
b8f458458b Bump cookidoo-api to 0.12.2 (#135045)
fix cookidoo .co.uk countries and group api endpoint
2025-01-08 10:24:09 +02:00
J. Nick Koston
85ecb04abf Bump dbus-fast to 2.28.0 (#135049)
changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v2.24.3...v2.28.0
2025-01-08 10:19:03 +02:00
Joakim Sørensen
20db7fdc96 Implement upload retry logic in CloudBackupAgent (#135062)
* Implement upload retry logic in CloudBackupAgent

* Update backup.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* nit

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-01-08 08:16:18 +01:00
Diogo Gomes
a1d43b9387 Add weather warning sensor to IPMA (#134054)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-01-07 22:11:24 +00:00
dontinelli
de9c05ad53 Add new sensors to fyta (#135032) 2025-01-07 22:01:21 +01:00
Brett Adams
a01521b224 Bump pyaussiebb to 0.1.5 (#134943)
Bump
2025-01-07 21:54:39 +01:00
Norbert Rittel
2413bb4f52 Improve Huawei LTE suspend integration service description (#135021) 2025-01-07 19:30:56 -01:00
Steven B.
1496da8e94 Add data description translations to all tplink config flow steps (#135022) 2025-01-07 20:26:00 +01:00
Mick Vleeshouwer
802ad55493 Catch errors in automation (instead of raise unexpected error) in Overkiz (#135026)
Catch errors in automation (instead of raise unexpected error)
2025-01-07 20:24:39 +01:00
Allen Porter
48da88583f Bump voluptuous openapi to 0.0.6 (#134998) 2025-01-07 19:09:11 +01:00
Mick Vleeshouwer
0ab66a4ed1 Improve logic for event polling duration in Overkiz (#133617) 2025-01-07 19:06:57 +01:00
epenet
3b13c5bfdd Move OneWireConfigEntry type definition (#135004) 2025-01-07 19:04:31 +01:00
Kevin Worrel
42532e9695 Add Controller state sensor to screenlogic (#133827) 2025-01-07 19:02:34 +01:00
Luke Lashley
0dd9845501 Add total cleaning count sensor to Roborock (#135015) 2025-01-07 19:01:04 +01:00
Simone Chemelli
3a213b2d17 Use standard "entity_registry_enabled_by_default" fixture (#134962) 2025-01-07 18:21:26 +01:00
epenet
d155d93462 Set PARALLEL_UPDATES to 1 in onewire (#135006) 2025-01-07 17:19:48 +01:00
Erik Montnemery
5888b83f22 Validate device id when adding or updating entity registry entry (#134982) 2025-01-07 16:10:51 +00:00
epenet
471f77fea4 Add reconfigure to onewire (#134996)
* Add reconfigure to onewire

* Adjust _async_abort_entries_match
2025-01-07 17:08:53 +01:00
Allen Porter
c684b06734 Simplify roborock coordinator (#134700)
* Update roborock coordinator to require maps on startup

* Fix indent in merge
2025-01-07 07:09:32 -08:00
Erik Montnemery
393551d696 Fix DSMR migration (#134990) 2025-01-07 15:42:07 +01:00
Andrew Sayre
24b81df0e6 Update HEOS Quality Scale docs-related items (#134466)
Update docs items
2025-01-07 08:26:42 -06:00
Allen Porter
a66cf62b09 Update roborock tests to only load the platform under test (#134694) 2025-01-07 14:08:12 +01:00
epenet
901099325b Set parallel-updates and scan-interval explicitly in onewire (#134953) 2025-01-07 14:06:19 +01:00
epenet
30695cfef5 Simplify onewire config-flow (#134952) 2025-01-07 13:22:16 +01:00
Joakim Sørensen
5d2a8e8208 Increase cloud backup download timeout (#134961)
Increese download timeout
2025-01-07 13:18:02 +01:00
Norbert Rittel
4019045e7b Use sentence case, capitalize "IP Secure" and "ID" (#134966) 2025-01-07 12:08:37 +01:00
Norbert Rittel
ec2c8da1c5 Change "id" to uppercase for consistency (#134971) 2025-01-07 12:03:44 +01:00
J. Nick Koston
d1e8a2a32d Bump zeroconf to 0.137.2 (#134942) 2025-01-07 10:44:22 +01:00
Kelyan Pegeot Selme
feeee2d15e Bump renault-api to 0.2.9 (#134858)
* chore: Bump Renault api version

* Update requirements_all.txt

* Update requirements_test_all.txt
2025-01-07 10:13:40 +01:00
David Rapan
8a052177a4 Update Shelly integration: Remove double "Error fetching ..." from error messages (#134950)
refactor: Remove double "Error fetching" from error messages
2025-01-07 10:12:10 +02:00
Eli Schleifer
875727ed27 add proxy view for unifiprotect to grab snapshot at specific time (#133546)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-06 13:49:58 -10:00
Raphael Hehl
f1c62000e1 UnifiProtect Refactor light control methods to use new API (#134625) 2025-01-06 13:48:22 -10:00
Norbert Rittel
e38f21c4ef Fix spelling of "ID", slightly reword action descriptions (#134778) 2025-01-07 00:25:42 +01:00
Franck Nijhof
00c052bb22 Revert "Remove deprecated supported features warning in ..." (multiple) (#134933) 2025-01-07 00:08:02 +01:00
Simone Chemelli
111ef13a3f Add device tracker test for Vodafone Station (#134334) 2025-01-06 23:17:50 +01:00
J. Nick Koston
89c73f56b1 Migrate to using aiohttp-asyncmdnsresolver for aiohttp resolver (#134830) 2025-01-06 12:06:28 -10:00
Paulus Schoutsen
d13c14eedb Add support for extra_system_prompt to OpenAI (#134931) 2025-01-06 23:01:13 +01:00
G Johansson
9532e98166 Remove deprecated config entry import from bluesound (#134926) 2025-01-06 22:58:29 +01:00
G Johansson
6884d790ca Remove deprecated hdr switch from reolink (#134924) 2025-01-06 22:46:59 +01:00
G Johansson
6ab45f8c9e Bump holidays to 0.64 (#134922) 2025-01-06 22:45:04 +01:00
Artur Pragacz
7009a96711 Revert "Remove deprecated supported features warning in LightEntity" (#134927) 2025-01-06 22:39:24 +01:00
Josef Zweck
a47fa08a9b Add device trackers to enabled_by_default fixture (#134446) 2025-01-06 22:03:32 +01:00
Norbert Rittel
4eb23f3039 Remove excessive newline code, fix "ID", enhance descriptions (#134920) 2025-01-06 20:54:26 +01:00
Klaas Schoute
1c314b5c02 Bump powerfox to v1.2.0 (#134908) 2025-01-06 20:52:54 +01:00
Tomer Shemesh
edee58f114 Bump pylutron-caseta to 0.23.0 (#134906) 2025-01-06 20:44:06 +01:00
Manu
ef652e57d1 Add bring_api to loggers in Bring integration (#134897)
Add bring-api to loggers
2025-01-06 20:37:01 +01:00
Paulus Schoutsen
b956aa68da Handle discovering user configured Wyoming flow (#134916) 2025-01-06 20:26:49 +01:00
Michael
75ce89dc41 Bump py-synologydsm-api to 2.6.0 (#134914)
bump py-synologydsm-api to 2.6.0
2025-01-06 20:08:58 +01:00
Manu
a9540e893f Fix wrong power limit decimal place in IronOS (#134902) 2025-01-06 19:55:47 +01:00
Bram Kragten
dd5625436b Update frontend to 20250106.0 (#134905) 2025-01-06 19:11:01 +01:00
Paulus Schoutsen
7a484ee0ae Add extra prompt to assist pipeline and conversation (#124743)
* Add extra prompt to assist pipeline and conversation

* extra_prompt -> extra_system_prompt

* Fix rebase

* Fix tests
2025-01-06 12:58:42 -05:00
starkillerOG
e5c5d1bcfd Fix Reolink playback of recodings (#134652) 2025-01-06 18:54:32 +01:00
Glenn Reilly
56a9cd010e fix typo "looses" to "loses" in MQTT configuration message (#134894) 2025-01-06 17:59:31 +01:00
Steven B.
b7b5577f0c Bump python-kasa to 0.9.1 (#134893)
Bump tplink python-kasa dependency to 0.9.1
2025-01-06 16:58:33 +01:00
Norbert Rittel
0787257cc0 Use uppercase for "ID" and sentence-case for "name" / "icon" (#134890) 2025-01-06 16:30:40 +01:00
Thijs W.
54263f1325 Bump pymodbus version to 3.8.3 (#134809) 2025-01-06 14:56:17 +00:00
Luke Lashley
14d2f2c589 Add extra failure exceptions during roborock setup (#134889)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-06 15:46:21 +01:00
starkillerOG
c533f63a87 Add Decorquip virtual motion blinds integration (#134402) 2025-01-06 15:36:38 +01:00
Ludovic BOUÉ
cd30f75be9 Matter Battery replacement icon (#134460) 2025-01-06 15:35:42 +01:00
jb101010-2
527775a5f1 Bump pysuezV2 to 2.0.1 (#134769) 2025-01-06 15:27:23 +01:00
Klaas Schoute
99d7f462a0 Add heat meter to Powerfox integration (#134799) 2025-01-06 15:23:47 +01:00
J. Diego Rodríguez Royo
67e2379d2b Iterate over a copy of the list of programs at Home Connect select setup entry (#134684) 2025-01-06 15:21:02 +01:00
Norbert Rittel
fb0047ead0 Use correct uppercase for "ID" and sentence-case otherwise (#134815) 2025-01-06 15:15:31 +01:00
Norbert Rittel
9764d704bd Fix a few typos or grammar issues in asus_wrt (#134813) 2025-01-06 15:15:08 +01:00
Norbert Rittel
3690d7c2b4 Fix spelling of "set up", change "id" to uppercase (#134888) 2025-01-06 14:12:52 +00:00
Norbert Rittel
204b5989e0 Replace "id" with "ID" for consistency across HA (#134798) 2025-01-06 15:10:29 +01:00
G Johansson
3892f6d8f3 Remove deprecated binary sensor battery charging from technove (#134844) 2025-01-06 15:03:52 +01:00
J. Diego Rodríguez Royo
140ff50eaf Fix how function arguments are passed on actions at Home Connect (#134845) 2025-01-06 15:03:25 +01:00
Avi Miller
5ef06b1f33 Bump aiolifx-themes to update colors (#134846) 2025-01-06 15:02:57 +01:00
Manu
9638bee8de Bump pynecil to v4.0.1 (#134852) 2025-01-06 14:55:50 +01:00
Norbert Rittel
cd88a8cebd Fix missing sentence-casing etc. in several strings (#134775) 2025-01-06 14:53:28 +01:00
G Johansson
d896b4e66a Raise ImportError in python_script (#134792) 2025-01-06 14:52:40 +01:00
Robin Wohlers-Reichel
e4eb414be8 Bump solax to 3.2.3 (#134876) 2025-01-06 14:47:52 +01:00
Joakim Sørensen
fce5be928e Log upload BackupAgentError (#134865)
* Log out BackupAgentError

* Update homeassistant/components/backup/manager.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/backup/manager.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Format

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-01-06 14:19:34 +01:00
Joakim Sørensen
c4455c709b Log cloud backup upload response status (#134871)
Log the status of the upload response
2025-01-06 13:10:38 +01:00
Allen Porter
2c7a1446b8 Update Roborock config flow message when an account is already configured (#134854) 2025-01-06 11:24:06 +01:00
G Johansson
20cf21d88e Add horizontal swing to Sensibo (#132117)
* Add horizontal swing to Sensibo

* Fixes

* Only load select if already there

* Remove feature check

* Fixes

* Mods

* Last bits

* Mod

* Fixes

* Mods

* Fix test

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-06 11:20:11 +01:00
G Johansson
eafbf1d1fd Add get device capabilities action call for Sensibo (#134596)
* Add get device capabilities action call for Sensibo

* Tests

* Mod

* Fix services

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-06 11:09:08 +01:00
Michael
acd95975e4 Make ChunkAsyncStreamIterator an aiohttp helper (#134843)
make ChunkAsyncStreamIterator a generic aiohttp helper
2025-01-06 04:37:07 +01:00
G Johansson
bc22e34fc3 Add python_script to strict typing (#134822) 2025-01-05 22:22:54 -05:00
G Johansson
bf0cf1c30f Set single_config_entry in System monitor manifest (#134838) 2025-01-05 22:17:08 -05:00
G Johansson
e95bfe438b Pass config entry directly to coordinator in System monitor (#134837) 2025-01-05 22:16:58 -05:00
Norbert Rittel
0a457979ec Fix spelling of "ID", slightly reword action description (#134817)
This commit fixes the spelling of "ID" (uppercase for abbreviations) and slightly changes the action description to use third-person singular.

The latter ensures proper (machine) translations, keeping a descriptive style.
2025-01-05 18:36:17 -08:00
Allen Porter
2f295efb3f Update roborock to ensure every room has a name, falling back to a placeholder (#134733)
* Update roborock to ensure every room has a name, falling back to a placeholder

* Change Map to Room
2025-01-05 18:28:17 -08:00
J. Nick Koston
74613ae0c4 Bump habluetooth to 3.7.0 (#134833) 2025-01-05 12:44:37 -10:00
Raphael Hehl
4d4cfabfba Bump uiprotect to version 7.4.1 (#134829) 2025-01-05 11:25:44 -10:00
Norbert Rittel
7ae81bae4c Fix spelling of "ID" in Roku integration (#134779)
* Fix spelling of "ID" and "Ethernet" in Roku integration

Small commit replacing "id" with "ID" and "ethernet" with "Ethernet".

* Revert entity change

Web editor does not support the necessary tests.
2025-01-05 20:21:06 +01:00
Norbert Rittel
7ec10bfd6f Use uppercase "ID" in Home Connect strings (#134783) 2025-01-05 20:19:45 +01:00
cdnninja
d662a4465c Remove unneeded vesync device base class (#134499)
* Remove unneeded entity to make code cleaner

* Update light.py

* Update fan.py

* Typing.

* Update homeassistant/components/vesync/common.py

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* Wrap

---------

Co-authored-by: Allen Porter <allen.porter@gmail.com>
2025-01-05 09:18:52 -08:00
Markus Lanthaler
66b4b24612 Add latest Nighthawk WiFi 7 routers to V2 models (#134765)
Click `WiFi Routers` | `Nighthawk WiFi 7 Router` on https://www.netgear.com/support/ to see the list of devices
2025-01-05 16:59:34 +01:00
TheJulianJES
a2077405e2 Bump ZHA to 0.0.45 (#134726) 2025-01-05 16:49:58 +01:00
Lucas Gasenzer
f0a1a6c2ad Add ATTR_MODEL to DeviceInfo for Aranet (#134307)
* add ATTR_MODEL to DeviceInfo

* add tests for device context

* Upstream change type.name --> type.model

* fix test to represent model names
2025-01-05 16:16:12 +02:00
Norbert Rittel
32b7b5aa66 Small fixes in the strings file of the Vera integration (#134780)
Just for consistency …
2025-01-05 15:10:32 +01:00
Norbert Rittel
871a7d0dc1 Use uppercase "Chime" for product name, fix "MAC address" (#134748) 2025-01-05 14:31:02 +01:00
Michael
da807001ab Register base device entry during coordinator setup in AVM Fritz!Tools integration (#134764)
* register base device entry during coordinator setup

* make mypy happy
2025-01-05 08:16:33 -05:00
Norbert Rittel
a104799893 Fix spelling of "MAC (address)" and "Slide" name in slide_local (#134747) 2025-01-05 13:06:48 +01:00
Duco Sebel
45d1624d70 Bumb python-homewizard-energy to 7.0.1 (#134753) 2025-01-05 12:37:06 +01:00
Lucas Gasenzer
1059cf3f07 Bump aranet4 to 2.5.0 (#134752)
update aranet4 to 2.5.0 before pull request
2025-01-05 13:10:08 +02:00
Norbert Rittel
dd34a10934 Fix swapped letter order in "°F" and "°C" temperature units (#134750)
Fixes the wrong order "F°" and "C°" for the temperature units.
2025-01-05 10:43:32 +01:00
Klaas Schoute
d4f3dd2335 Bump powerfox to v1.1.0 (#134730) 2025-01-05 10:10:55 +01:00
Sid
0ecb1ea8cf Bump openwebifpy to 4.3.1 (#134746) 2025-01-05 10:04:59 +01:00
Sid
3d5a42749d Bump ruff to 0.8.6 (#134745) 2025-01-05 09:47:42 +01:00
Rylie Pavlik
a2c2d37eb1 Add support for "Lumin Smart Light" LD-0003 (#133328) 2025-01-04 13:53:16 -10:00
Andrew Sayre
f68c16586d Deprecate HEOS sign_in and sign_out actions (#134616) 2025-01-05 00:13:46 +01:00
Norbert Rittel
11d80065ef Fix spelling of "MAC (address)" and "Gateway" name (#134724) 2025-01-05 00:05:15 +01:00
Norbert Rittel
7012648bf8 Fix typos / grammar in nasweb integration (#134721) 2025-01-04 23:23:26 +01:00
Norbert Rittel
d96b2499e2 Fix typos / grammar in description of create_task action (#134705) 2025-01-04 23:21:23 +01:00
Norbert Rittel
a41bdfe0cc Fix wrong description of group.set action (#134697) 2025-01-04 23:20:30 +01:00
dontinelli
0d3872a4c7 Change from host to ip in zeroconf discovery for slide_local (#134709) 2025-01-04 21:28:47 +01:00
Norbert Rittel
65d8d071dd Remove excessive newline codes from squeezebox strings (#134682) 2025-01-04 18:42:28 +01:00
Allen Porter
bb97a16756 Add prompts to MCP server (#134619)
* Add prompts to MCP server

* Improve test coverage for get prompt error cases
2025-01-04 12:35:05 -05:00
Andrew Sayre
c9a607aa45 Clean-up HEOS entity event setup (#134683)
* Use async_on_remove

* Remove redundant signal clearing
2025-01-04 12:32:19 -05:00
Allen Porter
c7993eff99 Bump gcal_sync to 7.0.0 (#134687) 2025-01-04 12:30:57 -05:00
Cyrill Raccaud
8a880d6134 Cookidoo exotic domains (#134676) 2025-01-04 16:33:42 +01:00
Brynley McDonald
cc0fb80481 Fix Flick Electric authentication (#134611) 2025-01-04 16:21:21 +01:00
epenet
276806d3e1 Fix hive color tunable light (#134628) 2025-01-04 16:19:38 +01:00
Franck Nijhof
0589df7d95 Update demetriek to 1.1.1 (#134663) 2025-01-04 16:19:16 +01:00
Joost Lekkerkerker
aab676a313 Add Overseerr service to get requests (#134229)
* Add service to get requests

* Add service to get requests

* Add service to get requests

* fix

* Add tests
2025-01-04 15:53:15 +01:00
Joost Lekkerkerker
7f473b8260 Prefer a local webhook for Overseerr (#134667) 2025-01-04 15:39:47 +01:00
Shay Levy
fea4a00424 Remove LG WebOS TV legacy uuid migration (#134671) 2025-01-04 15:31:36 +01:00
Cyrill Raccaud
7d146ddae0 Bump cookidoo-api library to 0.11.1 of for Cookidoo (#134661) 2025-01-04 15:02:00 +01:00
Franck Nijhof
8f06e0903f Update peblar to 0.3.3 (#134658) 2025-01-04 14:34:45 +01:00
Maikel Punie
677ba3a6a6 Add velbus cover platform testcases (#134654) 2025-01-04 14:07:25 +01:00
Franck Nijhof
a322deaab8 Update twentemilieu to 2.2.1 (#134651) 2025-01-04 14:05:24 +01:00
Franck Nijhof
584439cade Update guppy to 3.1.5 (#134646) 2025-01-04 13:24:33 +01:00
Joost Lekkerkerker
baa13debcc Remove call to remove slide (#134647) 2025-01-04 12:56:58 +01:00
Cyrill Raccaud
1d42890748 Set logging in manifest for Cookidoo (#134645) 2025-01-04 12:23:22 +01:00
Norbert Rittel
622d23cadd Fix description of device_id field of reconnect_client actions (#134275) 2025-01-04 12:21:25 +01:00
G Johansson
ebeb2ecb09 Replace aioclient_mock in Sensibo tests (#134543) 2025-01-04 12:14:58 +01:00
Norbert Rittel
b3cb2928fc Fix typo 'devide_id', use uppercase for abbreviations ID and LED (#134634) 2025-01-04 12:01:39 +01:00
J. Nick Koston
b639466453 Bump bleak-esphome to 2.0.0 (#134580) 2025-01-04 11:30:41 +01:00
Teemu R.
69241e4ca6 Mention case-sensitivity in tplink credentials prompt (#134606) 2025-01-04 11:12:46 +01:00
Allen Porter
80371a865e Bump ical to 8.3.0 (#134617)
* Bump ical to 8.3.0

* Update snapshots
2025-01-04 09:49:56 +01:00
Maikel Punie
c9dbb205dd Add velbus diagnostics tests (#134621) 2025-01-04 09:10:34 +01:00
Raphael Hehl
197ff932af Bump uiprotect to version 7.2.0 (#134587) 2025-01-04 00:27:06 +01:00
G Johansson
287b7eec13 Clean up docstrings in Sensibo (#134591) 2025-01-04 00:24:51 +01:00
Maikel Punie
e6da6d9612 Add velbus light and sensor platform testcases (#134485)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-03 20:42:01 +01:00
peteS-UK
d4f38099ae Small fix to allow playing of expandable favorites on Squeezebox (#134572) 2025-01-03 20:28:05 +01:00
Manu
9f2cb7bf56 Add image platform to Habitica integration (#129009) 2025-01-03 20:23:43 +01:00
Maikel Punie
8a84abd50f Velbus diagnostics code cleanup (#134553) 2025-01-03 20:15:58 +01:00
Nerdix
b15e08ca9c Add sleep switch for all Foscam cameras if more than 1 camera are configured (#126064) 2025-01-03 20:15:09 +01:00
Norbert Rittel
3fb980901e Improve habitica action descriptions (#134563) 2025-01-03 20:07:30 +01:00
Joost Lekkerkerker
bd3a3fd26c Require at least bronze for new integrations (#134537)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-01-03 18:14:27 +00:00
Andrew Sayre
dfcb977a1d Add HEOS Reauth Flow (#134465) 2025-01-03 18:11:10 +00:00
Abílio Costa
94ad6ae814 Bump whirlpool-sixth-sense to 0.18.11 (#134562) 2025-01-03 17:45:27 +01:00
G Johansson
97aa93f92b Add supported features property in Sensibo (#134479) 2025-01-03 17:30:18 +01:00
G Johansson
ee025198e8 Update quality scale for Sensibo (#134551) 2025-01-03 17:28:02 +01:00
puddly
90265e2afd Move SiLabs firmware probing helper from ZHA into homeassistant_hardware (#131586)
* Move firmware probing helper out of ZHA and into hardware

* Add a unit test
2025-01-03 10:57:39 -05:00
Bram Kragten
a53554dad3 Update frontend to 20250103.0 (#134561) 2025-01-03 16:36:40 +01:00
Joost Lekkerkerker
2b6ad84cf5 Set Ituran to silver (#134538) 2025-01-03 16:31:31 +01:00
Erik Montnemery
92655fd640 Log cloud backup agent file list (#134556) 2025-01-03 16:30:14 +01:00
Maciej Bieniek
e43f72c452 Add support for xvoltage sensor for Shelly Plus UNI (#134261)
* Add support for xvoltage sensor

* Cleaning
2025-01-03 15:27:47 +01:00
Manu
9320ccfa4f Remove deprecated sensors in Habitica integration (#134320)
* Remove deprecated sensors

* remove todos/dailies also from enum
2025-01-03 14:48:26 +01:00
Erik Montnemery
336af8b551 Avoid early COMPLETED event when restoring backup (#134546) 2025-01-03 14:44:24 +01:00
starkillerOG
8a2f8dc736 Add Reolink proxy for playback (#133916) 2025-01-03 14:24:39 +01:00
Erik Montnemery
dc048bfcf5 Simplify error handling when creating backup (#134528) 2025-01-03 14:16:05 +01:00
Norbert Rittel
fb474827b5 Fix description of google_assistant.request_sync action (#134535) 2025-01-03 14:08:54 +01:00
Markus Adrario
eec5fb2133 Add Homee integration to Core (#133738)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-01-03 13:44:06 +01:00
Erik Montnemery
8ad7c522f4 Add backup as after_dependency of frontend (#134534) 2025-01-03 13:35:56 +01:00
Maikel Punie
c7f6630718 Velbus add init testcases (#134533) 2025-01-03 13:29:01 +01:00
Marc Mueller
afa95293dc Enable strict typing for pandora (#134536) 2025-01-03 13:23:39 +01:00
G Johansson
36582f9ac2 Refactor all Sensibo tests (#134478)
* Add me json

* Mods

* Mods

* More

* Mods

* Mods

* clean

* last bits

* Fix

* unique id

* return_value

* remove blocking

* Fix rebase
2025-01-03 12:44:47 +01:00
Ståle Storø Hauknes
19852ecc24 Add state_class to Airthings integration (#134503)
Add state class
2025-01-03 11:55:24 +01:00
Manu
5726d090b0 Add get_tasks action to Habitica integration (#127687)
Add get_tasks action
2025-01-03 11:53:30 +01:00
Indu Prakash
add401ffcf Add coordinator to vesync (#134087) 2025-01-03 11:33:16 +01:00
Robert Svensson
fd12ae2ccd Handle deCONZ color temp 0 is never used when calculating kelvin CT (#134521) 2025-01-03 10:51:20 +01:00
Franck Nijhof
e15eda3aa2 Only load Peblar customization update entity when present (#134526) 2025-01-03 10:51:05 +01:00
Dan Raper
cc0adcf47f Add switch platform to Ohme (#134347)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-01-03 10:39:41 +01:00
Franck Nijhof
06580ce10f Update peblar to v0.3.2 (#134524) 2025-01-03 10:37:39 +01:00
Erik Montnemery
b78e39da2d Fix activating backup retention config on startup (#134523) 2025-01-03 10:29:29 +01:00
G Johansson
46824a2a53 Add quality scale to Sensibo (#134296) 2025-01-03 10:23:25 +01:00
Joost Lekkerkerker
ee01289ee8 Bump python-overseerr to 0.5.0 (#134522) 2025-01-03 10:22:59 +01:00
Erik Montnemery
0bd22eabc7 Improve recorder schema migration error test (#134518) 2025-01-03 10:05:07 +01:00
Erik Montnemery
c901352bef Add error prints for recorder fatal errors (#134517) 2025-01-03 10:01:35 +01:00
Joost Lekkerkerker
23ed62c1bc Push Overseerr updates via webhook (#134187) 2025-01-03 08:26:01 +01:00
Paulus Schoutsen
0ef254bc9a Fix backup dir not existing (#134506) 2025-01-03 00:21:19 -05:00
rrooggiieerr
629d108078 Use the latest version of the pyserial-asyncio-fast library (#134501) 2025-01-03 03:15:46 +01:00
Marc Mueller
6f3544fa47 Add types package for pexpect (#134461) 2025-01-03 02:53:08 +01:00
Franck Nijhof
cb389d29ea Fix input_datetime.set_datetime not accepting 0 timestamp value (#134489) 2025-01-02 23:45:00 +01:00
Josef Zweck
ac26ca2da5 Bump aioacaia to 0.1.13 (#134496) 2025-01-03 00:28:29 +02:00
G Johansson
d5bcb73d33 Bump psutil to 6.1.1 (#134494) 2025-01-02 22:45:24 +01:00
Marc Mueller
e6a18357db Update pillow to 11.1.0 (#134469) 2025-01-02 22:36:14 +01:00
G Johansson
13ec0659ff Remove deprecated uptime sensor from qnap_qsw (#134493) 2025-01-02 22:29:50 +01:00
G Johansson
a7fb20ab58 Remove deprecated attributes from ecovacs (#134492) 2025-01-02 22:19:51 +01:00
G Johansson
657da47458 Remove worldclock config entry import (#134491) 2025-01-02 21:45:20 +01:00
Franck Nijhof
a4708876a9 Update peblar to 0.3.1 (#134486) 2025-01-02 21:41:54 +01:00
G Johansson
4239c5b557 Improve error strings in Sensibo (#134487) 2025-01-02 21:19:20 +01:00
G Johansson
836354bb99 Use username as config entry title in Sensibo (#134488) 2025-01-02 21:18:19 +01:00
Norbert Rittel
a7af042e57 Fix a few small typos in peblar (#134481) 2025-01-02 21:17:29 +01:00
Franck Nijhof
09476ade82 Remove sneaked in IronOS submodule (#134477) 2025-01-02 20:22:17 +01:00
Andrea Arcangeli
25937d7868 open_meteo: correct UTC timezone handling in hourly forecast (#129664)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-01-02 19:37:36 +01:00
Duco Sebel
4e74d14beb Include host in Peblar EV-Charger discovery setup description (#133954)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-01-02 19:34:51 +01:00
SparkyDan555
309b7eb436 Change Reolink person binary sensor icon (#134472) 2025-01-02 19:18:40 +01:00
Erik Montnemery
cf238cd8f7 Don't start recorder if a database from the future is used (#134467) 2025-01-02 18:56:23 +01:00
Robert Resch
ee46edffa3 Bump deebot-client to 10.1.0 (#134470) 2025-01-02 18:54:27 +01:00
Erik Montnemery
876b3423ba Improve hassio backup create and restore parameter checks (#134434) 2025-01-02 17:52:50 +01:00
Norbert Rittel
2752a35e23 Remove excessive newline codes from strings.json (#134468) 2025-01-02 17:43:49 +01:00
Craig Andrews
9e8df72c0d Improve is docker env checks (#132404)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Sander Hoentjen <sander@hoentjen.eu>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-01-02 17:21:49 +01:00
Bram Kragten
5439613bff Update frontend to 20250102.0 (#134462) 2025-01-02 17:17:57 +01:00
Ілля Піскурьов
3b5455bc49 Add support for specifying hvac_onoff_register value on modbus (#128366)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-01-02 15:18:05 +00:00
Noah Husby
104151d322 Remove deprecated YAML import from MPD (#134459) 2025-01-02 16:08:33 +01:00
Martin Hjelmare
a329828bdf Handle backup errors more consistently (#133522)
* Add backup manager and read writer errors

* Clean up not needed default argument

* Clean up todo comment

* Trap agent bugs during upload

* Always release stream

* Clean up leftover

* Update test for backup with automatic settings

* Fix use of vol.Any

* Refactor test helper

* Only update successful timestamp if completed event is sent

* Always delete surplus copies

* Fix after rebase

* Fix after rebase

* Revert "Fix use of vol.Any"

This reverts commit 28fd7a544899bb6ed05f771e9e608bc5b41d2b5e.

* Inherit BackupReaderWriterError in IncorrectPasswordError

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-01-02 15:45:46 +01:00
Marc Mueller
aa9e721e8b Update pexpect to 4.9.0 (#134450) 2025-01-02 15:36:44 +01:00
Josef Zweck
1b49f88be9 Bump aioacaia to 0.1.12 (#134454) 2025-01-02 15:33:22 +01:00
Marc Mueller
c345f2d548 Improve pandora media_player typing (#134447) 2025-01-02 13:55:59 +01:00
Manu
1d731875ae Remove deprecated yaml import from pyLoad integration (#134200) 2025-01-02 13:29:55 +01:00
Erik Montnemery
0c3489c1b3 Adjust language in backup integration (#134440)
* Adjust language in backup integration

* Update tests
2025-01-02 13:29:46 +01:00
Marc Mueller
c5865c6d18 Add types package for pyserial (#134444) 2025-01-02 13:21:20 +01:00
Norbert Rittel
e1a0fb2f1a Improve action descriptions with some more detail from the docs (#134120)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-01-02 12:52:51 +01:00
Erik Montnemery
d725cdae13 Initialize AppleTVConfigFlow.identifiers (#134443) 2025-01-02 12:49:03 +01:00
Erik Montnemery
e1bd82ea32 Export IncorrectPasswordError from backup integration (#134436) 2025-01-02 12:40:10 +01:00
Thomas55555
4bcc551b61 Add sw_version to apsystems (#134441) 2025-01-02 12:28:48 +01:00
Marc Mueller
08019e76d8 Update types packages (#134433) 2025-01-02 12:00:29 +01:00
Sven Naumann
0b32342bf0 Add mode selector to Twinkly (#134041) 2025-01-02 10:54:29 +00:00
Krzysztof Dąbrowski
add4e1a708 Add state attributes translations to GIOS (#134390) 2025-01-02 11:38:12 +01:00
Stefan Agner
fb3105bdc0 Improve Supervisor backup error handling (#134346)
* Raise Home Assistant error in case backup restore fails

This change raises a Home Assistant error in case the backup restore
fails. The Supervisor is checking some common issues before starting
the actual restore in background. This early checks raise an exception
(represented by a HTTP 400 error). This change catches such errors and
raises a Home Assistant error with the message from the Supervisor
exception.

* Add test coverage
2025-01-02 11:37:25 +01:00
Norbert Rittel
3845acd0ce Improve names and descriptions in neato.custom_cleaning action (#134399) 2025-01-02 11:04:23 +01:00
Manu
b45c68554c Remove habitipy references in Habitica integration (#134419) 2025-01-02 10:47:40 +01:00
G Johansson
8a45aa4c42 Add translations to all Sensibo errors (#134422)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-01-02 10:46:55 +01:00
G Johansson
51ccba12af Add action translations to Sensibo (#134420) 2025-01-02 10:45:20 +01:00
John Barreiros
c8699dc066 Add current_humidity state attribute to Google Nest climate entity (#134426) 2025-01-02 10:44:15 +01:00
ashionky
87454babfa Add debug log and Optimize code (#134328)
* debug log

* add sw_version hw_version

* log
2025-01-02 09:10:01 +01:00
Andrew Sayre
c9ff575628 Add HEOS options flow for optional authentication (#134105)
* Add heos options flow

* Add options flow tests

* Test error condition during options sign out

* Use credentials when setting up

* Update warning instructions

* Simplify exception logic

* Cover unknown command error condition

* Add test for options

* Correct const import location

* Review feedback

* Update per feedback

* Parameterize tests and remaining feedback

* Correct log level in init

* nitpick feedback
2025-01-02 09:07:34 +01:00
G Johansson
877d16273b Fix SQL sensor name (#134414) 2025-01-02 08:51:49 +01:00
Marc Mueller
dc5bfba902 Update mypy-dev to 1.15.0a1 (#134416) 2025-01-02 08:45:05 +01:00
TheJulianJES
5e7a405f34 Bump ZHA to 0.0.44 (#134427) 2025-01-02 08:43:38 +01:00
Matthew FitzGerald-Chamberlain
5228f3d85c Improve support for Aprilaire S86WMUPR (#133974) 2025-01-02 08:39:57 +01:00
G Johansson
2efc75fdf5 Add base entity to Mill (#134415) 2025-01-02 07:31:54 +01:00
Michael Hansen
a435fd12f0 Bump intents to 2025.1.1 (#134424) 2025-01-01 21:03:17 -05:00
Allen Porter
a5d0c3528c Add the Model Context Protocol Server integration (#134122)
* Add the Model Context Protocol Server integration

* Remove unusued code in init

* Fix comment wording

* Use util.uild for unique ids

* Set config entry title to the LLM API name

* Extract an SSE parser and update comments

* Update comments and defend against already closed sessions

* Shorten description

* Update homeassistant/components/mcp_server/__init__.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Change integration type to service

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2025-01-01 19:38:33 -05:00
Daniel Hjelseth Høyer
5e981d00a4 Add mill number platform (#134044)
* Mill number, max heating power

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Mill number, max heating power

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Mill number, max heating power

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Mill number, max heating power

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Mill number, max heating power

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* type

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

---------

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-01-01 23:25:42 +01:00
G Johansson
97dc72a6e2 Move available property to base entity in Sensibo (#134410)
* Move available property to base entity in Sensibo

* Fix test
2025-01-01 23:02:06 +01:00
Maikel Punie
088b097a03 Velbus select platform testcases (#134394) 2025-01-01 17:39:39 +01:00
Jan Bouwhuis
85c94e6403 Calculate number of discovery topics correctly (#134393) 2025-01-01 16:55:41 +01:00
Maikel Punie
a2ef1604af Add Velbus climate platform tests (#134387) 2025-01-01 16:01:02 +01:00
Sven Naumann
55dc4b0d2c Implement base entity class for Twinkly (#134382)
* implement base entity class for twinkly

* Update homeassistant/components/twinkly/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* super init

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-01 13:49:13 +01:00
Adam Štrauch
18e8a3b185 Add new ID LAP-V201S-AEUR for Vital200S AirPurifier in Vesync integration (#133999) 2025-01-01 13:10:40 +01:00
cdnninja
3a68a0a67f Vesync unload error when not all platforms used (#134166) 2025-01-01 13:03:39 +01:00
Josef Zweck
7ab2d2e07a Cleanup lamarzocco tests (#134383) 2025-01-01 13:00:14 +01:00
Keith
809629c0e2 Add integration for igloohome devices (#130657)
Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-01-01 12:55:04 +01:00
G Johansson
2be578a33f Add diagnostics to Trafikverket Weatherstation (#134314) 2025-01-01 12:32:35 +01:00
Maikel Punie
5cff79ce50 Add velbus switch platform testcases (#134207) 2025-01-01 12:11:27 +01:00
Brett Adams
513c8487c5 Check vehicle metadata (#134381) 2025-01-01 12:09:15 +01:00
Kenny Root
031de8da51 Bump zabbix-utils to 2.0.2 (#134373) 2025-01-01 11:42:16 +01:00
G Johansson
2e1463b9e9 Add placeholder url to Sensibo api description (#134342) 2024-12-31 20:09:49 -06:00
Norbert Rittel
9a58440296 Use "restore from" in field descriptions of restore_partial action (#134285) 2024-12-31 23:29:15 +01:00
Joost Lekkerkerker
26e0fcdb08 Improve Mealie set mealplan service (#130606)
* Improve Mealie set mealplan service

* Fix

* Fix
2024-12-31 17:06:42 -05:00
Bram Kragten
e835e41d59 Update frontend to 20241231.0 (#134363) 2024-12-31 17:04:28 -05:00
Niels Mündler
c53c0a13be Bump pysynthru version to 0.8.0 (#134294) 2024-12-31 23:03:35 +01:00
Jan Bouwhuis
8098122dfe Ensure an entity platform is added in mqtt tests (#134331) 2024-12-31 23:01:55 +01:00
starkillerOG
1d6ecbd1d5 Change Reolink test switch entity ID (#134339) 2024-12-31 22:57:43 +01:00
Dan Raper
c8276ec325 Bump ohmepy to 1.2.3 (#134348) 2024-12-31 22:54:20 +01:00
Josef Zweck
ddfad614ab Bump pylamarzocco to 1.4.6 (#134367) 2024-12-31 22:49:29 +01:00
Norbert Rittel
8eb21749b5 Remove leftover newline codes and periods from strings.json (#134354) 2024-12-31 22:39:07 +01:00
Josef Zweck
a6ba25d3d4 Use text selectors for lamarzocco config flow (#134368) 2024-12-31 22:38:31 +01:00
starkillerOG
1e70a0060b Add Reolink baby crying binary sensor (#134290)
* Add baby crying detection

* Bump reolink-aio to 0.11.6
2024-12-31 22:27:01 +01:00
Noah Husby
6c47f03d17 Bump aiorussound to 4.4.0 (#134366) 2024-12-31 22:21:14 +01:00
Markus Jacobsen
2054988790 Add Bang & Olufsen button Event entities (#127550)
* Add button events

* Remove unused common keys
Rename Preset to Favourite

* Add event testing

* Add check for Beoconnect Core

* Rename device controls

* Add test for Beoconnect core event entity creation

* Fix config entry type

* Add a type checking check before assertion

* Add icon translations

* Remove useless defined icons

* Remove base event class

* Update homeassistant/components/bang_olufsen/event.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2024-12-31 21:55:24 +01:00
tronikos
f1ad3040b8 Allow automations to pass any conversation_id for Google Generative AI (#134251) 2024-12-31 15:52:29 -05:00
Michael Hansen
53ca31c112 Bump hassil to 2.1.0 (#134359) 2024-12-31 15:52:15 -05:00
Michael Hansen
23459a0355 Revert speech seconds to 0.3 (#134360) 2024-12-31 20:04:41 +01:00
Simone Chemelli
a8bfe285bf Bump aioshelly to 12.2.0 (#134352) 2024-12-31 17:16:12 +01:00
Noah Husby
0888d1a169 Bump aiorussound to 4.3.0 (#134242)
* Bump aiorussound to 4.3.0

* Force CI
2024-12-31 16:14:24 +01:00
Dave T
8b20272272 Refactor and simplify config flow in generic camera (#134330)
Refactor and simplify config flow
2024-12-31 07:05:50 -08:00
Bram Kragten
06b33e5589 Set backup manager state to completed when restore is finished (#134283) 2024-12-31 15:01:06 +01:00
Brynley McDonald
9348569f90 Update Flick Electric API (#133475) 2024-12-31 14:28:24 +01:00
starkillerOG
4a9d545ffe Bump reolink-aio to 0.11.6 (#134286) 2024-12-31 10:31:40 +01:00
Simone Chemelli
277ee03145 Full test coverage for Vodafone Station sensor platform (#133285)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-12-31 09:55:54 +01:00
Norbert Rittel
6c9c17f129 Update description of the script toggle action (#134093) 2024-12-31 07:07:52 +00:00
Dave T
bf59241dab Add stream preview to options flow in generic camera (#133927)
* Add stream preview to options flow

* Increase test coverage

* Code review: use correct flow handler type in cast

* Restore test coverage to 100%

* Remove error and test that can't be triggered yet
2024-12-30 15:46:42 -08:00
Indu Prakash
57b7635b70 Bump pyvesync to 2.1.15 (#134156)
Bumped pyvesync to 2.1.15
2024-12-30 23:33:41 +01:00
G Johansson
4b96266647 Set parallel updates in Trafikverket Train (#134302) 2024-12-30 23:18:35 +01:00
G Johansson
6266a4153d Explicitly set config entry in Trafikverket Train coordinator (#134304) 2024-12-30 23:18:22 +01:00
G Johansson
a9949a0aab Use typed config entry everywhere in Trafikverket Train (#134303) 2024-12-30 23:17:21 +01:00
G Johansson
428a74fa48 Explicitly set config entry in Trafikverket Ferry coordinator (#134305) 2024-12-30 23:17:04 +01:00
G Johansson
9f1023b195 Explicitly set config entry in Trafikverket Weatherstation coordinator (#134310) 2024-12-30 23:16:41 +01:00
G Johansson
256fc54aa1 Set parallel updates in Trafiverket Weatherstation (#134309) 2024-12-30 23:16:16 +01:00
G Johansson
94c1b9a434 Use typed config entry everywhere in Trafikverket Weatherstation (#134308) 2024-12-30 23:15:54 +01:00
G Johansson
275c15e2ae Set parallel updates in Trafikverket Ferry (#134301) 2024-12-30 22:50:47 +01:00
G Johansson
9cdbcd93cd Use typed config entry everywhere in Trafikverket Ferry (#134300) 2024-12-30 22:48:33 +01:00
G Johansson
f2e856b8a2 Use typed config entry in Trafikverket Camera (#134299) 2024-12-30 22:48:22 +01:00
G Johansson
820f04e1e1 Add parallel updates to camera platform in Trafikverket Camera (#134298) 2024-12-30 22:48:12 +01:00
Noah Husby
b7541f098c Add discovery to Russound RIO (#134245) 2024-12-30 22:46:08 +01:00
Norbert Rittel
a345e80368 Replace unnecessary abbreviations in set_room_temperature action (#134278) 2024-12-30 21:28:38 +01:00
Norbert Rittel
7a3d9a9345 Replace "service" with "action" (#134279) 2024-12-30 21:26:53 +01:00
Dan Raper
a0fb6df5ba Add battery sensor to ohme (#134222)
* Add battery sensor to ohme

* Forgot the snapshots!

* Add translation key to battery

* Change car to vehicle and fix snapshot tests

* Fix snapshot again - not sure what was going on with my local dev env
2024-12-30 20:15:11 +01:00
Bram Kragten
04020d5a56 Update frontend to 20241230.0 (#134284) 2024-12-30 20:04:50 +01:00
Norbert Rittel
f785b17314 Fix two descriptions of yeelight actions (#134282) 2024-12-30 19:22:12 +01:00
Andrew Jackson
6631c57cfb Bump aiomealie to 0.9.5 (#134274) 2024-12-30 17:47:58 +01:00
Norbert Rittel
bc76dc3c34 Remove excessive period at end of action name (#134272) 2024-12-30 16:22:30 +01:00
Ludovic BOUÉ
ea4931ca3a Bump Python Matter server to 7.0.0 (Matter 1.4) (#132502)
* Matter 1.4 rename BridgedDevice device type

BREAKING change in the client: BridgedDevice is renamed to BridgedNode in the device types with Matter 1.4

* `ColorMode` enum type is renamed to `ColorModeEnum`

* Item `ColorTemperature` renamed to `ColorTemperatureMireds`

* Update ColorControl bitmaps and attributes

* Bump Python Matter server to 7.0.0 (Matter 1.4)

* Bump requirements to Python Matter server to 7.0.0
2024-12-30 15:41:14 +01:00
Arne Keller
dd20204bf0 ollama: update to 0.4.5 (#134265) 2024-12-30 14:42:46 +01:00
Norbert Rittel
ef46c62bc6 Make triggers and condition for monetary sensor consistent (#131184) 2024-12-30 13:47:16 +01:00
Alberto Geniola
2bb6e03a36 Bump elmax-api (#133845) 2024-12-30 13:46:53 +01:00
G Johansson
2288f89415 Fix duplicate sensor disk entities in Systemmonitor (#134139) 2024-12-30 13:38:48 +01:00
Josef Zweck
e7ab5afc14 Bump pylamarzocco to 1.4.5 (#134259)
* Bump pylamarzocco to 1.4.4

* Bump pylamarzocco to 1.4.5

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-12-30 12:27:32 +01:00
Adam Goode
4db88dfaff Quickly process unavailable metrics in Prometheus (#133219) 2024-12-30 12:05:33 +01:00
Joost Lekkerkerker
906c95048c Record LG WebOS TV Quality scale (#133732)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2024-12-30 11:45:44 +01:00
Manu
df38c1b1d7 Remove deprecated yaml import from OTP integration (#134196) 2024-12-30 11:12:16 +01:00
tronikos
af97bf1c5f Fix 400 This voice does not support speaking rate or pitch parameters at this time for Google Cloud Journey voices (#134255) 2024-12-30 09:20:35 +01:00
tronikos
a7c2d96ecf Avoid KeyError for ignored entries in async_step_zeroconf of Android TV Remote (#134250) 2024-12-30 10:13:51 +02:00
Noah Husby
1b06b4e45b Remove unused translations from Russound RIO (#134246) 2024-12-30 10:11:37 +02:00
Manu
b74b9bc360 Bump habiticalib to v0.3.2 (#134244) 2024-12-30 10:10:18 +02:00
Brett Adams
810689ce66 Handle missing application credentials in Tesla Fleet (#134237)
* Handle missing application credentials

* Add tests

* Test reauth starts

* Only catch ValueError
2024-12-29 22:21:18 -08:00
G Johansson
249d93574a Set Scrape sensor unavailable when errors (#134143) 2024-12-29 22:59:57 +01:00
Michael
e2c59f276a Bump aiopegelonline to 0.1.1 (#134230)
bump aiopegelonline to 0.1.1
2024-12-29 21:36:49 +01:00
Manu
9804e8aa98 Add reauth flow to Habitica integration (#131676)
* Add reauth flow to Habitica integration

* tests, invalid_credentials string

* test only api_key

* section consts

* test config entry

* test reauth is triggered

* set reauthentication-flow to done

* use consts in tests

* reauth_entry

* changes

* fix import

* changes
2024-12-29 21:12:36 +01:00
Paul Daumlechner
53e69af088 Bump pyvlx to 0.2.26 (#115483) 2024-12-29 10:00:26 -10:00
tronikos
1530edbe20 Bump opower to 0.8.7 (#134228)
* Bump opower to 0.8.7

* update deps
2024-12-29 11:44:33 -08:00
Paulus Schoutsen
7dbf32d693 Bump frontend to 20241229.0 (#134225) 2024-12-29 13:35:46 -05:00
Michael Hansen
49646ad994 Bump VoIP utils to 0.2.2 (#134219) 2024-12-29 11:56:27 -06:00
G Johansson
1e652db37f Use config entry runtime data in Open-Meteo (#134198) 2024-12-29 18:16:41 +01:00
Dan Raper
88d366b0c5 Add slot list service to ohme (#134170)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-12-29 18:07:12 +01:00
Lucas Gasenzer
65147f8d4c Fix Wake on LAN Port input as Box instead of Slider (#134216) 2024-12-29 18:03:41 +01:00
Simone Chemelli
52b919101a Bump aiocomelit to 0.10.1 (#134214) 2024-12-29 17:30:52 +01:00
Aaron Bach
24fd74d839 Change SimpliSafe websocket reconnection log to DEBUG-level (#134063)
* Change SimpliSafe websocket reconnection log to `DEBUG`-level

* revert
2024-12-29 11:23:44 -05:00
Marc Mueller
2599faa622 Fix method subtyping [helpers] (#134213) 2024-12-29 17:16:38 +01:00
Marc Mueller
3df91cfba5 Fix method subtyping [recorder] (#134212) 2024-12-29 17:16:11 +01:00
Marc Mueller
d3fab42c85 Fix method subtyping [knx] (#134211) 2024-12-29 16:41:23 +01:00
Marc Mueller
beb881492a Fix method subtyping [elkm1] (#134210) 2024-12-29 16:40:51 +01:00
Matthias Alphart
9d7c7f9fcf Update knx-frontend to 2024.12.26.233449 (#134184) 2024-12-29 16:39:37 +01:00
Shay Levy
419307a7c4 Bump aioswitcher to 6.0.0 (#134185) 2024-12-29 15:42:33 +01:00
G Johansson
409dc4ad48 Move coordinator to own file in Open-Meteo (#134197) 2024-12-29 15:25:40 +01:00
Michael
7704ef95a4 Make feedreader recoverable (#134202)
raise ConfigEntryNotReady on connection errors during setup
2024-12-29 15:08:15 +01:00
Manu
0db07a033b Migrate Habitica integration to habiticalib (#131032)
* Migrate data to habiticalib

* Add habiticalib to init and coordinator

* Migrate Habitica config flow to habiticalib

* migrate init to habiticalib

* migrate buttons to habiticalib

* migrate switch to habiticalib

* update habiticalib

* cast_skill action

* migrate update_score

* migrate transformation items action

* migrate quest actions

* fix fixture errors

* Migrate coordinator data and content

* bump habiticalib

* Remove habitipy and use wrapper in habiticalub

* changes

* some fixes

* minor refactoring

* class_needed annotation

* Update diagnostics

* do integration setup in coordinator setup

* small changes

* raise HomeAssistantError for TooManyRequestsError

* fix docstring

* update tests

* changes to tests/snapshots

* fix update_todo_item
2024-12-29 15:00:31 +01:00
Joost Lekkerkerker
4717eb3142 Bump python-overseerr to 0.4.0 (#134192) 2024-12-29 15:46:30 +02:00
Joost Lekkerkerker
c23f5c9f2c Make elevenlabs recoverable (#134094)
* Make elevenlabs recoverable

* Add tests for entry setup

* Use the same fixtures for setup and config flow

* Update tests/components/elevenlabs/test_setup.py

Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>

---------

Co-authored-by: Simon Sorg <simon.sorg@student.hpi.de>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>
2024-12-29 14:26:59 +01:00
Michael
873b078bb3 Make PEGELONLINE recoverable (#134199) 2024-12-29 14:07:45 +01:00
Manu
0dd93a18c5 Add button platform to IronOS integration (#133678)
* Add button platform to IronOS integration

* Add tests

* load platform

* refactor

* update tests
2024-12-29 12:39:13 +01:00
Maikel Punie
da96e2077b Add Velbus Button tests (#134186)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-12-29 11:55:52 +01:00
Manu
1d69cf11a5 Bump pynecil to v3.0.1 (#134174) 2024-12-29 10:06:29 +02:00
Manu
adb1fbbbc4 Add switch platform to IronOS integration (#133691)
* Add switch platform

* Add tests

* prevent switch bouncing

* some changes

* icons

* update tests

* changes
2024-12-28 21:59:06 +01:00
G Johansson
645f2e44b9 Fix Nord Pool empty response (#134033)
* Fix Nord Pool empty response

* Mods

* reset validate prices
2024-12-28 21:38:04 +01:00
Artur Pragacz
b3aede611a Fix Onkyo volume rounding (#134157) 2024-12-28 21:34:01 +01:00
jb101010-2
72a96249b1 Suez_water: clear quality scale (#134027)
* Suez_water: clear quality scale

Revert invalid done rules and mark inapplicable ones as exempted.

* Mark entity disabled as todo

* Mark devices as todo

* missing push

* Update homeassistant/components/suez_water/quality_scale.yaml

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Update quality_scale.yaml

* Update quality_scale.yaml again

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2024-12-28 20:59:11 +01:00
Joost Lekkerkerker
80dbce14ec Add binary sensor to Tile (#134153) 2024-12-28 16:49:14 +01:00
Manu
0376f75ee3 Bump pynecil to v3.0.0 (#134151) 2024-12-28 16:48:28 +01:00
jb101010-2
e58bd62c68 Suez_water: use meter id as unique_id (#133959)
* Suez_water: use meter id as unique_id

* Review fixes

* No more afraid check :)

* review again

* Apply suggestions from code review

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-12-28 16:25:10 +01:00
Matthias Alphart
6dbcd130b0 Add quality_scale.yaml for KNX (#133937)
* Add quality_scale.yaml

* Update quality_scale.yaml
2024-12-28 16:24:49 +01:00
Andrew Jackson
4639f57014 Remove deprecated Mastodon yaml config import (#134040)
* Remove Mastodon yaml import

* Revert removal of async_migrate_entry
2024-12-28 16:22:32 +01:00
G Johansson
4080455c12 Use x,y in roborock action call (#134133)
* Use x,y in roborock action call

* Fix description
2024-12-28 16:12:09 +01:00
Joost Lekkerkerker
df7d518f38 Add versions to Tile device (#134150)
* Add versions to Tile device

* Add versions to Tile device
2024-12-28 16:04:36 +01:00
Joost Lekkerkerker
47adfb574f Bump python-overseerr to 0.3.0 (#134147)
Bump Overseerr to 0.3.0
2024-12-28 15:44:15 +01:00
Joost Lekkerkerker
4c5d0c2ec4 Add Tile device tracker tests (#134137) 2024-12-28 15:36:56 +01:00
G Johansson
4febe43021 Add missing device classes in scrape (#134141) 2024-12-28 15:36:23 +01:00
Maikel Punie
af13979855 Add Velbus binary sensor tests (#134132)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-12-28 14:57:48 +01:00
Marc Mueller
d9f2140df3 Add ClassVar annotation for singleton patterns (#134135) 2024-12-28 13:17:15 +01:00
Joost Lekkerkerker
cc80108629 Bump yt-dlp to 2024.12.23 (#134131) 2024-12-28 13:13:07 +01:00
Joost Lekkerkerker
16af76b968 Add Tile device tests (#134138) 2024-12-28 13:10:13 +01:00
Joost Lekkerkerker
590f0ce61f Refactor Tile tests (#134130) 2024-12-28 12:37:21 +01:00
Allen Porter
14059c6df8 Remove unused parameters from function calls in rainbird (#134124)
Remove unused parameters from rainbird function calls
2024-12-28 11:34:27 +00:00
Joost Lekkerkerker
268c21addd Add Overseerr integration (#133981)
* Add Overseerr integration

* Add Overseerr integration

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix
2024-12-28 11:50:36 +01:00
Andre Lengwenus
565fa4ea1f Remove incorrect device check in LCN events (#134116) 2024-12-28 09:26:49 +01:00
Raj Laud
28cd7f2473 Bump pysqueezebox to v0.11.1 (#134097) 2024-12-28 09:24:22 +01:00
Noah Husby
aceb1b39ba Add mute support to Russound RIO (#134118) 2024-12-28 09:22:13 +01:00
Allen Porter
6edf06f8a4 Converge stream av open methods, options, and error handling (#134020)
* Converge stream av open methods, options, and error handling

* Remove exception that is never thrown

* Update exceptions thrown in generic tests

* Increase stream test coverage
2024-12-27 18:47:33 -08:00
Noah Husby
07ae9b15d0 Bump aiorussound to 4.2.0 (#134117) 2024-12-27 18:23:57 -08:00
G Johansson
d676169b04 Cleanup devices in Nord Pool from reconfiguration (#134043)
* Cleanup devices in Nord Pool from reconfiguration

* Mods

* Mod
2024-12-27 21:33:37 +01:00
Noah Husby
24ce3d7daa Remove deprecated yaml import for Russound RIO (#134072) 2024-12-27 21:27:33 +01:00
Joost Lekkerkerker
417e736746 Migrate Tile to use entry.runtime_data (#134107) 2024-12-27 21:25:36 +01:00
Cyrill Raccaud
bb8d4ca255 Add unit test for sensors in swiss public transport (#134115)
* add unit test for sensors

* clean up
2024-12-27 21:21:45 +01:00
Joost Lekkerkerker
375af6cb1c Introduce base entity for Tile (#134109) 2024-12-27 21:18:01 +01:00
Jan Bouwhuis
263e0acd3a Set PARALLEL_UPDATES for incomfort entity platforms (#134110) 2024-12-27 20:43:30 +01:00
Erwin Douna
da531d0e4e Bump Tado to 0.18.5 (#133988) 2024-12-27 20:26:19 +01:00
Joost Lekkerkerker
844e36c8fe Bump python-homeassistant-analytics to 0.8.1 (#134101) 2024-12-27 20:21:12 +01:00
Joost Lekkerkerker
9976c07f89 Remove YAML import from Tile (#134108) 2024-12-27 20:15:48 +01:00
Aaron Bach
7df9d2e938 Bump pytile to 2024.12.0 (#134103) 2024-12-27 20:04:35 +01:00
Joost Lekkerkerker
52318f5f37 Extract Tile coordinator in separate file (#134104) 2024-12-27 19:30:13 +01:00
Joost Lekkerkerker
b9c2b3f7e3 Remove Tile unique id migration (#134106) 2024-12-27 19:25:10 +01:00
Andrew Sayre
a9ff5b8007 Bump pyheos to v0.8.0 (#134069)
Bump pyheos and update usage
2024-12-27 11:01:35 -06:00
Joost Lekkerkerker
7076ba7c9d Make google tasks recoverable (#134092) 2024-12-27 08:52:33 -08:00
Josef Zweck
5e0088feaa Add azure_data_explorer to microsoft brand (#134088) 2024-12-27 15:36:07 +01:00
Franck Nijhof
f8399b2c0f Revert "Add state_class to EcoWittSensorTypes.DEGREE" (#134079) 2024-12-27 13:17:47 +01:00
Matthias Alphart
415fdf4956 Fix KNX config flow translations and add data descriptions (#134078)
* Fix KNX config flow translations and add data descriptions

* Update strings.json

* typo
2024-12-27 12:59:52 +01:00
Noah Husby
ad89004189 Remove timeout from Russound RIO initialization (#134070) 2024-12-27 11:01:10 +01:00
Noah Husby
b6afbe4b29 Bump aiorussound to 4.1.1 (#134058)
* Bump aiorussound to 4.1.1

* Trigger Build

* Trigger Build
2024-12-26 22:03:50 -06:00
Cyrill Raccaud
402340955e Fix swiss public transport line field none (#133964)
* fix #133116

The line can theoretically be none, when no line info is available (lets say walking sections first?)

* fix line field

* add unit test with missing line field
2024-12-27 00:24:47 +01:00
Raphael Hehl
b2a160d926 Roborock Add vacuum_goto service (#133994)
* Roborock Add vacuum_goto service to control vacuum movement to specified coordinates

* roborock Add type specification for x_coord and y_coord in vacuum_goto service

* roborock Add get_current_position service to retrieve vacuum's current coordinates

* Rename vacuum services for clarity and consistency

* Apply suggestions from code review

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Add integration field to vacuum service targets for Roborock

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2024-12-27 00:20:09 +01:00
Thomas Kunzfeld
9840785363 Add state_class to EcoWittSensorTypes.DEGREE (#134004)
Add state_class to EcoWittSensorTypes.DEGREE (#129260)
2024-12-27 00:12:54 +01:00
jb101010-2
a53c92d4b5 Suez_water: remove redundant log on refresh failure (#134025)
Suez_water: remove redundent log on refresh failure
2024-12-27 00:05:28 +01:00
Marc Mueller
adc97b6c15 Fix unifiprotect DeprecationWarnings in tests (#134060) 2024-12-26 23:50:03 +01:00
Jan Bouwhuis
7b2a5d0684 Remove mqtt publish templates after 6 months of deprecation (#134056) 2024-12-26 23:25:44 +01:00
Diogo Gomes
acb511d395 Bump pyipma to 3.0.8 (#134055)
bump pyipma
2024-12-26 21:01:53 +00:00
Norbert Rittel
c025390c6c Replace "service" with "action" plus fixed descriptions (#134053) 2024-12-26 15:39:18 -05:00
J. Nick Koston
942fbdedcf Ensure all states have been migrated to use timestamps (#134007) 2024-12-26 07:48:55 -10:00
Allen Porter
3bfb6707e9 Fix Nest ConfigEntry typing (#134021) 2024-12-26 09:27:20 -08:00
Norbert Rittel
5172139579 Use correct uppercase for abbreviations (#134028)
Fix the spelling of "SSDP" and "MAC" (address) to ensure proper translations.
2024-12-26 11:09:30 +01:00
Norbert Rittel
cfb43c7b58 Fix typo in get_command action description (#134026) 2024-12-26 09:56:08 +01:00
Allen Porter
45657ece7c Improve Google Tasks error messages (#134023) 2024-12-26 09:53:20 +01:00
Erwin Douna
f7fe2f2122 Tado update code owners (#133987)
Update code owners
2024-12-26 09:13:24 +01:00
Allen Porter
c75222e63c Bump python-google-nest-sdm to 7.0.0 (#134016)
Update python-google-nest-sdm to 7.0.0
2024-12-26 00:03:44 -05:00
Brett Adams
299250ebec Bump Tesla Fleet API library (#134019)
Bump Tesla Fleet
2024-12-25 23:26:55 -05:00
Josef Zweck
ed8e242049 Bump pylamarzocco to 1.4.3 (#134008) 2024-12-25 16:25:13 -08:00
Cyrill Raccaud
95e4a40ad5 Update silver docs for swiss public transport (#134001)
update docs
2024-12-25 21:36:30 +01:00
Christopher Fenner
e61717ce7a Fulfill IQS rule docs-removal-instructions in ViCare integration (#133982)
update iqs state
2024-12-25 15:30:33 +01:00
Cyrill Raccaud
73b6bd8bd3 Add config flow data description to swiss public transport (#133997)
* add config flow data description

* improve strings
2024-12-25 15:20:09 +01:00
Cyrill Raccaud
60774c69cd Add clear shopping list button for Cookidoo (#133583)
* add clear button

* set clear button to disabled per default

* add actions exception
2024-12-25 14:58:19 +01:00
Cyrill Raccaud
c383b41a12 Add parallel updates to swiss public transport (#133996)
add parallel updates
2024-12-25 14:55:34 +01:00
J. Nick Koston
05a8b773b9 Bump numpy to 2.2.1 (#133844)
* Bump numpy to 2.2.1

changelog: https://github.com/numpy/numpy/compare/v2.2.0...v2.2.1

* make sure ninja is up to date

* Revert "make sure ninja is up to date"

This reverts commit a26dd8b768.

* test

* Revert "test"

This reverts commit 972f40e3ee.

* try a single build

* try a single build

* Revert "Revert "test""

This reverts commit ec282ce021.

* Revert "Revert "Revert "test"""

This reverts commit 315599cbae.

* Revert "try a single build"

This reverts commit 63529dd2c5.

* Revert "try a single build"

This reverts commit 7058ae9288.
2024-12-25 11:27:00 +02:00
G-Two
1bee423c22 Bump subarulink to 0.7.13 (#133970) 2024-12-25 10:13:04 +02:00
Marc Mueller
687afd23bc Add pip wheel build constraints to fix numpy builds (#133962) 2024-12-24 15:06:21 -10:00
cdnninja
0020c48a15 Update pyvesync version (#131433) 2024-12-24 17:51:40 +01:00
Bram Kragten
760cbcc596 Update frontend to 20241224.0 (#133963) 2024-12-24 16:41:36 +01:00
Philipp Danner
da8f4e5b57 fix "Slow" response leads to "Could not find a charging station" #124129 (#133889)
fix #124129
2024-12-24 14:00:34 +01:00
Claudio Ruggeri - CR-Tech
5c0659c8df Fix reload modbus component issue (#133820)
fix issue 116675
2024-12-24 13:57:18 +01:00
Marc Mueller
15806c2af6 Update Jinja2 to 3.1.5 (#133951) 2024-12-24 13:44:09 +01:00
Maikel Punie
97d8d16cc5 Bump velbusaio to 2024.12.3 (#133939) 2024-12-24 12:35:22 +02:00
Khole
33435fa36f Hive: Fix error when device goes offline (#133848) 2024-12-24 10:42:35 +01:00
Joost Lekkerkerker
6fc1cfded9 Use SignedSession in Xbox (#133938) 2024-12-24 10:17:02 +01:00
Franck Nijhof
a9d6a42781 Update apprise to v1.9.1 (#133936) 2024-12-24 10:15:21 +01:00
Kevin Worrel
f2a706ecf7 Make screenlogic state enums lowercase (#133866) 2024-12-24 09:12:18 +01:00
G-Two
4a2ae7f6fd Stop using shared aiohttp client session for Subaru integration (#133931) 2024-12-24 08:59:51 +01:00
Franck Nijhof
771ead9d7b Prevent imports from tests in core codebase (#133928)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2024-12-24 08:40:05 +01:00
Jordi
2d5e2aa4b4 Add Harvey virtual integration (#133874)
Add harvey virtual integration
2024-12-24 08:01:50 +01:00
Franck Nijhof
6f11524b84 Remove myself as codeowner from Tuya integration (#133921) 2024-12-24 07:55:44 +01:00
Dave T
561f319e3b Fix missing % in string for generic camera (#133925)
Fix missing % in generic camera string
2024-12-24 07:45:13 +01:00
Franck Nijhof
0c9ec4b699 Fix Peblar import in data coordinator (#133926) 2024-12-24 07:42:48 +01:00
Brett Adams
cbb2930805 Slow down polling in Teslemetry (#133924) 2024-12-24 01:59:36 +01:00
Franck Nijhof
aa29a93fbe Remove myself as codeowner from Plugwise (#133920) 2024-12-24 01:34:23 +01:00
J. Nick Koston
ff4ba553c4 Sort integration platforms preload list (#133905)
* Sort integration platforms preload list

https://github.com/home-assistant/core/pull/133856#discussion_r1895385026

* sort

* Sort them all

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-12-23 23:38:59 +01:00
Thomas55555
2f101c5054 Catch ClientConnectorError and TimeOutError in APSystems (#132027) 2024-12-23 22:49:59 +01:00
karwosts
72e2b835d9 Fix a history stats bug when window and tracked state change simultaneously (#133770) 2024-12-23 22:47:26 +01:00
Martin Mrazik
8f6e4cd294 Map RGB+CCT to RGB for WLED (#133900) 2024-12-23 22:26:38 +01:00
Mick Vleeshouwer
bd0edd4996 Revise codeowners for Overkiz (#133784) 2024-12-23 22:24:22 +01:00
J. Nick Koston
3f441e7090 Ensure cloud and recorder backup platforms do not have to wait for the import executor (#133907)
* Ensure cloud and recorder backup platforms do not have to wait for the import executor

partially fixes #133904

* backup.backup as well
2024-12-23 22:19:28 +01:00
Abílio Costa
253098d79c Mark missing IQS requirements for Idasen Desk as done (#133910) 2024-12-23 21:38:27 +01:00
Abílio Costa
53ebf84339 Add cronsim to default dependencies (#133913) 2024-12-23 21:34:36 +01:00
J. Nick Koston
7cfbc3eeae Fix duplicate call to async_register_preload_platform (#133909) 2024-12-23 09:20:44 -10:00
Franck Nijhof
8d32531bc1 Bump version to 2025.2.0dev0 (#133893) 2024-12-23 17:54:32 +01:00
Simon
30d95f37d8 Add removal instructions to ElevenLabs (#133895) 2024-12-23 18:37:19 +02:00
1038 changed files with 54238 additions and 19691 deletions

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 11
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.1"
HA_SHORT_VERSION: "2025.2"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
# 10.3 is the oldest supported version

View File

@@ -76,8 +76,20 @@ jobs:
# Use C-Extension for SQLAlchemy
echo "REQUIRE_SQLALCHEMY_CEXT=1"
# Add additional pip wheel build constraints
echo "PIP_CONSTRAINT=build_constraints.txt"
) > .env_file
- name: Write pip wheel build constraints
run: |
(
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
# this caused the numpy builds to fail
# https://github.com/scikit-build/ninja-python-distributions/issues/274
echo "ninja==1.11.1.1"
) > build_constraints.txt
- name: Upload env_file
uses: actions/upload-artifact@v4.5.0
with:
@@ -86,6 +98,13 @@ jobs:
include-hidden-files: true
overwrite: true
- name: Upload build_constraints
uses: actions/upload-artifact@v4.5.0
with:
name: build_constraints
path: ./build_constraints.txt
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.5.0
with:
@@ -123,6 +142,11 @@ jobs:
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.1.8
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
with:
@@ -142,7 +166,7 @@ jobs:
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
@@ -167,6 +191,11 @@ jobs:
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.1.8
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.1.8
with:
@@ -205,7 +234,7 @@ jobs:
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
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;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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-dev"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
@@ -219,7 +248,7 @@ jobs:
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
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;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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-dev"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
@@ -233,7 +262,7 @@ jobs:
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
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;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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-dev"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-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
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.3
rev: v0.8.6
hooks:
- id: ruff
args:

View File

@@ -291,6 +291,7 @@ homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.letpot.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
@@ -311,6 +312,7 @@ homeassistant.components.manual.*
homeassistant.components.mastodon.*
homeassistant.components.matrix.*
homeassistant.components.matter.*
homeassistant.components.mcp_server.*
homeassistant.components.mealie.*
homeassistant.components.media_extractor.*
homeassistant.components.media_player.*
@@ -362,7 +364,9 @@ homeassistant.components.openuv.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
homeassistant.components.pandora.*
homeassistant.components.panel_custom.*
homeassistant.components.peblar.*
homeassistant.components.peco.*
@@ -380,6 +384,7 @@ homeassistant.components.pure_energie.*
homeassistant.components.purpleair.*
homeassistant.components.pushbullet.*
homeassistant.components.pvoutput.*
homeassistant.components.python_script.*
homeassistant.components.qnap_qsw.*
homeassistant.components.rabbitair.*
homeassistant.components.radarr.*

View File

@@ -637,6 +637,8 @@ build.json @home-assistant/supervisor
/tests/components/homeassistant_sky_connect/ @home-assistant/core
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
/tests/components/homeassistant_yellow/ @home-assistant/core
/homeassistant/components/homee/ @Taraman17
/tests/components/homee/ @Taraman17
/homeassistant/components/homekit/ @bdraco
/tests/components/homekit/ @bdraco
/homeassistant/components/homekit_controller/ @Jc2k @bdraco
@@ -686,6 +688,8 @@ build.json @home-assistant/supervisor
/tests/components/icloud/ @Quentame @nzapponi
/homeassistant/components/idasen_desk/ @abmantis
/tests/components/idasen_desk/ @abmantis
/homeassistant/components/igloohome/ @keithle888
/tests/components/igloohome/ @keithle888
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/image/ @home-assistant/core
@@ -827,6 +831,8 @@ build.json @home-assistant/supervisor
/tests/components/led_ble/ @bdraco
/homeassistant/components/lektrico/ @lektrico
/tests/components/lektrico/ @lektrico
/homeassistant/components/letpot/ @jpelgrom
/tests/components/letpot/ @jpelgrom
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
@@ -887,6 +893,8 @@ build.json @home-assistant/supervisor
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mcp_server/ @allenporter
/tests/components/mcp_server/ @allenporter
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
/tests/components/mealie/ @joostlek @andrew-codechimp
/homeassistant/components/meater/ @Sotolotl @emontnemery
@@ -1103,8 +1111,10 @@ build.json @home-assistant/supervisor
/tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
/homeassistant/components/overkiz/ @imicknl
/tests/components/overkiz/ @imicknl
/homeassistant/components/overseerr/ @joostlek
/tests/components/overseerr/ @joostlek
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
@@ -1135,8 +1145,8 @@ build.json @home-assistant/supervisor
/tests/components/plaato/ @JohNan
/homeassistant/components/plex/ @jjlawren
/tests/components/plex/ @jjlawren
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
/tests/components/plugwise/ @CoMPaTech @bouwew
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
/homeassistant/components/point/ @fredrike
@@ -1478,8 +1488,8 @@ build.json @home-assistant/supervisor
/tests/components/system_bridge/ @timmo001
/homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/tado/ @chiefdragon @erwindouna
/tests/components/tado/ @chiefdragon @erwindouna
/homeassistant/components/tado/ @erwindouna
/tests/components/tado/ @erwindouna
/homeassistant/components/tag/ @balloob @dmulcahey
/tests/components/tag/ @balloob @dmulcahey
/homeassistant/components/tailscale/ @frenck
@@ -1573,8 +1583,8 @@ build.json @home-assistant/supervisor
/tests/components/triggercmd/ @rvmey
/homeassistant/components/tts/ @home-assistant/core
/tests/components/tts/ @home-assistant/core
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
/tests/components/tuya/ @Tuya @zlinoliver @frenck
/homeassistant/components/tuya/ @Tuya @zlinoliver
/tests/components/tuya/ @Tuya @zlinoliver
/homeassistant/components/twentemilieu/ @frenck
/tests/components/twentemilieu/ @frenck
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen

View File

@@ -89,7 +89,7 @@ from .helpers import (
)
from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info, is_official_image
from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType
from .setup import (
# _setup_started is marked as protected to make it clear
@@ -106,6 +106,7 @@ from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
from .util.system_info import is_official_image
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop

View File

@@ -2,6 +2,7 @@
"domain": "microsoft",
"name": "Microsoft",
"integrations": [
"azure_data_explorer",
"azure_devops",
"azure_event_hub",
"azure_service_bus",

View File

@@ -34,17 +34,17 @@
"services": {
"capture_image": {
"name": "Capture image",
"description": "Request a new image capture from a camera device.",
"description": "Requests a new image capture from a camera device.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the camera to request an image."
"description": "Entity ID of the camera to request an image from."
}
}
},
"change_setting": {
"name": "Change setting",
"description": "Change an Abode system setting.",
"description": "Changes an Abode system setting.",
"fields": {
"setting": {
"name": "Setting",
@@ -58,11 +58,11 @@
},
"trigger_automation": {
"name": "Trigger automation",
"description": "Trigger an Abode automation.",
"description": "Triggers an Abode automation.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the automation to trigger."
"description": "Entity ID of the automation to trigger."
}
}
}

View File

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

View File

@@ -39,45 +39,54 @@ SENSORS: dict[str, SensorEntityDescription] = {
key="temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"humidity": SensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
"pressure": SensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"battery": SensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
"co2": SensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
"voc": SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
),
"light": SensorEntityDescription(
key="light",
native_unit_of_measurement=PERCENTAGE,
translation_key="light",
state_class=SensorStateClass.MEASUREMENT,
),
"virusRisk": SensorEntityDescription(
key="virusRisk",
translation_key="virus_risk",
state_class=SensorStateClass.MEASUREMENT,
),
"mold": SensorEntityDescription(
key="mold",
translation_key="mold",
state_class=SensorStateClass.MEASUREMENT,
),
"rssi": SensorEntityDescription(
key="rssi",
@@ -85,16 +94,19 @@ SENSORS: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
"pm1": SensorEntityDescription(
key="pm1",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
),
"pm25": SensorEntityDescription(
key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
}

View File

@@ -67,18 +67,21 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"humidity": SensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"pressure": SensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"battery": SensorEntityDescription(
key="battery",
@@ -86,24 +89,28 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
),
"co2": SensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"voc": SensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"illuminance": SensorEntityDescription(
key="illuminance",
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.8.0"],
"requirements": ["python-homeassistant-analytics==0.8.1"],
"single_config_entry": true
}

View File

@@ -21,7 +21,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_unique_id": "Impossible to determine a valid unique id for the device"
"invalid_unique_id": "Impossible to determine a valid unique ID for the device"
}
},
"options": {
@@ -38,17 +38,17 @@
}
},
"apps": {
"title": "Configure Android Apps",
"description": "Configure application id {app_id}",
"title": "Configure Android apps",
"description": "Configure application ID {app_id}",
"data": {
"app_name": "Application Name",
"app_name": "Application name",
"app_id": "Application ID",
"app_delete": "Check to delete this application"
}
},
"rules": {
"title": "Configure Android state detection rules",
"description": "Configure detection rule for application id {rule_id}",
"description": "Configure detection rule for application ID {rule_id}",
"data": {
"rule_id": "[%key:component::androidtv::options::step::apps::data::app_id%]",
"rule_values": "List of state detection rules (see documentation)",

View File

@@ -156,7 +156,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
# and one of them, which could end up being in discovery_info.host, is from a
# different device. If any of the discovery_info.ip_addresses matches the
# existing host, don't update the host.
if existing_config_entry and len(discovery_info.ip_addresses) > 1:
if (
existing_config_entry
# Ignored entries don't have host
and CONF_HOST in existing_config_entry.data
and len(discovery_info.ip_addresses) > 1
):
existing_host = existing_config_entry.data[CONF_HOST]
if existing_host != self.host:
if existing_host in [

View File

@@ -44,12 +44,12 @@
}
},
"apps": {
"title": "Configure Android Apps",
"description": "Configure application id {app_id}",
"title": "Configure Android apps",
"description": "Configure application ID {app_id}",
"data": {
"app_name": "Application Name",
"app_name": "Application name",
"app_id": "Application ID",
"app_icon": "Application Icon",
"app_icon": "Application icon",
"app_delete": "Check to delete this application"
}
}

View File

@@ -98,7 +98,6 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
scan_filter: str | None = None
all_identifiers: set[str]
atv: BaseConfig | None = None
atv_identifiers: list[str] | None = None
_host: str # host in zeroconf discovery info, should not be accessed by other flows
@@ -118,6 +117,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize a new AppleTVConfigFlow."""
self.credentials: dict[int, str | None] = {} # Protocol -> credentials
self.all_identifiers: set[str] = set()
@property
def device_identifier(self) -> str | None:

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["apprise"],
"quality_scale": "legacy",
"requirements": ["apprise==1.9.0"]
"requirements": ["apprise==1.9.1"]
}

View File

@@ -120,6 +120,8 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Wait for the client to be ready."""
if not self.data or Attribute.MAC_ADDRESS not in self.data:
await self.client.read_mac_address()
data = await self.client.wait_for_response(
FunctionalDomain.IDENTIFICATION, 2, WAIT_TIMEOUT
)
@@ -130,12 +132,9 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
return False
if not self.data or Attribute.NAME not in self.data:
await self.client.wait_for_response(
FunctionalDomain.IDENTIFICATION, 4, WAIT_TIMEOUT
)
if not self.data or Attribute.THERMOSTAT_MODES not in self.data:
await self.client.read_thermostat_iaq_available()
await self.client.wait_for_response(
FunctionalDomain.CONTROL, 7, WAIT_TIMEOUT
)
@@ -144,10 +143,16 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
not self.data
or Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS not in self.data
):
await self.client.read_sensors()
await self.client.wait_for_response(
FunctionalDomain.SENSORS, 2, WAIT_TIMEOUT
)
await self.client.read_thermostat_status()
await self.client.read_iaq_status()
await ready_callback(True)
return True

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.7.4"]
"requirements": ["pyaprilaire==0.7.7"]
}

View File

@@ -29,6 +29,8 @@ class ApSystemsSensorData:
class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
"""Coordinator used for all sensors."""
device_version: str
def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None:
"""Initialize my coordinator."""
super().__init__(
@@ -46,6 +48,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
raise UpdateFailed from None
self.api.max_power = device_info.maxPower
self.api.min_power = device_info.minPower
self.device_version = device_info.devVer
async def _async_update_data(self) -> ApSystemsSensorData:
try:

View File

@@ -21,7 +21,8 @@ class ApSystemsEntity(Entity):
"""Initialize the APsystems entity."""
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, data.device_id)},
serial_number=data.device_id,
manufacturer="APsystems",
model="EZ1-M",
serial_number=data.device_id,
sw_version=data.coordinator.device_version.split(" ")[1],
)

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from aiohttp import ClientConnectorError
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
from homeassistant.const import UnitOfPower
from homeassistant.core import HomeAssistant
@@ -45,7 +47,13 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
async def async_update(self) -> None:
"""Set the state with the value fetched from the inverter."""
self._attr_native_value = await self._api.get_max_power()
try:
status = await self._api.get_max_power()
except (TimeoutError, ClientConnectorError):
self._attr_available = False
else:
self._attr_available = True
self._attr_native_value = status
async def async_set_native_value(self, value: float) -> None:
"""Set the desired output power."""

View File

@@ -19,5 +19,5 @@
"documentation": "https://www.home-assistant.io/integrations/aranet",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["aranet4==2.4.0"]
"requirements": ["aranet4==2.5.0"]
}

View File

@@ -22,6 +22,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_SW_VERSION,
CONCENTRATION_PARTS_PER_MILLION,
@@ -142,6 +143,7 @@ def _sensor_device_info_to_hass(
if adv.readings and adv.readings.name:
hass_device_info[ATTR_NAME] = adv.readings.name
hass_device_info[ATTR_MANUFACTURER] = ARANET_MANUFACTURER_NAME
hass_device_info[ATTR_MODEL] = adv.readings.type.model
if adv.manufacturer_data:
hass_device_info[ATTR_SW_VERSION] = str(adv.manufacturer_data.version)
return hass_device_info

View File

@@ -90,7 +90,7 @@ class ArubaDeviceScanner(DeviceScanner):
"""Retrieve data from Aruba Access Point and return parsed result."""
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
ssh = pexpect.spawn(connect)
ssh: pexpect.spawn[str] = pexpect.spawn(connect, encoding="utf-8")
query = ssh.expect(
[
"password:",
@@ -125,12 +125,12 @@ class ArubaDeviceScanner(DeviceScanner):
ssh.expect("#")
ssh.sendline("show clients")
ssh.expect("#")
devices_result = ssh.before.split(b"\r\n")
devices_result = (ssh.before or "").splitlines()
ssh.sendline("exit")
devices: dict[str, dict[str, str]] = {}
for device in devices_result:
if match := _DEVICES_REGEX.search(device.decode("utf-8")):
if match := _DEVICES_REGEX.search(device):
devices[match.group("ip")] = {
"ip": match.group("ip"),
"mac": match.group("mac").upper(),

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pexpect", "ptyprocess"],
"quality_scale": "legacy",
"requirements": ["pexpect==4.6.0"]
"requirements": ["pexpect==4.9.0"]
}

View File

@@ -108,6 +108,7 @@ async def async_pipeline_from_audio_stream(
device_id: str | None = None,
start_stage: PipelineStage = PipelineStage.STT,
end_stage: PipelineStage = PipelineStage.TTS,
conversation_extra_system_prompt: str | None = None,
) -> None:
"""Create an audio pipeline from an audio stream.
@@ -119,6 +120,7 @@ async def async_pipeline_from_audio_stream(
stt_metadata=stt_metadata,
stt_stream=stt_stream,
wake_word_phrase=wake_word_phrase,
conversation_extra_system_prompt=conversation_extra_system_prompt,
run=PipelineRun(
hass,
context=context,

View File

@@ -1010,7 +1010,11 @@ class PipelineRun:
self.intent_agent = agent_info.id
async def recognize_intent(
self, intent_input: str, conversation_id: str | None, device_id: str | None
self,
intent_input: str,
conversation_id: str | None,
device_id: str | None,
conversation_extra_system_prompt: str | None,
) -> str:
"""Run intent recognition portion of pipeline. Returns text to speak."""
if self.intent_agent is None:
@@ -1045,6 +1049,7 @@ class PipelineRun:
device_id=device_id,
language=input_language,
agent_id=self.intent_agent,
extra_system_prompt=conversation_extra_system_prompt,
)
processed_locally = self.intent_agent == conversation.HOME_ASSISTANT_AGENT
@@ -1392,8 +1397,13 @@ class PipelineInput:
"""Input for text-to-speech. Required when start_stage = tts."""
conversation_id: str | None = None
"""Identifier for the conversation."""
conversation_extra_system_prompt: str | None = None
"""Extra prompt information for the conversation agent."""
device_id: str | None = None
"""Identifier of the device that is processing the input/output of the pipeline."""
async def execute(self) -> None:
"""Run pipeline."""
@@ -1483,6 +1493,7 @@ class PipelineInput:
intent_input,
self.conversation_id,
self.device_id,
self.conversation_extra_system_prompt,
)
if tts_input.strip():
current_stage = PipelineStage.TTS

View File

@@ -75,7 +75,7 @@ class AudioBuffer:
class VoiceCommandSegmenter:
"""Segments an audio stream into voice commands."""
speech_seconds: float = 0.1
speech_seconds: float = 0.3
"""Seconds of speech before voice command has started."""
command_seconds: float = 1.0

View File

@@ -31,8 +31,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"invalid_unique_id": "Impossible to determine a valid unique id for the device",
"no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible"
"invalid_unique_id": "Impossible to determine a valid unique ID for the device",
"no_unique_id": "A device without a valid unique ID is already configured. Configuration of multiple instances is not possible"
}
},
"options": {
@@ -42,7 +42,7 @@
"consider_home": "Seconds to wait before considering a device away",
"track_unknown": "Track unknown / unnamed devices",
"interface": "The interface that you want statistics from (e.g. eth0, eth1 etc)",
"dnsmasq": "The location in the router of the dnsmasq.leases files",
"dnsmasq": "The location of the dnsmasq.leases file in the router",
"require_ip": "Devices must have IP (for access point mode)"
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
"iot_class": "cloud_polling",
"loggers": ["aussiebb"],
"requirements": ["pyaussiebb==0.1.4"]
"requirements": ["pyaussiebb==0.1.5"]
}

View File

@@ -5,6 +5,10 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
# Pre-import backup to avoid it being imported
# later when the import executor is busy and delaying
# startup
from . import backup # noqa: F401
from .agent import (
BackupAgent,
BackupAgentError,
@@ -17,8 +21,10 @@ from .manager import (
BackupManager,
BackupPlatformProtocol,
BackupReaderWriter,
BackupReaderWriterError,
CoreBackupReaderWriter,
CreateBackupEvent,
IncorrectPasswordError,
ManagerBackup,
NewBackup,
WrittenBackup,
@@ -35,8 +41,10 @@ __all__ = [
"BackupAgentPlatformProtocol",
"BackupPlatformProtocol",
"BackupReaderWriter",
"BackupReaderWriterError",
"CreateBackupEvent",
"Folder",
"IncorrectPasswordError",
"LocalBackupAgent",
"NewBackup",
"WrittenBackup",

View File

@@ -7,6 +7,7 @@ from collections.abc import Callable
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta
from enum import StrEnum
import random
from typing import TYPE_CHECKING, Self, TypedDict
from cronsim import CronSim
@@ -17,7 +18,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util
from .const import LOGGER
from .models import Folder
from .models import BackupManagerError, Folder
if TYPE_CHECKING:
from .manager import BackupManager, ManagerBackup
@@ -28,6 +29,10 @@ if TYPE_CHECKING:
CRON_PATTERN_DAILY = "45 4 * * *"
CRON_PATTERN_WEEKLY = "45 4 * * {}"
# Randomize the start time of the backup by up to 60 minutes to avoid
# all backups running at the same time.
BACKUP_START_TIME_JITTER = 60 * 60
class StoredBackupConfig(TypedDict):
"""Represent the stored backup config."""
@@ -124,6 +129,7 @@ class BackupConfig:
def load(self, stored_config: StoredBackupConfig) -> None:
"""Load config."""
self.data = BackupConfigData.from_dict(stored_config)
self.data.retention.apply(self._manager)
self.data.schedule.apply(self._manager)
async def update(
@@ -160,8 +166,13 @@ class RetentionConfig:
def apply(self, manager: BackupManager) -> None:
"""Apply backup retention configuration."""
if self.days is not None:
LOGGER.debug(
"Scheduling next automatic delete of backups older than %s in 1 day",
self.days,
)
self._schedule_next(manager)
else:
LOGGER.debug("Unscheduling next automatic delete")
self._unschedule_next(manager)
def to_dict(self) -> StoredRetentionConfig:
@@ -318,11 +329,13 @@ class BackupSchedule:
password=config_data.create_backup.password,
with_automatic_settings=True,
)
except BackupManagerError as err:
LOGGER.error("Error creating backup: %s", err)
except Exception: # noqa: BLE001
# another more specific exception will be added
# and handled in the future
LOGGER.exception("Unexpected error creating automatic backup")
next_time += timedelta(seconds=random.randint(0, BACKUP_START_TIME_JITTER))
LOGGER.debug("Scheduling next automatic backup at %s", next_time)
manager.remove_next_backup_event = async_track_point_in_time(
manager.hass, _create_backup, next_time
)

View File

@@ -46,15 +46,11 @@ from .const import (
EXCLUDE_FROM_BACKUP,
LOGGER,
)
from .models import AgentBackup, Folder
from .models import AgentBackup, BackupManagerError, Folder
from .store import BackupStore
from .util import make_backup_dir, read_backup, validate_password
class IncorrectPasswordError(HomeAssistantError):
"""Raised when the password is incorrect."""
@dataclass(frozen=True, kw_only=True, slots=True)
class NewBackup:
"""New backup class."""
@@ -245,6 +241,14 @@ class BackupReaderWriter(abc.ABC):
"""Restore a backup."""
class BackupReaderWriterError(HomeAssistantError):
"""Backup reader/writer error."""
class IncorrectPasswordError(BackupReaderWriterError):
"""Raised when the password is incorrect."""
class BackupManager:
"""Define the format that backup managers can have."""
@@ -373,7 +377,9 @@ class BackupManager:
)
for result in pre_backup_results:
if isinstance(result, Exception):
raise result
raise BackupManagerError(
f"Error during pre-backup: {result}"
) from result
async def async_post_backup_actions(self) -> None:
"""Perform post backup actions."""
@@ -386,7 +392,9 @@ class BackupManager:
)
for result in post_backup_results:
if isinstance(result, Exception):
raise result
raise BackupManagerError(
f"Error during post-backup: {result}"
) from result
async def load_platforms(self) -> None:
"""Load backup platforms."""
@@ -422,11 +430,22 @@ class BackupManager:
return_exceptions=True,
)
for idx, result in enumerate(sync_backup_results):
if isinstance(result, Exception):
if isinstance(result, BackupReaderWriterError):
# writer errors will affect all agents
# no point in continuing
raise BackupManagerError(str(result)) from result
if isinstance(result, BackupAgentError):
LOGGER.error("Error uploading to %s: %s", agent_ids[idx], result)
agent_errors[agent_ids[idx]] = result
LOGGER.exception(
"Error during backup upload - %s", result, exc_info=result
)
continue
if isinstance(result, Exception):
# trap bugs from agents
agent_errors[agent_ids[idx]] = result
LOGGER.error("Unexpected error: %s", result, exc_info=result)
continue
if isinstance(result, BaseException):
raise result
return agent_errors
async def async_get_backups(
@@ -449,7 +468,7 @@ class BackupManager:
agent_errors[agent_ids[idx]] = result
continue
if isinstance(result, BaseException):
raise result
raise result # unexpected error
for agent_backup in result:
if (backup_id := agent_backup.backup_id) not in backups:
if known_backup := self.known_backups.get(backup_id):
@@ -499,7 +518,7 @@ class BackupManager:
agent_errors[agent_ids[idx]] = result
continue
if isinstance(result, BaseException):
raise result
raise result # unexpected error
if not result:
continue
if backup is None:
@@ -563,7 +582,7 @@ class BackupManager:
agent_errors[agent_ids[idx]] = result
continue
if isinstance(result, BaseException):
raise result
raise result # unexpected error
if not agent_errors:
self.known_backups.remove(backup_id)
@@ -578,7 +597,7 @@ class BackupManager:
) -> None:
"""Receive and store a backup file from upload."""
if self.state is not BackupManagerState.IDLE:
raise HomeAssistantError(f"Backup manager busy: {self.state}")
raise BackupManagerError(f"Backup manager busy: {self.state}")
self.async_on_backup_event(
ReceiveBackupEvent(stage=None, state=ReceiveBackupState.IN_PROGRESS)
)
@@ -652,6 +671,7 @@ class BackupManager:
include_homeassistant=include_homeassistant,
name=name,
password=password,
raise_task_error=True,
with_automatic_settings=with_automatic_settings,
)
assert self._backup_finish_task
@@ -669,11 +689,12 @@ class BackupManager:
include_homeassistant: bool,
name: str | None,
password: str | None,
raise_task_error: bool = False,
with_automatic_settings: bool = False,
) -> NewBackup:
"""Initiate generating a backup."""
if self.state is not BackupManagerState.IDLE:
raise HomeAssistantError(f"Backup manager busy: {self.state}")
raise BackupManagerError(f"Backup manager busy: {self.state}")
if with_automatic_settings:
self.config.data.last_attempted_automatic_backup = dt_util.now()
@@ -692,6 +713,7 @@ class BackupManager:
include_homeassistant=include_homeassistant,
name=name,
password=password,
raise_task_error=raise_task_error,
with_automatic_settings=with_automatic_settings,
)
except Exception:
@@ -714,57 +736,81 @@ class BackupManager:
include_homeassistant: bool,
name: str | None,
password: str | None,
raise_task_error: bool,
with_automatic_settings: bool,
) -> NewBackup:
"""Initiate generating a backup."""
if not agent_ids:
raise HomeAssistantError("At least one agent must be selected")
if any(agent_id not in self.backup_agents for agent_id in agent_ids):
raise HomeAssistantError("Invalid agent selected")
raise BackupManagerError("At least one agent must be selected")
if invalid_agents := [
agent_id for agent_id in agent_ids if agent_id not in self.backup_agents
]:
raise BackupManagerError(f"Invalid agents selected: {invalid_agents}")
if include_all_addons and include_addons:
raise HomeAssistantError(
raise BackupManagerError(
"Cannot include all addons and specify specific addons"
)
backup_name = (
name
or f"{"Automatic" if with_automatic_settings else "Custom"} {HAVERSION}"
or f"{"Automatic" if with_automatic_settings else "Custom"} backup {HAVERSION}"
)
new_backup, self._backup_task = await self._reader_writer.async_create_backup(
agent_ids=agent_ids,
backup_name=backup_name,
extra_metadata={
"instance_id": await instance_id.async_get(self.hass),
"with_automatic_settings": with_automatic_settings,
},
include_addons=include_addons,
include_all_addons=include_all_addons,
include_database=include_database,
include_folders=include_folders,
include_homeassistant=include_homeassistant,
on_progress=self.async_on_backup_event,
password=password,
)
self._backup_finish_task = self.hass.async_create_task(
try:
(
new_backup,
self._backup_task,
) = await self._reader_writer.async_create_backup(
agent_ids=agent_ids,
backup_name=backup_name,
extra_metadata={
"instance_id": await instance_id.async_get(self.hass),
"with_automatic_settings": with_automatic_settings,
},
include_addons=include_addons,
include_all_addons=include_all_addons,
include_database=include_database,
include_folders=include_folders,
include_homeassistant=include_homeassistant,
on_progress=self.async_on_backup_event,
password=password,
)
except BackupReaderWriterError as err:
raise BackupManagerError(str(err)) from err
backup_finish_task = self._backup_finish_task = self.hass.async_create_task(
self._async_finish_backup(agent_ids, with_automatic_settings),
name="backup_manager_finish_backup",
)
if not raise_task_error:
def log_finish_task_error(task: asyncio.Task[None]) -> None:
if task.done() and not task.cancelled() and (err := task.exception()):
if isinstance(err, BackupManagerError):
LOGGER.error("Error creating backup: %s", err)
else:
LOGGER.error("Unexpected error: %s", err, exc_info=err)
backup_finish_task.add_done_callback(log_finish_task_error)
return new_backup
async def _async_finish_backup(
self, agent_ids: list[str], with_automatic_settings: bool
) -> None:
"""Finish a backup."""
if TYPE_CHECKING:
assert self._backup_task is not None
backup_success = False
try:
written_backup = await self._backup_task
except Exception as err: # noqa: BLE001
LOGGER.debug("Generating backup failed", exc_info=err)
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
except Exception as err:
if with_automatic_settings:
self._update_issue_backup_failed()
if isinstance(err, BackupReaderWriterError):
raise BackupManagerError(str(err)) from err
raise # unexpected error
else:
LOGGER.debug(
"Generated new backup with backup_id %s, uploading to agents %s",
@@ -777,28 +823,40 @@ class BackupManager:
state=CreateBackupState.IN_PROGRESS,
)
)
agent_errors = await self._async_upload_backup(
backup=written_backup.backup,
agent_ids=agent_ids,
open_stream=written_backup.open_stream,
)
await written_backup.release_stream()
if with_automatic_settings:
# create backup was successful, update last_completed_automatic_backup
self.config.data.last_completed_automatic_backup = dt_util.now()
self.store.save()
self._update_issue_after_agent_upload(agent_errors)
self.known_backups.add(written_backup.backup, agent_errors)
try:
agent_errors = await self._async_upload_backup(
backup=written_backup.backup,
agent_ids=agent_ids,
open_stream=written_backup.open_stream,
)
finally:
await written_backup.release_stream()
self.known_backups.add(written_backup.backup, agent_errors)
if not agent_errors:
if with_automatic_settings:
# create backup was successful, update last_completed_automatic_backup
self.config.data.last_completed_automatic_backup = dt_util.now()
self.store.save()
backup_success = True
if with_automatic_settings:
self._update_issue_after_agent_upload(agent_errors)
# delete old backups more numerous than copies
# try this regardless of agent errors above
await delete_backups_exceeding_configured_count(self)
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED)
)
finally:
self._backup_task = None
self._backup_finish_task = None
self.async_on_backup_event(
CreateBackupEvent(
stage=None,
state=CreateBackupState.COMPLETED
if backup_success
else CreateBackupState.FAILED,
)
)
self.async_on_backup_event(IdleEvent())
async def async_restore_backup(
@@ -814,7 +872,7 @@ class BackupManager:
) -> None:
"""Initiate restoring a backup."""
if self.state is not BackupManagerState.IDLE:
raise HomeAssistantError(f"Backup manager busy: {self.state}")
raise BackupManagerError(f"Backup manager busy: {self.state}")
self.async_on_backup_event(
RestoreBackupEvent(stage=None, state=RestoreBackupState.IN_PROGRESS)
@@ -829,6 +887,9 @@ class BackupManager:
restore_folders=restore_folders,
restore_homeassistant=restore_homeassistant,
)
self.async_on_backup_event(
RestoreBackupEvent(stage=None, state=RestoreBackupState.COMPLETED)
)
except Exception:
self.async_on_backup_event(
RestoreBackupEvent(stage=None, state=RestoreBackupState.FAILED)
@@ -851,7 +912,7 @@ class BackupManager:
"""Initiate restoring a backup."""
agent = self.backup_agents[agent_id]
if not await agent.async_get_backup(backup_id):
raise HomeAssistantError(
raise BackupManagerError(
f"Backup {backup_id} not found in agent {agent_id}"
)
@@ -1024,11 +1085,11 @@ class CoreBackupReaderWriter(BackupReaderWriter):
backup_id = _generate_backup_id(date_str, backup_name)
if include_addons or include_all_addons or include_folders:
raise HomeAssistantError(
raise BackupReaderWriterError(
"Addons and folders are not supported by core backup"
)
if not include_homeassistant:
raise HomeAssistantError("Home Assistant must be included in backup")
raise BackupReaderWriterError("Home Assistant must be included in backup")
backup_task = self._hass.async_create_task(
self._async_create_backup(
@@ -1099,6 +1160,13 @@ class CoreBackupReaderWriter(BackupReaderWriter):
password,
local_agent_tar_file_path,
)
except (BackupManagerError, OSError, tarfile.TarError, ValueError) as err:
# BackupManagerError from async_pre_backup_actions
# OSError from file operations
# TarError from tarfile
# ValueError from json_bytes
raise BackupReaderWriterError(str(err)) from err
else:
backup = AgentBackup(
addons=[],
backup_id=backup_id,
@@ -1116,12 +1184,15 @@ class CoreBackupReaderWriter(BackupReaderWriter):
async_add_executor_job = self._hass.async_add_executor_job
async def send_backup() -> AsyncIterator[bytes]:
f = await async_add_executor_job(tar_file_path.open, "rb")
try:
while chunk := await async_add_executor_job(f.read, 2**20):
yield chunk
finally:
await async_add_executor_job(f.close)
f = await async_add_executor_job(tar_file_path.open, "rb")
try:
while chunk := await async_add_executor_job(f.read, 2**20):
yield chunk
finally:
await async_add_executor_job(f.close)
except OSError as err:
raise BackupReaderWriterError(str(err)) from err
async def open_backup() -> AsyncIterator[bytes]:
return send_backup()
@@ -1129,14 +1200,20 @@ class CoreBackupReaderWriter(BackupReaderWriter):
async def remove_backup() -> None:
if local_agent_tar_file_path:
return
await async_add_executor_job(tar_file_path.unlink, True)
try:
await async_add_executor_job(tar_file_path.unlink, True)
except OSError as err:
raise BackupReaderWriterError(str(err)) from err
return WrittenBackup(
backup=backup, open_stream=open_backup, release_stream=remove_backup
)
finally:
# Inform integrations the backup is done
await manager.async_post_backup_actions()
try:
await manager.async_post_backup_actions()
except BackupManagerError as err:
raise BackupReaderWriterError(str(err)) from err
def _mkdir_and_generate_backup_contents(
self,
@@ -1206,6 +1283,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
if self._local_agent_id in agent_ids:
local_agent = manager.local_backup_agents[self._local_agent_id]
tar_file_path = local_agent.get_backup_path(backup.backup_id)
await async_add_executor_job(make_backup_dir, tar_file_path.parent)
await async_add_executor_job(shutil.move, temp_file, tar_file_path)
else:
tar_file_path = temp_file
@@ -1249,11 +1327,11 @@ class CoreBackupReaderWriter(BackupReaderWriter):
"""
if restore_addons or restore_folders:
raise HomeAssistantError(
raise BackupReaderWriterError(
"Addons and folders are not supported in core restore"
)
if not restore_homeassistant and not restore_database:
raise HomeAssistantError(
raise BackupReaderWriterError(
"Home Assistant or database must be included in restore"
)
@@ -1298,7 +1376,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
)
await self._hass.async_add_executor_job(_write_restore_file)
await self._hass.services.async_call("homeassistant", "restart", {})
await self._hass.services.async_call("homeassistant", "restart", blocking=True)
def _generate_backup_id(date: str, name: str) -> str:

View File

@@ -6,6 +6,8 @@ from dataclasses import asdict, dataclass
from enum import StrEnum
from typing import Any, Self
from homeassistant.exceptions import HomeAssistantError
@dataclass(frozen=True, kw_only=True)
class AddonInfo:
@@ -67,3 +69,7 @@ class AgentBackup:
protected=data["protected"],
size=data["size"],
)
class BackupManagerError(HomeAssistantError):
"""Backup manager error."""

View File

@@ -5,8 +5,8 @@
"description": "The automatic backup could not be created. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
},
"automatic_backup_failed_upload_agents": {
"title": "Automatic backup could not be uploaded to agents",
"description": "The automatic backup could not be uploaded to agents {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
"title": "Automatic backup could not be uploaded to the configured locations",
"description": "The automatic backup could not be uploaded to the configured locations {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
}
},
"services": {

View File

@@ -34,7 +34,7 @@ class BangOlufsenData:
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:

View File

@@ -79,6 +79,7 @@ class WebsocketNotification(StrEnum):
"""Enum for WebSocket notification types."""
ACTIVE_LISTENING_MODE = "active_listening_mode"
BUTTON = "button"
PLAYBACK_ERROR = "playback_error"
PLAYBACK_METADATA = "playback_metadata"
PLAYBACK_PROGRESS = "playback_progress"
@@ -203,14 +204,60 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
),
]
)
# Map for storing compatibility of devices.
MODEL_SUPPORT_DEVICE_BUTTONS: Final[str] = "device_buttons"
MODEL_SUPPORT_MAP = {
MODEL_SUPPORT_DEVICE_BUTTONS: (
BangOlufsenModel.BEOLAB_8,
BangOlufsenModel.BEOLAB_28,
BangOlufsenModel.BEOSOUND_2,
BangOlufsenModel.BEOSOUND_A5,
BangOlufsenModel.BEOSOUND_A9,
BangOlufsenModel.BEOSOUND_BALANCE,
BangOlufsenModel.BEOSOUND_EMERGE,
BangOlufsenModel.BEOSOUND_LEVEL,
BangOlufsenModel.BEOSOUND_THEATRE,
)
}
# Device events
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = {
"shortPress (Release)": "short_press_release",
"longPress (Timeout)": "long_press_timeout",
"longPress (Release)": "long_press_release",
"veryLongPress (Timeout)": "very_long_press_timeout",
"veryLongPress (Release)": "very_long_press_release",
}
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [
"Bluetooth",
"Microphone",
"Next",
"PlayPause",
"Preset1",
"Preset2",
"Preset3",
"Preset4",
"Previous",
"Volume",
]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
"short_press_release",
"long_press_timeout",
"long_press_release",
"very_long_press_timeout",
"very_long_press_release",
]
# Beolink Converter NL/ML sources need to be transformed to upper case
BEOLINK_JOIN_SOURCES_TO_UPPER = (
"aux_a",

View File

@@ -0,0 +1,76 @@
"""Event entities for the Bang & Olufsen integration."""
from __future__ import annotations
from homeassistant.components.event import EventDeviceClass, EventEntity
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BangOlufsenConfigEntry
from .const import (
CONNECTION_STATUS,
DEVICE_BUTTON_EVENTS,
DEVICE_BUTTONS,
MODEL_SUPPORT_DEVICE_BUTTONS,
MODEL_SUPPORT_MAP,
WebsocketNotification,
)
from .entity import BangOlufsenEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BangOlufsenConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Sensor entities from config entry."""
if config_entry.data[CONF_MODEL] in MODEL_SUPPORT_MAP[MODEL_SUPPORT_DEVICE_BUTTONS]:
async_add_entities(
BangOlufsenButtonEvent(config_entry, button_type)
for button_type in DEVICE_BUTTONS
)
class BangOlufsenButtonEvent(BangOlufsenEntity, EventEntity):
"""Event class for Button events."""
_attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False
_attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
"""Initialize Button."""
super().__init__(config_entry, config_entry.runtime_data.client)
self._attr_unique_id = f"{self._unique_id}_{button_type}"
# Make the native button name Home Assistant compatible
self._attr_translation_key = button_type.lower()
self._button_type = button_type
async def async_added_to_hass(self) -> None:
"""Listen to WebSocket button events."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.BUTTON}_{self._button_type}",
self._async_handle_event,
)
)
@callback
def _async_handle_event(self, event: str) -> None:
"""Handle event."""
self._trigger_event(event)
self.async_write_ha_state()

View File

@@ -1,7 +1,12 @@
{
"common": {
"jid_options_description": "Advanced grouping options, where devices' unique Beolink IDs (Called JIDs) are used directly. JIDs can be found in the state attributes of the media player entity.",
"jid_options_name": "JID options",
"jid_options_description": "Advanced grouping options, where devices' unique Beolink IDs (Called JIDs) are used directly. JIDs can be found in the state attributes of the media player entity."
"long_press_release": "Release of long press",
"long_press_timeout": "Long press",
"short_press_release": "Release of short press",
"very_long_press_release": "Release of very long press",
"very_long_press_timeout": "Very long press"
},
"config": {
"error": {
@@ -29,6 +34,150 @@
}
}
},
"entity": {
"event": {
"bluetooth": {
"name": "Bluetooth",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"microphone": {
"name": "Microphone",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"next": {
"name": "Next",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"playpause": {
"name": "Play / Pause",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"preset1": {
"name": "Favourite 1",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"preset2": {
"name": "Favourite 2",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"preset3": {
"name": "Favourite 3",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"preset4": {
"name": "Favourite 4",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"previous": {
"name": "Previous",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
},
"volume": {
"name": "Volume",
"state_attributes": {
"event_type": {
"state": {
"short_press_release": "[%key:component::bang_olufsen::common::short_press_release%]",
"long_press_timeout": "[%key:component::bang_olufsen::common::long_press_timeout%]",
"long_press_release": "[%key:component::bang_olufsen::common::long_press_release%]",
"very_long_press_timeout": "[%key:component::bang_olufsen::common::very_long_press_timeout%]",
"very_long_press_release": "[%key:component::bang_olufsen::common::very_long_press_release%]"
}
}
}
}
}
},
"selector": {
"source_ids": {
"options": {

View File

@@ -3,8 +3,10 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from mozart_api.models import (
ButtonEvent,
ListeningModeProps,
PlaybackContentMetadata,
PlaybackError,
@@ -26,6 +28,7 @@ from homeassistant.util.enum import try_parse_enum
from .const import (
BANG_OLUFSEN_WEBSOCKET_EVENT,
CONNECTION_STATUS,
EVENT_TRANSLATION_MAP,
WebsocketNotification,
)
from .entity import BangOlufsenBase
@@ -54,6 +57,8 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self._client.get_active_listening_mode_notifications(
self.on_active_listening_mode
)
self._client.get_button_notifications(self.on_button_notification)
self._client.get_playback_error_notifications(
self.on_playback_error_notification
)
@@ -104,6 +109,19 @@ class BangOlufsenWebsocket(BangOlufsenBase):
notification,
)
def on_button_notification(self, notification: ButtonEvent) -> None:
"""Send button dispatch."""
# State is expected to always be available.
if TYPE_CHECKING:
assert notification.state
# Send to event entity
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.BUTTON}_{notification.button}",
EVENT_TRANSLATION_MAP[notification.state],
)
def on_notification_notification(
self, notification: WebsocketNotificationTag
) -> None:

View File

@@ -14,10 +14,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .coordinator import BluesoundCoordinator
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.MEDIA_PLAYER]
PLATFORMS = [
Platform.MEDIA_PLAYER,
]
@dataclass
@@ -26,6 +29,7 @@ class BluesoundRuntimeData:
player: Player
sync_status: SyncStatus
coordinator: BluesoundCoordinator
type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
@@ -33,9 +37,6 @@ type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Bluesound."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = []
return True
@@ -46,13 +47,16 @@ async def async_setup_entry(
host = config_entry.data[CONF_HOST]
port = config_entry.data[CONF_PORT]
session = async_get_clientsession(hass)
async with Player(host, port, session=session, default_timeout=10) as player:
try:
sync_status = await player.sync_status(timeout=1)
except PlayerUnreachableError as ex:
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
player = Player(host, port, session=session, default_timeout=10)
try:
sync_status = await player.sync_status(timeout=1)
except PlayerUnreachableError as ex:
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status)
coordinator = BluesoundCoordinator(hass, player, sync_status)
await coordinator.async_config_entry_first_refresh()
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status, coordinator)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

View File

@@ -71,27 +71,6 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
),
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import bluesound config entry from configuration.yaml."""
session = async_get_clientsession(self.hass)
async with Player(
import_data[CONF_HOST], import_data[CONF_PORT], session=session
) as player:
try:
sync_status = await player.sync_status(timeout=1)
except PlayerUnreachableError:
return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(
format_unique_id(sync_status.mac, import_data[CONF_PORT])
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=sync_status.name,
data=import_data,
)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:

View File

@@ -0,0 +1,160 @@
"""Define a base coordinator for Bluesound entities."""
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass, replace
from datetime import timedelta
import logging
from pyblu import Input, Player, Preset, Status, SyncStatus
from pyblu.errors import PlayerUnreachableError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
NODE_OFFLINE_CHECK_TIMEOUT = timedelta(minutes=3)
PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15)
@dataclass
class BluesoundData:
"""Define a class to hold Bluesound data."""
sync_status: SyncStatus
status: Status
presets: list[Preset]
inputs: list[Input]
def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]:
"""Cancel a task."""
async def _cancel_task() -> None:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
return _cancel_task
class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]):
"""Define an object to hold Bluesound data."""
def __init__(
self, hass: HomeAssistant, player: Player, sync_status: SyncStatus
) -> None:
"""Initialize."""
self.player = player
self._inital_sync_status = sync_status
super().__init__(
hass,
logger=_LOGGER,
name=sync_status.name,
)
async def _async_setup(self) -> None:
assert self.config_entry is not None
preset = await self.player.presets()
inputs = await self.player.inputs()
status = await self.player.status()
self.async_set_updated_data(
BluesoundData(
sync_status=self._inital_sync_status,
status=status,
presets=preset,
inputs=inputs,
)
)
status_loop_task = self.hass.async_create_background_task(
self._poll_status_loop(),
name=f"bluesound.poll_status_loop_{self.data.sync_status.id}",
)
self.config_entry.async_on_unload(cancel_task(status_loop_task))
sync_status_loop_task = self.hass.async_create_background_task(
self._poll_sync_status_loop(),
name=f"bluesound.poll_sync_status_loop_{self.data.sync_status.id}",
)
self.config_entry.async_on_unload(cancel_task(sync_status_loop_task))
presets_and_inputs_loop_task = self.hass.async_create_background_task(
self._poll_presets_and_inputs_loop(),
name=f"bluesound.poll_presets_and_inputs_loop_{self.data.sync_status.id}",
)
self.config_entry.async_on_unload(cancel_task(presets_and_inputs_loop_task))
async def _async_update_data(self) -> BluesoundData:
return self.data
async def _poll_presets_and_inputs_loop(self) -> None:
while True:
await asyncio.sleep(PRESET_AND_INPUTS_INTERVAL.total_seconds())
try:
preset = await self.player.presets()
inputs = await self.player.inputs()
self.async_set_updated_data(
replace(
self.data,
presets=preset,
inputs=inputs,
)
)
except PlayerUnreachableError as ex:
self.async_set_update_error(ex)
except asyncio.CancelledError:
return
except Exception as ex: # noqa: BLE001 - this loop should never stop
self.async_set_update_error(ex)
async def _poll_status_loop(self) -> None:
"""Loop which polls the status of the player."""
while True:
try:
status = await self.player.status(
etag=self.data.status.etag, poll_timeout=120, timeout=125
)
self.async_set_updated_data(
replace(
self.data,
status=status,
)
)
except PlayerUnreachableError as ex:
self.async_set_update_error(ex)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
except asyncio.CancelledError:
return
except Exception as ex: # noqa: BLE001 - this loop should never stop
self.async_set_update_error(ex)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
async def _poll_sync_status_loop(self) -> None:
"""Loop which polls the sync status of the player."""
while True:
try:
sync_status = await self.player.sync_status(
etag=self.data.sync_status.etag, poll_timeout=120, timeout=125
)
self.async_set_updated_data(
replace(
self.data,
sync_status=sync_status,
)
)
except PlayerUnreachableError as ex:
self.async_set_update_error(ex)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())
except asyncio.CancelledError:
raise
except Exception as ex: # noqa: BLE001 - this loop should never stop
self.async_set_update_error(ex)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())

View File

@@ -2,20 +2,16 @@
from __future__ import annotations
import asyncio
from asyncio import CancelledError, Task
from contextlib import suppress
from asyncio import Task
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
from pyblu import Input, Player, Preset, Status, SyncStatus
from pyblu.errors import PlayerUnreachableError
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
BrowseMedia,
MediaPlayerEntity,
MediaPlayerEntityFeature,
@@ -23,16 +19,10 @@ from homeassistant.components.media_player import (
MediaType,
async_process_play_media_url,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -43,10 +33,11 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
import homeassistant.util.dt as dt_util
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN, INTEGRATION_TITLE
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
from .coordinator import BluesoundCoordinator
from .utils import dispatcher_join_signal, dispatcher_unjoin_signal, format_unique_id
if TYPE_CHECKING:
@@ -64,71 +55,8 @@ SERVICE_JOIN = "join"
SERVICE_SET_TIMER = "set_sleep_timer"
SERVICE_UNJOIN = "unjoin"
NODE_OFFLINE_CHECK_TIMEOUT = 180
NODE_RETRY_INITIATION = timedelta(minutes=3)
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
POLL_TIMEOUT = 120
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOSTS): vol.All(
cv.ensure_list,
[
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
],
)
}
)
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
"""Import config entry from configuration.yaml."""
if not hass.config_entries.async_entries(DOMAIN):
# Start import flow
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
if (
result["type"] == FlowResultType.ABORT
and result["reason"] == "cannot_connect"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2025.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.2.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -137,10 +65,10 @@ async def async_setup_entry(
) -> None:
"""Set up the Bluesound entry."""
bluesound_player = BluesoundPlayer(
config_entry.runtime_data.coordinator,
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.runtime_data.player,
config_entry.runtime_data.sync_status,
)
platform = entity_platform.async_get_current_platform()
@@ -155,27 +83,10 @@ async def async_setup_entry(
)
platform.async_register_entity_service(SERVICE_UNJOIN, None, "async_unjoin")
hass.data[DATA_BLUESOUND].append(bluesound_player)
async_add_entities([bluesound_player], update_before_add=True)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None,
) -> None:
"""Trigger import flows."""
hosts = config.get(CONF_HOSTS, [])
for host in hosts:
import_data = {
CONF_HOST: host[CONF_HOST],
CONF_PORT: host.get(CONF_PORT, 11000),
}
hass.async_create_task(_async_import(hass, import_data))
class BluesoundPlayer(MediaPlayerEntity):
class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity):
"""Representation of a Bluesound Player."""
_attr_media_content_type = MediaType.MUSIC
@@ -184,12 +95,15 @@ class BluesoundPlayer(MediaPlayerEntity):
def __init__(
self,
coordinator: BluesoundCoordinator,
host: str,
port: int,
player: Player,
sync_status: SyncStatus,
) -> None:
"""Initialize the media player."""
super().__init__(coordinator)
sync_status = coordinator.data.sync_status
self.host = host
self.port = port
self._poll_status_loop_task: Task[None] | None = None
@@ -197,15 +111,14 @@ class BluesoundPlayer(MediaPlayerEntity):
self._id = sync_status.id
self._last_status_update: datetime | None = None
self._sync_status = sync_status
self._status: Status | None = None
self._inputs: list[Input] = []
self._presets: list[Preset] = []
self._status: Status = coordinator.data.status
self._inputs: list[Input] = coordinator.data.inputs
self._presets: list[Preset] = coordinator.data.presets
self._group_name: str | None = None
self._group_list: list[str] = []
self._bluesound_device_name = sync_status.name
self._player = player
self._is_leader = False
self._leader: BluesoundPlayer | None = None
self._last_status_update = dt_util.utcnow()
self._attr_unique_id = format_unique_id(sync_status.mac, port)
# there should always be one player with the default port per mac
@@ -228,52 +141,10 @@ class BluesoundPlayer(MediaPlayerEntity):
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
async def _poll_status_loop(self) -> None:
"""Loop which polls the status of the player."""
while True:
try:
await self.async_update_status()
except PlayerUnreachableError:
_LOGGER.error(
"Node %s:%s is offline, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
except CancelledError:
_LOGGER.debug(
"Stopping the polling of node %s:%s", self.host, self.port
)
return
except: # noqa: E722 - this loop should never stop
_LOGGER.exception(
"Unexpected error for %s:%s, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
async def _poll_sync_status_loop(self) -> None:
"""Loop which polls the sync status of the player."""
while True:
try:
await self.update_sync_status()
except PlayerUnreachableError:
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
except CancelledError:
raise
except: # noqa: E722 - all errors must be caught for this loop
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
async def async_added_to_hass(self) -> None:
"""Start the polling task."""
await super().async_added_to_hass()
self._poll_status_loop_task = self.hass.async_create_background_task(
self._poll_status_loop(),
name=f"bluesound.poll_status_loop_{self.host}:{self.port}",
)
self._poll_sync_status_loop_task = self.hass.async_create_background_task(
self._poll_sync_status_loop(),
name=f"bluesound.poll_sync_status_loop_{self.host}:{self.port}",
)
assert self._sync_status.id is not None
self.async_on_remove(
async_dispatcher_connect(
@@ -294,105 +165,24 @@ class BluesoundPlayer(MediaPlayerEntity):
"""Stop the polling task."""
await super().async_will_remove_from_hass()
assert self._poll_status_loop_task is not None
if self._poll_status_loop_task.cancel():
# the sleeps in _poll_loop will raise CancelledError
with suppress(CancelledError):
await self._poll_status_loop_task
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._sync_status = self.coordinator.data.sync_status
self._status = self.coordinator.data.status
self._inputs = self.coordinator.data.inputs
self._presets = self.coordinator.data.presets
assert self._poll_sync_status_loop_task is not None
if self._poll_sync_status_loop_task.cancel():
# the sleeps in _poll_sync_status_loop will raise CancelledError
with suppress(CancelledError):
await self._poll_sync_status_loop_task
self.hass.data[DATA_BLUESOUND].remove(self)
async def async_update(self) -> None:
"""Update internal status of the entity."""
if not self.available:
return
with suppress(PlayerUnreachableError):
await self.async_update_presets()
await self.async_update_captures()
async def async_update_status(self) -> None:
"""Use the poll session to always get the status of the player."""
etag = None
if self._status is not None:
etag = self._status.etag
try:
status = await self._player.status(
etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
)
self._attr_available = True
self._last_status_update = dt_util.utcnow()
self._status = status
self.async_write_ha_state()
except PlayerUnreachableError:
self._attr_available = False
self._last_status_update = None
self._status = None
self.async_write_ha_state()
_LOGGER.error(
"Client connection error, marking %s as offline",
self._bluesound_device_name,
)
raise
async def update_sync_status(self) -> None:
"""Update the internal status."""
etag = None
if self._sync_status:
etag = self._sync_status.etag
sync_status = await self._player.sync_status(
etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
)
self._sync_status = sync_status
self._last_status_update = dt_util.utcnow()
self._group_list = self.rebuild_bluesound_group()
if sync_status.leader is not None:
self._is_leader = False
leader_id = f"{sync_status.leader.ip}:{sync_status.leader.port}"
leader_device = [
device
for device in self.hass.data[DATA_BLUESOUND]
if device.id == leader_id
]
if leader_device and leader_id != self.id:
self._leader = leader_device[0]
else:
self._leader = None
_LOGGER.error("Leader not found %s", leader_id)
else:
if self._leader is not None:
self._leader = None
followers = self._sync_status.followers
self._is_leader = followers is not None
self.async_write_ha_state()
async def async_update_captures(self) -> None:
"""Update Capture sources."""
inputs = await self._player.inputs()
self._inputs = inputs
async def async_update_presets(self) -> None:
"""Update Presets."""
presets = await self._player.presets()
self._presets = presets
@property
def state(self) -> MediaPlayerState:
"""Return the state of the device."""
if self._status is None:
if self.available is False:
return MediaPlayerState.OFF
if self.is_grouped and not self.is_leader:
@@ -409,7 +199,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_title(self) -> str | None:
"""Title of current playing media."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
return self._status.name
@@ -417,7 +207,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_artist(self) -> str | None:
"""Artist of current playing media (Music track only)."""
if self._status is None:
if self.available is False:
return None
if self.is_grouped and not self.is_leader:
@@ -428,7 +218,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_album_name(self) -> str | None:
"""Artist of current playing media (Music track only)."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
return self._status.album
@@ -436,7 +226,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_image_url(self) -> str | None:
"""Image url of current playing media."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
url = self._status.image
@@ -451,7 +241,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
mediastate = self.state
@@ -470,7 +260,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def media_duration(self) -> int | None:
"""Duration of current playing media in seconds."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
duration = self._status.total_seconds
@@ -487,16 +277,11 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
volume = None
volume = self._status.volume
if self._status is not None:
volume = self._status.volume
if self.is_grouped:
volume = self._sync_status.volume
if volume is None:
return None
return volume / 100
@property
@@ -529,7 +314,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def source_list(self) -> list[str] | None:
"""List of available input sources."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
sources = [x.text for x in self._inputs]
@@ -540,7 +325,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Name of the current input source."""
if self._status is None or (self.is_grouped and not self.is_leader):
if self.available is False or (self.is_grouped and not self.is_leader):
return None
if self._status.input_id is not None:
@@ -557,7 +342,7 @@ class BluesoundPlayer(MediaPlayerEntity):
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag of media commands that are supported."""
if self._status is None:
if self.available is False:
return MediaPlayerEntityFeature(0)
if self.is_grouped and not self.is_leader:
@@ -659,16 +444,21 @@ class BluesoundPlayer(MediaPlayerEntity):
if self.sync_status.leader is None and self.sync_status.followers is None:
return []
player_entities: list[BluesoundPlayer] = self.hass.data[DATA_BLUESOUND]
config_entries: list[BluesoundConfigEntry] = (
self.hass.config_entries.async_entries(DOMAIN)
)
sync_status_list = [
x.runtime_data.coordinator.data.sync_status for x in config_entries
]
leader_sync_status: SyncStatus | None = None
if self.sync_status.leader is None:
leader_sync_status = self.sync_status
else:
required_id = f"{self.sync_status.leader.ip}:{self.sync_status.leader.port}"
for x in player_entities:
if x.sync_status.id == required_id:
leader_sync_status = x.sync_status
for sync_status in sync_status_list:
if sync_status.id == required_id:
leader_sync_status = sync_status
break
if leader_sync_status is None or leader_sync_status.followers is None:
@@ -676,9 +466,9 @@ class BluesoundPlayer(MediaPlayerEntity):
follower_ids = [f"{x.ip}:{x.port}" for x in leader_sync_status.followers]
follower_names = [
x.sync_status.name
for x in player_entities
if x.sync_status.id in follower_ids
sync_status.name
for sync_status in sync_status_list
if sync_status.id in follower_ids
]
follower_names.insert(0, leader_sync_status.name)
return follower_names

View File

@@ -19,7 +19,7 @@
"bluetooth-adapters==0.20.2",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.20.0",
"dbus-fast==2.24.3",
"habluetooth==3.6.0"
"dbus-fast==2.28.0",
"habluetooth==3.7.0"
]
}

View File

@@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["bring_api"],
"requirements": ["bring-api==0.9.1"]
}

View File

@@ -12,7 +12,7 @@
}
},
"discovery_confirm": {
"description": "Do you want to setup {name}?"
"description": "Do you want to set up {name}?"
},
"reconfigure": {
"description": "Reconfigure your Cambridge Audio Streamer.",
@@ -28,7 +28,7 @@
"cannot_connect": "Failed to connect to Cambridge Audio device. Please make sure the device is powered up and connected to the network. Try power-cycling the device if it does not connect."
},
"abort": {
"wrong_device": "This Cambridge Audio device does not match the existing device id. Please make sure you entered the correct IP address.",
"wrong_device": "This Cambridge Audio device does not match the existing device ID. Please make sure you entered the correct IP address.",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"

View File

@@ -516,6 +516,19 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Flag supported features."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> CameraEntityFeature:
"""Return the supported features as CameraEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = CameraEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
@cached_property
def is_recording(self) -> bool:
"""Return true if the device is recording."""
@@ -569,7 +582,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self._deprecate_attr_frontend_stream_type_logged = True
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features:
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
if (
self._webrtc_provider
@@ -798,7 +811,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async def async_internal_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_internal_added_to_hass()
self.__supports_stream = self.supported_features & CameraEntityFeature.STREAM
self.__supports_stream = (
self.supported_features_compat & CameraEntityFeature.STREAM
)
await self.async_refresh_providers(write_state=False)
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
@@ -838,7 +853,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
) -> _T | None:
"""Get first provider that supports this camera."""
if CameraEntityFeature.STREAM not in self.supported_features:
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
return await fn(self.hass, self)
@@ -896,7 +911,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def camera_capabilities(self) -> CameraCapabilities:
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features:
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
@@ -916,7 +931,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
super().async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features & CameraEntityFeature.STREAM
supports_stream := self.supported_features_compat
& CameraEntityFeature.STREAM
):
self.__supports_stream = supports_stream
self._invalidate_camera_capabilities_cache()

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import configparser
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar
from urllib.parse import urlparse
import aiohttp
@@ -129,7 +129,7 @@ class ChromecastInfo:
class ChromeCastZeroconf:
"""Class to hold a zeroconf instance."""
__zconf: zeroconf.HaZeroconf | None = None
__zconf: ClassVar[zeroconf.HaZeroconf | None] = None
@classmethod
def set_zeroconf(cls, zconf: zeroconf.HaZeroconf) -> None:

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import re
from pexpect import pxssh
import voluptuous as vol
@@ -101,11 +100,11 @@ class CiscoDeviceScanner(DeviceScanner):
return False
def _get_arp_data(self):
def _get_arp_data(self) -> str | None:
"""Open connection to the router and get arp entries."""
try:
cisco_ssh = pxssh.pxssh()
cisco_ssh: pxssh.pxssh[str] = pxssh.pxssh(encoding="uft-8")
cisco_ssh.login(
self.host,
self.username,
@@ -115,12 +114,11 @@ class CiscoDeviceScanner(DeviceScanner):
)
# Find the hostname
initial_line = cisco_ssh.before.decode("utf-8").splitlines()
initial_line = (cisco_ssh.before or "").splitlines()
router_hostname = initial_line[len(initial_line) - 1]
router_hostname += "#"
# Set the discovered hostname as prompt
regex_expression = f"(?i)^{router_hostname}".encode()
cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
cisco_ssh.PROMPT = f"(?i)^{router_hostname}"
# Allow full arp table to print at once
cisco_ssh.sendline("terminal length 0")
cisco_ssh.prompt(1)
@@ -128,13 +126,11 @@ class CiscoDeviceScanner(DeviceScanner):
cisco_ssh.sendline("show ip arp")
cisco_ssh.prompt(1)
devices_result = cisco_ssh.before
return devices_result.decode("utf-8")
except pxssh.ExceptionPxssh as px_e:
_LOGGER.error("Failed to login via pxssh: %s", px_e)
return None
return None
return cisco_ssh.before
def _parse_cisco_mac_address(cisco_hardware_addr):

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pexpect", "ptyprocess"],
"quality_scale": "legacy",
"requirements": ["pexpect==4.6.0"]
"requirements": ["pexpect==4.9.0"]
}

View File

@@ -36,7 +36,14 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.signal_type import SignalType
from . import account_link, http_api
# Pre-import backup to avoid it being imported
# later when the import executor is busy and delaying
# startup
from . import (
account_link,
backup, # noqa: F401
http_api,
)
from .client import CloudClient
from .const import (
CONF_ACCOUNT_LINK_SERVER,

View File

@@ -2,12 +2,15 @@
from __future__ import annotations
import asyncio
import base64
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
import hashlib
from typing import Any, Self
import logging
import random
from typing import Any
from aiohttp import ClientError, ClientTimeout, StreamReader
from aiohttp import ClientError, ClientTimeout
from hass_nabucasa import Cloud, CloudError
from hass_nabucasa.cloud_api import (
async_files_delete_file,
@@ -18,12 +21,17 @@ from hass_nabucasa.cloud_api import (
from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .client import CloudClient
from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT
_LOGGER = logging.getLogger(__name__)
_STORAGE_BACKUP = "backup"
_RETRY_LIMIT = 5
_RETRY_SECONDS_MIN = 60
_RETRY_SECONDS_MAX = 600
async def _b64md5(stream: AsyncIterator[bytes]) -> str:
@@ -71,31 +79,6 @@ def async_register_backup_agents_listener(
return unsub
class ChunkAsyncStreamIterator:
"""Async iterator for chunked streams.
Based on aiohttp.streams.ChunkTupleAsyncStreamIterator, but yields
bytes instead of tuple[bytes, bool].
"""
__slots__ = ("_stream",)
def __init__(self, stream: StreamReader) -> None:
"""Initialize."""
self._stream = stream
def __aiter__(self) -> Self:
"""Iterate."""
return self
async def __anext__(self) -> bytes:
"""Yield next chunk."""
rv = await self._stream.readchunk()
if rv == (b"", False):
raise StopAsyncIteration
return rv[0]
class CloudBackupAgent(BackupAgent):
"""Cloud backup agent."""
@@ -136,13 +119,55 @@ class CloudBackupAgent(BackupAgent):
raise BackupAgentError("Failed to get download details") from err
try:
resp = await self._cloud.websession.get(details["url"])
resp = await self._cloud.websession.get(
details["url"],
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
)
resp.raise_for_status()
except ClientError as err:
raise BackupAgentError("Failed to download backup") from err
return ChunkAsyncStreamIterator(resp.content)
async def _async_do_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
filename: str,
base64md5hash: str,
metadata: dict[str, Any],
size: int,
) -> None:
"""Upload a backup."""
try:
details = await async_files_upload_details(
self._cloud,
storage_type=_STORAGE_BACKUP,
filename=filename,
metadata=metadata,
size=size,
base64md5hash=base64md5hash,
)
except (ClientError, CloudError) as err:
raise BackupAgentError("Failed to get upload details") from err
try:
upload_status = await self._cloud.websession.put(
details["url"],
data=await open_stream(),
headers=details["headers"] | {"content-length": str(size)},
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
)
_LOGGER.log(
logging.DEBUG if upload_status.status < 400 else logging.WARNING,
"Backup upload status: %s",
upload_status.status,
)
upload_status.raise_for_status()
except (TimeoutError, ClientError) as err:
raise BackupAgentError("Failed to upload backup") from err
async def async_upload_backup(
self,
*,
@@ -159,29 +184,34 @@ class CloudBackupAgent(BackupAgent):
raise BackupAgentError("Cloud backups must be protected")
base64md5hash = await _b64md5(await open_stream())
filename = self._get_backup_filename()
metadata = backup.as_dict()
size = backup.size
try:
details = await async_files_upload_details(
self._cloud,
storage_type=_STORAGE_BACKUP,
filename=self._get_backup_filename(),
metadata=backup.as_dict(),
size=backup.size,
base64md5hash=base64md5hash,
)
except (ClientError, CloudError) as err:
raise BackupAgentError("Failed to get upload details") from err
try:
upload_status = await self._cloud.websession.put(
details["url"],
data=await open_stream(),
headers=details["headers"] | {"content-length": str(backup.size)},
timeout=ClientTimeout(connect=10.0, total=43200.0), # 43200s == 12h
)
upload_status.raise_for_status()
except (TimeoutError, ClientError) as err:
raise BackupAgentError("Failed to upload backup") from err
tries = 1
while tries <= _RETRY_LIMIT:
try:
await self._async_do_upload_backup(
open_stream=open_stream,
filename=filename,
base64md5hash=base64md5hash,
metadata=metadata,
size=size,
)
break
except BackupAgentError as err:
if tries == _RETRY_LIMIT:
raise
tries += 1
retry_timer = random.randint(_RETRY_SECONDS_MIN, _RETRY_SECONDS_MAX)
_LOGGER.info(
"Failed to upload backup, retrying (%s/%s) in %ss: %s",
tries,
_RETRY_LIMIT,
retry_timer,
err,
)
await asyncio.sleep(retry_timer)
async def async_delete_backup(
self,
@@ -208,6 +238,7 @@ class CloudBackupAgent(BackupAgent):
"""List backups."""
try:
backups = await async_files_list(self._cloud, storage_type=_STORAGE_BACKUP)
_LOGGER.debug("Cloud backups: %s", backups)
except (ClientError, CloudError) as err:
raise BackupAgentError("Failed to list backups") from err

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.9.1"]
"requirements": ["aiocomelit==0.10.1"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/compensation",
"iot_class": "calculated",
"quality_scale": "legacy",
"requirements": ["numpy==2.2.0"]
"requirements": ["numpy==2.2.1"]
}

View File

@@ -75,6 +75,7 @@ async def async_converse(
language: str | None = None,
agent_id: str | None = None,
device_id: str | None = None,
extra_system_prompt: str | None = None,
) -> ConversationResult:
"""Process text and get intent."""
agent = async_get_agent(hass, agent_id)
@@ -99,6 +100,7 @@ async def async_converse(
device_id=device_id,
language=language,
agent_id=agent_id,
extra_system_prompt=extra_system_prompt,
)
with async_conversation_trace() as trace:
trace.add_event(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.20"]
"requirements": ["hassil==2.1.0", "home-assistant-intents==2025.1.1"]
}

View File

@@ -40,6 +40,9 @@ class ConversationInput:
agent_id: str | None = None
"""Agent to use for processing."""
extra_system_prompt: str | None = None
"""Extra prompt to provide extra info to LLMs how to understand the command."""
def as_dict(self) -> dict[str, Any]:
"""Return input as a dict."""
return {
@@ -49,6 +52,7 @@ class ConversationInput:
"device_id": self.device_id,
"language": self.language,
"agent_id": self.agent_id,
"extra_system_prompt": self.extra_system_prompt,
}

View File

@@ -2,39 +2,29 @@
from __future__ import annotations
from cookidoo_api import Cookidoo, CookidooConfig, CookidooLocalizationConfig
import logging
from homeassistant.const import (
CONF_COUNTRY,
CONF_EMAIL,
CONF_LANGUAGE,
CONF_PASSWORD,
Platform,
)
from cookidoo_api import CookidooAuthException, CookidooRequestException
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
from .helpers import cookidoo_from_config_entry
PLATFORMS: list[Platform] = [Platform.TODO]
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.TODO]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool:
"""Set up Cookidoo from a config entry."""
cookidoo = Cookidoo(
async_get_clientsession(hass),
CookidooConfig(
email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
localization=CookidooLocalizationConfig(
country_code=entry.data[CONF_COUNTRY].lower(),
language=entry.data[CONF_LANGUAGE],
),
),
coordinator = CookidooDataUpdateCoordinator(
hass, await cookidoo_from_config_entry(hass, entry), entry
)
coordinator = CookidooDataUpdateCoordinator(hass, cookidoo, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
@@ -47,3 +37,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: CookidooConfigEntry
) -> bool:
"""Migrate config entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1 and config_entry.minor_version == 1:
# Add the unique uuid
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
try:
auth_data = await cookidoo.login()
except (CookidooRequestException, CookidooAuthException) as e:
_LOGGER.error(
"Could not migrate config config_entry: %s",
str(e),
)
return False
unique_id = auth_data.sub
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=config_entry.entry_id
)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
device_registry.async_update_device(
dev.id, new_identifiers={(DOMAIN, unique_id)}
)
for ent in entity_entries:
assert ent.config_entry_id
entity_registry.async_update_entity(
ent.entity_id,
new_unique_id=ent.unique_id.replace(ent.config_entry_id, unique_id),
)
hass.config_entries.async_update_entry(
config_entry, unique_id=auth_data.sub, minor_version=2
)
_LOGGER.debug(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True

View File

@@ -0,0 +1,71 @@
"""Support for Cookidoo buttons."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from cookidoo_api import Cookidoo, CookidooException
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
from .entity import CookidooBaseEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class CookidooButtonEntityDescription(ButtonEntityDescription):
"""Describes cookidoo button entity."""
press_fn: Callable[[Cookidoo], Awaitable[None]]
TODO_CLEAR = CookidooButtonEntityDescription(
key="todo_clear",
translation_key="todo_clear",
press_fn=lambda client: client.clear_shopping_list(),
entity_registry_enabled_default=False,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: CookidooConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Cookidoo button entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities([CookidooButton(coordinator, TODO_CLEAR)])
class CookidooButton(CookidooBaseEntity, ButtonEntity):
"""Defines an Cookidoo button."""
entity_description: CookidooButtonEntityDescription
def __init__(
self,
coordinator: CookidooDataUpdateCoordinator,
description: CookidooButtonEntityDescription,
) -> None:
"""Initialize cookidoo button."""
super().__init__(coordinator)
self.entity_description = description
assert coordinator.config_entry.unique_id
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
async def async_press(self) -> None:
"""Press the button."""
try:
await self.entity_description.press_fn(self.coordinator.cookidoo)
except CookidooException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="button_clear_todo_failed",
) from e
await self.coordinator.async_refresh()

View File

@@ -7,10 +7,7 @@ import logging
from typing import Any
from cookidoo_api import (
Cookidoo,
CookidooAuthException,
CookidooConfig,
CookidooLocalizationConfig,
CookidooRequestException,
get_country_options,
get_localization_options,
@@ -24,7 +21,6 @@ from homeassistant.config_entries import (
ConfigFlowResult,
)
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
CountrySelector,
CountrySelectorConfig,
@@ -36,6 +32,7 @@ from homeassistant.helpers.selector import (
)
from .const import DOMAIN
from .helpers import cookidoo_from_config_data
_LOGGER = logging.getLogger(__name__)
@@ -58,10 +55,14 @@ AUTH_DATA_SCHEMA = {
class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cookidoo."""
VERSION = 1
MINOR_VERSION = 2
COUNTRY_DATA_SCHEMA: dict
LANGUAGE_DATA_SCHEMA: dict
user_input: dict[str, Any]
user_uuid: str
async def async_step_reconfigure(
self, user_input: dict[str, Any]
@@ -79,8 +80,11 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None and not (
errors := await self.validate_input(user_input)
):
await self.async_set_unique_id(self.user_uuid)
if self.source == SOURCE_USER:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
self._abort_if_unique_id_configured()
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
self.user_input = user_input
return await self.async_step_language()
await self.generate_country_schema()
@@ -154,10 +158,8 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
if not (
errors := await self.validate_input({**reauth_entry.data, **user_input})
):
if user_input[CONF_EMAIL] != reauth_entry.data[CONF_EMAIL]:
self._async_abort_entries_match(
{CONF_EMAIL: user_input[CONF_EMAIL]}
)
await self.async_set_unique_id(self.user_uuid)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reauth_entry, data_updates=user_input
)
@@ -219,22 +221,12 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
else:
data_input[CONF_LANGUAGE] = (
await get_localization_options(country=data_input[CONF_COUNTRY].lower())
)[0] # Pick any language to test login
)[0].language # Pick any language to test login
session = async_get_clientsession(self.hass)
cookidoo = Cookidoo(
session,
CookidooConfig(
email=data_input[CONF_EMAIL],
password=data_input[CONF_PASSWORD],
localization=CookidooLocalizationConfig(
country_code=data_input[CONF_COUNTRY].lower(),
language=data_input[CONF_LANGUAGE],
),
),
)
cookidoo = await cookidoo_from_config_data(self.hass, data_input)
try:
await cookidoo.login()
auth_data = await cookidoo.login()
self.user_uuid = auth_data.sub
if language_input:
await cookidoo.get_additional_items()
except CookidooRequestException:

View File

@@ -21,10 +21,12 @@ class CookidooBaseEntity(CoordinatorEntity[CookidooDataUpdateCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
assert coordinator.config_entry.unique_id
self.device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name="Cookidoo",
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
manufacturer="Vorwerk International & Co. KmG",
model="Cookidoo - Thermomix® recipe portal",
)

View File

@@ -0,0 +1,37 @@
"""Helpers for cookidoo."""
from typing import Any
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import CookidooConfigEntry
async def cookidoo_from_config_data(
hass: HomeAssistant, data: dict[str, Any]
) -> Cookidoo:
"""Build cookidoo from config data."""
localizations = await get_localization_options(
country=data[CONF_COUNTRY].lower(),
language=data[CONF_LANGUAGE],
)
return Cookidoo(
async_get_clientsession(hass),
CookidooConfig(
email=data[CONF_EMAIL],
password=data[CONF_PASSWORD],
localization=localizations[0],
),
)
async def cookidoo_from_config_entry(
hass: HomeAssistant, entry: CookidooConfigEntry
) -> Cookidoo:
"""Build cookidoo from config entry."""
return await cookidoo_from_config_data(hass, dict(entry.data))

View File

@@ -1,5 +1,10 @@
{
"entity": {
"button": {
"todo_clear": {
"default": "mdi:cart-off"
}
},
"todo": {
"ingredient_list": {
"default": "mdi:cart-plus"

View File

@@ -6,6 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/cookidoo",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["cookidoo_api"],
"quality_scale": "silver",
"requirements": ["cookidoo-api==0.10.0"]
"requirements": ["cookidoo-api==0.12.2"]
}

View File

@@ -44,10 +44,16 @@
"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%]"
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The user identifier does not match the previous identifier"
}
},
"entity": {
"button": {
"todo_clear": {
"name": "Clear shopping list and additional purchases"
}
},
"todo": {
"ingredient_list": {
"name": "Shopping list"
@@ -58,6 +64,9 @@
}
},
"exceptions": {
"button_clear_todo_failed": {
"message": "Failed to clear all items from the Cookidoo shopping list"
},
"todo_save_item_failed": {
"message": "Failed to save {name} to Cookidoo shopping list"
},

View File

@@ -52,7 +52,8 @@ class CookidooIngredientsTodoListEntity(CookidooBaseEntity, TodoListEntity):
def __init__(self, coordinator: CookidooDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_ingredients"
assert coordinator.config_entry.unique_id
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_ingredients"
@property
def todo_items(self) -> list[TodoItem]:
@@ -112,7 +113,8 @@ class CookidooAdditionalItemTodoListEntity(CookidooBaseEntity, TodoListEntity):
def __init__(self, coordinator: CookidooDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_additional_items"
assert coordinator.config_entry.unique_id
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_additional_items"
@property
def todo_items(self) -> list[TodoItem]:

View File

@@ -300,6 +300,10 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def supported_features(self) -> CoverEntityFeature:
"""Flag supported features."""
if (features := self._attr_supported_features) is not None:
if type(features) is int: # noqa: E721
new_features = CoverEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
supported_features = (

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Sequence
import os
from serial.tools.list_ports_common import ListPortInfo
@@ -12,7 +13,7 @@ from .const import DONT_USE_USB, MANUAL_PATH, REFRESH_LIST
def list_ports_as_str(
serial_ports: list[ListPortInfo], no_usb_option: bool = True
serial_ports: Sequence[ListPortInfo], no_usb_option: bool = True
) -> list[str]:
"""Represent currently available serial ports as string.

View File

@@ -266,7 +266,7 @@ class DeconzBaseLight[_LightDeviceT: Group | Light](
@property
def color_temp_kelvin(self) -> int | None:
"""Return the CT color value."""
if self._device.color_temp is None:
if self._device.color_temp is None or self._device.color_temp == 0:
return None
return color_temperature_mired_to_kelvin(self._device.color_temp)

View File

@@ -0,0 +1 @@
"""Virtual integration: Decorquip."""

View File

@@ -0,0 +1,6 @@
{
"domain": "decorquip",
"name": "Decorquip Dream",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@@ -50,7 +50,7 @@
"services": {
"get_command": {
"name": "Get command",
"description": "Send sa generic HTTP get command.",
"description": "Sends a generic HTTP get command.",
"fields": {
"command": {
"name": "Command",

View File

@@ -19,7 +19,7 @@ rules:
The integration does not provide any additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
@@ -41,7 +41,7 @@ rules:
status: exempt
comment: |
The integration does not provide any additional options.
docs-installation-parameters: todo
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==11.0.0"]
"requirements": ["pydoods==1.0.2", "Pillow==11.1.0"]
}

View File

@@ -20,6 +20,7 @@ from dsmr_parser.objects import DSMRObject, MbusDevice, Telegram
import serial
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -456,24 +457,29 @@ def rename_old_gas_to_mbus(
if entity.unique_id.endswith(
"belgium_5min_gas_meter_reading"
) or entity.unique_id.endswith("hourly_gas_meter_reading"):
try:
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=mbus_device_id,
)
except ValueError:
if ent_reg.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, mbus_device_id
):
LOGGER.debug(
"Skip migration of %s because it already exists",
entity.entity_id,
)
else:
LOGGER.debug(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
continue
new_device = dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, mbus_device_id)},
)
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=new_device.id,
)
LOGGER.debug(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
# Cleanup old device
dev_entities = er.async_entries_for_device(
ent_reg, device_id, include_disabled_entities=True

View File

@@ -57,11 +57,11 @@
"services": {
"get_gas_prices": {
"name": "Get gas prices",
"description": "Request gas prices from easyEnergy.",
"description": "Requests gas prices from easyEnergy.",
"fields": {
"config_entry": {
"name": "Config Entry",
"description": "The config entry to use for this service."
"description": "The configuration entry to use for this action."
},
"incl_vat": {
"name": "VAT Included",
@@ -79,7 +79,7 @@
},
"get_energy_usage_prices": {
"name": "Get energy usage prices",
"description": "Request usage energy prices from easyEnergy.",
"description": "Requests usage energy prices from easyEnergy.",
"fields": {
"config_entry": {
"name": "[%key:component::easyenergy::services::get_gas_prices::fields::config_entry::name%]",
@@ -101,7 +101,7 @@
},
"get_energy_return_prices": {
"name": "Get energy return prices",
"description": "Request return energy prices from easyEnergy.",
"description": "Requests return energy prices from easyEnergy.",
"fields": {
"config_entry": {
"name": "[%key:component::easyenergy::services::get_gas_prices::fields::config_entry::name%]",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==10.0.1"]
"requirements": ["py-sucks==0.9.10", "deebot-client==10.1.0"]
}

View File

@@ -163,11 +163,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
data: dict[str, Any] = {}
data[ATTR_ERROR] = self.error
# these attributes are deprecated and can be removed in 2025.2
for key, val in self.device.components.items():
attr_name = ATTR_COMPONENT_PREFIX + key
data[attr_name] = int(val * 100)
return data
def return_to_base(self, **kwargs: Any) -> None:

View File

@@ -6,11 +6,16 @@ from dataclasses import dataclass
from elevenlabs import AsyncElevenLabs, Model
from elevenlabs.core import ApiError
from httpx import ConnectError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.httpx_client import get_async_client
from .const import CONF_MODEL
@@ -48,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ElevenLabsConfigEntry) -
model_id = entry.options[CONF_MODEL]
try:
model = await get_model_by_id(client, model_id)
except ConnectError as err:
raise ConfigEntryNotReady("Failed to connect") from err
except ApiError as err:
raise ConfigEntryAuthFailed("Auth failed") from err

View File

@@ -13,7 +13,7 @@ rules:
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: >

View File

@@ -49,7 +49,7 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
_element: Zone
_attr_entity_registry_enabled_default = False
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
# Zone in NORMAL state is OFF; any other state is ON
self._attr_is_on = bool(
self._element.logical_status != ZoneLogicalStatus.NORMAL

View File

@@ -120,7 +120,7 @@ class ElkCounter(ElkSensor):
_attr_icon = "mdi:numeric"
_element: Counter
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
self._attr_native_value = self._element.value
@@ -153,7 +153,7 @@ class ElkKeypad(ElkSensor):
attrs["last_keypress"] = self._element.last_keypress
return attrs
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
self._attr_native_value = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPERATURE
)
@@ -173,7 +173,7 @@ class ElkPanel(ElkSensor):
attrs["system_trouble_status"] = self._element.system_trouble_status
return attrs
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
if self._elk.is_connected():
self._attr_native_value = (
"Paused" if self._element.remote_programming_status else "Connected"
@@ -188,7 +188,7 @@ class ElkSetting(ElkSensor):
_attr_translation_key = "setting"
_element: Setting
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
self._attr_native_value = self._element.value
@property
@@ -257,7 +257,7 @@ class ElkZone(ElkSensor):
return UnitOfElectricPotential.VOLT
return None
def _element_changed(self, _: Element, changeset: Any) -> None:
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
if self._element.definition == ZoneType.TEMPERATURE:
self._attr_native_value = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPERATURE

View File

@@ -151,7 +151,9 @@ class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN):
port=self._panel_direct_port,
)
)
ssl_context = build_direct_ssl_context(cadata=self._panel_direct_ssl_cert)
ssl_context = await self.hass.async_add_executor_job(
build_direct_ssl_context, self._panel_direct_ssl_cert
)
# Attempt the connection to make sure the pin works. Also, take the chance to retrieve the panel ID via APIs.
client_api_url = get_direct_api_url(

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/elmax",
"iot_class": "cloud_polling",
"loggers": ["elmax_api"],
"requirements": ["elmax-api==0.0.6.3"],
"requirements": ["elmax-api==0.0.6.4rc0"],
"zeroconf": [
{
"type": "_elmax-ssl._tcp.local."

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.3.0"]
"requirements": ["openwebifpy==4.3.1"]
}

View File

@@ -8,11 +8,7 @@ rules:
comment: fixed 1 minute cycle based on Enphase Envoy device characteristics
brands: done
common-modules: done
config-flow-test-coverage:
status: todo
comment: |
- test_zero_conf_malformed_serial_property - with pytest.raises(KeyError) as ex::
I don't believe this should be able to raise a KeyError Shouldn't we abort the flow?
config-flow-test-coverage: done
config-flow:
status: todo
comment: |
@@ -60,11 +56,7 @@ rules:
status: done
comment: pending https://github.com/home-assistant/core/pull/132373
reauthentication-flow: done
test-coverage:
status: todo
comment: |
- test_config_different_unique_id -> unique_id set to the mock config entry is an int, not a str
- Apart from the coverage, test_option_change_reload does not verify that the config entry is reloaded
test-coverage: done
# Gold
devices: done

View File

@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.0.0"]
}

View File

@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
from aioesphomeapi import APIClient, DeviceInfo
from bleak_esphome import connect_scanner
from bleak_esphome.backend.cache import ESPHomeBluetoothCache
from homeassistant.components.bluetooth import async_register_scanner
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
@@ -28,10 +27,9 @@ def async_connect_scanner(
entry_data: RuntimeEntryData,
cli: APIClient,
device_info: DeviceInfo,
cache: ESPHomeBluetoothCache,
) -> CALLBACK_TYPE:
"""Connect scanner."""
client_data = connect_scanner(cli, device_info, cache, entry_data.available)
client_data = connect_scanner(cli, device_info, entry_data.available)
entry_data.bluetooth_device = client_data.bluetooth_device
client_data.disconnect_callbacks = entry_data.disconnect_callbacks
scanner = client_data.scanner

View File

@@ -6,8 +6,6 @@ from dataclasses import dataclass, field
from functools import cache
from typing import Self
from bleak_esphome.backend.cache import ESPHomeBluetoothCache
from homeassistant.core import HomeAssistant
from homeassistant.helpers.json import JSONEncoder
@@ -22,9 +20,6 @@ class DomainData:
"""Define a class that stores global esphome data in hass.data[DOMAIN]."""
_stores: dict[str, ESPHomeStorage] = field(default_factory=dict)
bluetooth_cache: ESPHomeBluetoothCache = field(
default_factory=ESPHomeBluetoothCache
)
def get_entry_data(self, entry: ESPHomeConfigEntry) -> RuntimeEntryData:
"""Return the runtime entry data associated with this config entry.

View File

@@ -423,9 +423,7 @@ class ESPHomeManager:
if device_info.bluetooth_proxy_feature_flags_compat(api_version):
entry_data.disconnect_callbacks.add(
async_connect_scanner(
hass, entry_data, cli, device_info, self.domain_data.bluetooth_cache
)
async_connect_scanner(hass, entry_data, cli, device_info)
)
if device_info.voice_assistant_feature_flags_compat(api_version) and (

View File

@@ -18,7 +18,7 @@
"requirements": [
"aioesphomeapi==28.0.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.1.0"
"bleak-esphome==2.0.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -14,6 +14,7 @@ import feedparser
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -101,7 +102,11 @@ class FeedReaderCoordinator(
async def async_setup(self) -> None:
"""Set up the feed manager."""
feed = await self._async_fetch_feed()
try:
feed = await self._async_fetch_feed()
except UpdateFailed as err:
raise ConfigEntryNotReady from err
self.logger.debug("Feed data fetched from %s : %s", self.url, feed["feed"])
if feed_author := feed["feed"].get("author"):
self.feed_author = html.unescape(feed_author)

View File

@@ -23,10 +23,10 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.system_info import is_official_image
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.signal_type import SignalType
from homeassistant.util.system_info import is_official_image
DOMAIN = "ffmpeg"

View File

@@ -2,10 +2,11 @@
from datetime import datetime as dt
import logging
from typing import Any
import jwt
from pyflick import FlickAPI
from pyflick.authentication import AbstractFlickAuth
from pyflick.authentication import SimpleFlickAuth
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
from homeassistant.config_entries import ConfigEntry
@@ -20,7 +21,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from .const import CONF_TOKEN_EXPIRY, DOMAIN
from .const import CONF_ACCOUNT_ID, CONF_SUPPLY_NODE_REF, CONF_TOKEN_EXPIRY
from .coordinator import FlickConfigEntry, FlickElectricDataCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -29,36 +31,85 @@ CONF_ID_TOKEN = "id_token"
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> bool:
"""Set up Flick Electric from a config entry."""
auth = HassFlickAuth(hass, entry)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth)
coordinator = FlickElectricDataCoordinator(
hass, FlickAPI(auth), entry.data[CONF_SUPPLY_NODE_REF]
)
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: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: FlickConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
class HassFlickAuth(AbstractFlickAuth):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 2:
return False
if config_entry.version == 1:
api = FlickAPI(HassFlickAuth(hass, config_entry))
accounts = await api.getCustomerAccounts()
active_accounts = [
account for account in accounts if account["status"] == "active"
]
# A single active account can be auto-migrated
if (len(active_accounts)) == 1:
account = active_accounts[0]
new_data = {**config_entry.data}
new_data[CONF_ACCOUNT_ID] = account["id"]
new_data[CONF_SUPPLY_NODE_REF] = account["main_consumer"]["supply_node_ref"]
hass.config_entries.async_update_entry(
config_entry,
title=account["address"],
unique_id=account["id"],
data=new_data,
version=2,
)
return True
config_entry.async_start_reauth(hass, data={**config_entry.data})
return False
return True
class HassFlickAuth(SimpleFlickAuth):
"""Implementation of AbstractFlickAuth based on a Home Assistant entity config."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: FlickConfigEntry) -> None:
"""Flick authentication based on a Home Assistant entity config."""
super().__init__(aiohttp_client.async_get_clientsession(hass))
super().__init__(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
client_id=entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID),
client_secret=entry.data.get(CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET),
websession=aiohttp_client.async_get_clientsession(hass),
)
self._entry = entry
self._hass = hass
async def _get_entry_token(self):
async def _get_entry_token(self) -> dict[str, Any]:
# No token saved, generate one
if (
CONF_TOKEN_EXPIRY not in self._entry.data
@@ -75,13 +126,8 @@ class HassFlickAuth(AbstractFlickAuth):
async def _update_token(self):
_LOGGER.debug("Fetching new access token")
token = await self.get_new_token(
username=self._entry.data[CONF_USERNAME],
password=self._entry.data[CONF_PASSWORD],
client_id=self._entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID),
client_secret=self._entry.data.get(
CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET
),
token = await super().get_new_token(
self._username, self._password, self._client_id, self._client_secret
)
_LOGGER.debug("New token: %s", token)

View File

@@ -1,14 +1,18 @@
"""Config Flow for Flick Electric integration."""
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from pyflick.authentication import AuthException, SimpleFlickAuth
from aiohttp import ClientResponseError
from pyflick import FlickAPI
from pyflick.authentication import AbstractFlickAuth, SimpleFlickAuth
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
from pyflick.types import APIException, AuthException, CustomerAccount
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
@@ -17,12 +21,18 @@ from homeassistant.const import (
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import DOMAIN
from .const import CONF_ACCOUNT_ID, CONF_SUPPLY_NODE_REF, DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
LOGIN_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
@@ -35,10 +45,13 @@ DATA_SCHEMA = vol.Schema(
class FlickConfigFlow(ConfigFlow, domain=DOMAIN):
"""Flick config flow."""
VERSION = 1
VERSION = 2
auth: AbstractFlickAuth
accounts: list[CustomerAccount]
data: dict[str, Any]
async def _validate_input(self, user_input):
auth = SimpleFlickAuth(
async def _validate_auth(self, user_input: Mapping[str, Any]) -> bool:
self.auth = SimpleFlickAuth(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
websession=aiohttp_client.async_get_clientsession(self.hass),
@@ -48,22 +61,83 @@ class FlickConfigFlow(ConfigFlow, domain=DOMAIN):
try:
async with asyncio.timeout(60):
token = await auth.async_get_access_token()
except TimeoutError as err:
token = await self.auth.async_get_access_token()
except (TimeoutError, ClientResponseError) as err:
raise CannotConnect from err
except AuthException as err:
raise InvalidAuth from err
return token is not None
async def async_step_select_account(
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Ask user to select account."""
errors = {}
if user_input is not None and CONF_ACCOUNT_ID in user_input:
self.data[CONF_ACCOUNT_ID] = user_input[CONF_ACCOUNT_ID]
self.data[CONF_SUPPLY_NODE_REF] = self._get_supply_node_ref(
user_input[CONF_ACCOUNT_ID]
)
try:
# Ensure supply node is active
await FlickAPI(self.auth).getPricing(self.data[CONF_SUPPLY_NODE_REF])
except (APIException, ClientResponseError):
errors["base"] = "cannot_connect"
except AuthException:
# We should never get here as we have a valid token
return self.async_abort(reason="no_permissions")
else:
# Supply node is active
return await self._async_create_entry()
try:
self.accounts = await FlickAPI(self.auth).getCustomerAccounts()
except (APIException, ClientResponseError):
errors["base"] = "cannot_connect"
active_accounts = [a for a in self.accounts if a["status"] == "active"]
if len(active_accounts) == 0:
return self.async_abort(reason="no_accounts")
if len(active_accounts) == 1:
self.data[CONF_ACCOUNT_ID] = active_accounts[0]["id"]
self.data[CONF_SUPPLY_NODE_REF] = self._get_supply_node_ref(
active_accounts[0]["id"]
)
return await self._async_create_entry()
return self.async_show_form(
step_id="select_account",
data_schema=vol.Schema(
{
vol.Required(CONF_ACCOUNT_ID): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=account["id"], label=account["address"]
)
for account in active_accounts
],
mode=SelectSelectorMode.LIST,
)
)
}
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
self, user_input: Mapping[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle gathering login info."""
errors = {}
if user_input is not None:
try:
await self._validate_input(user_input)
await self._validate_auth(user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -72,20 +146,61 @@ class FlickConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
f"flick_electric_{user_input[CONF_USERNAME]}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"Flick Electric: {user_input[CONF_USERNAME]}",
data=user_input,
)
self.data = dict(user_input)
return await self.async_step_select_account(user_input)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="user", data_schema=LOGIN_SCHEMA, errors=errors
)
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication."""
self.data = {**user_input}
return await self.async_step_user(user_input)
async def _async_create_entry(self) -> ConfigFlowResult:
"""Create an entry for the flow."""
await self.async_set_unique_id(self.data[CONF_ACCOUNT_ID])
account = self._get_account(self.data[CONF_ACCOUNT_ID])
if self.source == SOURCE_REAUTH:
# Migration completed
if self._get_reauth_entry().version == 1:
self.hass.config_entries.async_update_entry(
self._get_reauth_entry(),
unique_id=self.unique_id,
data=self.data,
version=self.VERSION,
)
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
unique_id=self.unique_id,
title=account["address"],
data=self.data,
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=account["address"],
data=self.data,
)
def _get_account(self, account_id: str) -> CustomerAccount:
"""Get the account for the account ID."""
return next(a for a in self.accounts if a["id"] == account_id)
def _get_supply_node_ref(self, account_id: str) -> str:
"""Get the supply node ref for the account."""
return self._get_account(account_id)["main_consumer"][CONF_SUPPLY_NODE_REF]
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@@ -3,6 +3,8 @@
DOMAIN = "flick_electric"
CONF_TOKEN_EXPIRY = "expires"
CONF_ACCOUNT_ID = "account_id"
CONF_SUPPLY_NODE_REF = "supply_node_ref"
ATTR_START_AT = "start_at"
ATTR_END_AT = "end_at"

View File

@@ -0,0 +1,47 @@
"""Data Coordinator for Flick Electric."""
import asyncio
from datetime import timedelta
import logging
import aiohttp
from pyflick import FlickAPI, FlickPrice
from pyflick.types import APIException, AuthException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=5)
type FlickConfigEntry = ConfigEntry[FlickElectricDataCoordinator]
class FlickElectricDataCoordinator(DataUpdateCoordinator[FlickPrice]):
"""Coordinator for flick power price."""
def __init__(
self, hass: HomeAssistant, api: FlickAPI, supply_node_ref: str
) -> None:
"""Initialize FlickElectricDataCoordinator."""
super().__init__(
hass,
_LOGGER,
name="Flick Electric",
update_interval=SCAN_INTERVAL,
)
self.supply_node_ref = supply_node_ref
self._api = api
async def _async_update_data(self) -> FlickPrice:
"""Fetch pricing data from Flick Electric."""
try:
async with asyncio.timeout(60):
return await self._api.getPricing(self.supply_node_ref)
except AuthException as err:
raise ConfigEntryAuthFailed from err
except (APIException, aiohttp.ClientResponseError) as err:
raise UpdateFailed from err

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyflick"],
"requirements": ["PyFlick==0.0.2"]
"requirements": ["PyFlick==1.1.3"]
}

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