Compare commits

..

145 Commits

Author SHA1 Message Date
Franck Nijhof 6145ea2323 2025.1.4 (#136407)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Matt Doran <mattdoran76@gmail.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Makrit <sinticlee@gmail.com>
Co-authored-by: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Yuxin Wang <yuxinwang.dev@gmail.com>
Co-authored-by: Åke Strandberg <ake@strandberg.eu>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Klaas Schoute <klaas_schoute@hotmail.com>
Fix slave id equal to 0 (#136263)
2025-01-24 09:50:20 +01:00
Franck Nijhof 223b437cb9 Bump version to 2025.1.4 2025-01-24 08:02:10 +00:00
Klaas Schoute b9443fa204 Bump powerfox to v1.2.1 (#136366) 2025-01-24 08:01:52 +00:00
Joost Lekkerkerker acbbb19788 Bump aiowithings to 3.1.5 (#136350) 2025-01-24 08:01:49 +00:00
Paul Bottein 7590a868b9 Update frontend to 20250109.2 (#136348) 2025-01-24 08:01:45 +00:00
Paul Bottein 4b13c20e74 Update frontend to 20250109.1 (#136339) 2025-01-24 08:01:42 +00:00
Åke Strandberg 4cf1b1a707 Avoid keyerror on incomplete api data in myuplink (#136333)
* Avoid keyerror

* Inject erroneous value in device point fixture

* Update diagnostics snapshot
2025-01-24 08:01:39 +00:00
Franck Nijhof 1f8129f4b8 Update peblar to v0.4.0 (#136329)
* Update peblar to v0.4.0

* Update snapshots
2025-01-24 08:01:35 +00:00
Yuxin Wang 2e4a19b058 Fallback to None for literal "Blank" serial number for APCUPSD integration (#136297)
* Fallback to None for Blank serial number

* Fix comments
2025-01-24 08:01:32 +00:00
Simon Lamon 0caa1ed825 Handle LinkPlay devices with no mac (#136272)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-24 08:01:28 +00:00
Claudio Ruggeri - CR-Tech e7a4f5fd27 Fix slave id equal to 0 (#136263)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-24 08:00:49 +00:00
Makrit 0512fc5e0c Handle width and height placeholders in the thumbnail URL (#136227) 2025-01-24 07:52:29 +00:00
G Johansson 8440a27152 Bump holidays to 0.65 (#136122) 2025-01-24 07:52:26 +00:00
Matt Doran 7af7219b01 Update Hydrawise maximum watering duration to meet the app limits (#136050)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-01-24 07:52:22 +00:00
Franck Nijhof 3e1d13b6ad 2025.1.3 (#136092) 2025-01-20 18:04:03 +01:00
Franck Nijhof d9e6549ad5 Bump version to 2025.1.3 2025-01-20 16:03:47 +00:00
Erik Montnemery 3c534a73f5 Always include SSL folder in backups (#136080) 2025-01-20 16:03:35 +00:00
Robert Resch 92b786e8cf Bump deebot-client to 11.0.0 (#136073) 2025-01-20 16:03:32 +00:00
Joost Lekkerkerker 4ed027b1cc Bump yt-dlp to 2025.01.15 (#136072) 2025-01-20 16:03:29 +00:00
J. Nick Koston b9b9322c91 Bump onvif-zeep-async to 3.2.3 (#136022) 2025-01-20 16:03:26 +00:00
Scott K Logan 3922b8eb80 Bump aioraven to 0.7.1 (#136017) 2025-01-20 16:03:23 +00:00
J. Nick Koston 5d1e2d17da Handle invalid datetime in onvif (#136014) 2025-01-20 16:03:20 +00:00
Joakim Plate b1445e5926 Correct type for off delay in rfxtrx (#135994) 2025-01-20 16:03:17 +00:00
Joost Lekkerkerker 8101fee9bb Fix switchbot cloud library logger (#135987) 2025-01-20 16:03:13 +00:00
J. Nick Koston 670371ff38 Bump aiooui to 0.1.9 (#135956) 2025-01-20 16:02:24 +00:00
J. Nick Koston f8eb42a094 Bump aiooui to 0.1.8 (#135945) 2025-01-20 16:00:39 +00:00
Matthias Alphart ca891bfc3e Update knx-frontend to 2025.1.18.164225 (#135941) 2025-01-20 15:58:44 +00:00
Glenn Vandeuren (aka Iondependent) 6da6de6a35 Update NHC lib to v0.3.4 (#135923)
Update NHC to v0.3.4
2025-01-20 15:58:40 +00:00
Glenn Vandeuren (aka Iondependent) 1bf1804492 Round brightness in Niko Home Control (#135920) 2025-01-20 15:58:37 +00:00
J. Nick Koston 11205f1c9d Bump onvif-zeep-async to 3.2.2 (#135898) 2025-01-20 15:58:34 +00:00
J. Nick Koston 84b3db1674 Prevent HomeKit from going unavailable when min/max is reversed (#135892) 2025-01-20 15:58:30 +00:00
Raphael Hehl a42c2b2986 Remove device_class from NFC and fingerprint event descriptions (#135867) 2025-01-20 15:58:27 +00:00
Álvaro Fernández Rojas 480045887a Update aioairzone to v0.9.9 (#135866)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-20 15:58:23 +00:00
J. Nick Koston 4f5235cbd4 Handle invalid HS color values in HomeKit Bridge (#135739) 2025-01-20 15:58:20 +00:00
Joost Lekkerkerker 83ab6b8ea2 Add reauthentication to SmartThings (#135673)
* Add reauthentication to SmartThings

* Add reauthentication to SmartThings

* Add reauthentication to SmartThings

* Add reauthentication to SmartThings
2025-01-20 15:58:16 +00:00
Jan Bouwhuis cc0989b50e Fix mqtt number state validation (#135621) 2025-01-20 15:58:12 +00:00
Glenn Waters 44046c5f83 Bump elkm1-lib to 2.2.11 (#135616) 2025-01-20 15:58:09 +00:00
Joost Lekkerkerker 0bd03346e8 Use device supplied ranges in LaMetric (#135590) 2025-01-20 15:58:05 +00:00
Joost Lekkerkerker c6cde13615 Bump demetriek to 1.2.0 (#135580) 2025-01-20 15:58:02 +00:00
Michael Hansen 0e37e04928 Use STT/TTS languages for LLM fallback (#135533) 2025-01-20 15:57:59 +00:00
Artur Pragacz bef545259e Fix referenced objects in script sequences (#135499) 2025-01-20 15:57:55 +00:00
Khole d77ec8ffbe Replace pyhiveapi with pyhive-integration (#135482) 2025-01-20 15:57:52 +00:00
Mick Vleeshouwer 75a1a46a49 Fix incorrect cast in HitachiAirToWaterHeatingZone in Overkiz (#135468) 2025-01-20 15:57:48 +00:00
Ravaka Razafimanantsoa 2b636423d9 Bump switchbot-api to 2.3.1 (#135451) 2025-01-20 15:57:45 +00:00
Norbert Rittel ed4c54a700 Fix descriptions of send_message action of Bring! integration (#135446)
* Make "Urgent message" selector consistent, use "Bring!" as name

- Replace one occurrence of "bring" with the brand name "Bring!"
- Change description of action to third-person singular for consistency in Home Assistant
- Make all occurrences of the selector "Urgent message" consistent (in sentence case) so they all get consistent translations, too
- Change one related error message to refer to the UI name of the required "Article" field

* Changed ` to '  to avoid Regex problems

* Reverted change to notify_missing_argument_item

Reverted to avoid failing test

* Reverted change to "bring"

* Add "is" to description of "Article"

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-01-20 15:57:42 +00:00
Joost Lekkerkerker 1d22fa9b45 Actually use translated entity names in Lametric (#135381) 2025-01-20 15:57:38 +00:00
Quentame 5356ffa539 Bump Freebox to 1.2.2 (#135313) 2025-01-20 15:57:35 +00:00
epenet 0660eae6f4 Fix missing comma in ollama MODEL_NAMES (#135262) 2025-01-20 15:57:32 +00:00
adam-the-hero 56f54cdccf Fix Watergate Power supply mode description and MQTT/Wifi uptimes (#135085) 2025-01-20 15:57:28 +00:00
Brett Adams 48c23c2e79 Bump pyaussiebb to 0.1.5 (#134943)
Bump
2025-01-20 15:57:25 +00:00
Renier Moorcroft 93c5915faa Image entity key error when camera is ignored in EZVIZ (#134343) 2025-01-20 15:57:22 +00:00
dcmeglio 8865fc0c33 Gracefully handle webhook unsubscription if error occurs while contacting Withings (#134271) 2025-01-20 15:57:19 +00:00
Matthew FitzGerald-Chamberlain 9680abf51e Aprilaire - Fix humidifier showing when it is not available (#133984) 2025-01-20 15:57:15 +00:00
Konrad Vité c687a6f669 Fix DiscoveryFlowHandler when discovery_function returns bool (#133563)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-01-20 15:57:02 +00:00
Bram Kragten bceccd85ee 2025.1.2 (#135241) 2025-01-09 23:25:42 +01:00
Bram Kragten 0027d907a4 Bump version to 2025.1.2 2025-01-09 22:25:42 +01:00
Bram Kragten 5d201406cb Update frontend to 20250109.0 (#135235) 2025-01-09 22:24:43 +01:00
Brynley McDonald 30924b561a Fix Flick Electric Pricing (#135154) 2025-01-09 22:24:42 +01:00
jb101010-2 1eddb4a21b Bump pysuezV2 to 2.0.3 (#135080) 2025-01-09 22:24:41 +01:00
Erik Montnemery 42cdd25d90 Add jitter to backup start time to avoid thundering herd (#135065) 2025-01-09 22:24:41 +01:00
Bram Kragten b8b7daff5a 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-09 22:23:53 +01:00
Cyrill Raccaud 7f3f550b7b Bump cookidoo-api to 0.12.2 (#135045)
fix cookidoo .co.uk countries and group api endpoint
2025-01-09 22:14:35 +01:00
Thomas55555 3c14e2f0a8 Bump aioautomower to 2025.1.0 (#135039) 2025-01-09 22:14:34 +01:00
starkillerOG 9601455d9f Fix channel retrieval for Reolink DUO V1 connected to a NVR (#135035)
fix channel retrieval for DUO V1 connected to a NVR
2025-01-09 22:14:33 +01:00
Mick Vleeshouwer 902bd57b4b Catch errors in automation (instead of raise unexpected error) in Overkiz (#135026)
Catch errors in automation (instead of raise unexpected error)
2025-01-09 22:14:32 +01:00
puddly ab071d1c1b Fix ZHA "referencing a non existing via_device" warning (#135008) 2025-01-09 22:14:31 +01:00
Joakim Sørensen 2c02eefa11 Increase cloud backup download timeout (#134961)
Increese download timeout
2025-01-09 22:14:31 +01:00
Quentame 44808c02f9 Fix Météo-France setup in non French cities (because of failed next rain sensor) (#134782) 2025-01-09 22:14:30 +01:00
Franck Nijhof d59a91a905 2025.1.1 (#134940) 2025-01-07 08:43:32 +01:00
Franck Nijhof 298f059488 Revert "Remove deprecated supported features warning in ..." (multiple) (#134933) 2025-01-07 06:53:14 +00:00
Franck Nijhof 7a5525951d Bump version to 2025.1.1 2025-01-06 23:42:21 +00:00
Artur Pragacz 9a9514d53b Revert "Remove deprecated supported features warning in LightEntity" (#134927) 2025-01-06 23:42:00 +00:00
G Johansson 5337ab2e72 Bump holidays to 0.64 (#134922) 2025-01-06 23:41:55 +00:00
Klaas Schoute b815899fdc Bump powerfox to v1.2.0 (#134908) 2025-01-06 23:41:51 +00:00
Klaas Schoute 81a669c163 Bump powerfox to v1.1.0 (#134730) 2025-01-06 23:41:45 +00:00
Bram Kragten 188def51c6 Update frontend to 20250106.0 (#134905) 2025-01-06 23:40:07 +00:00
Manu eb345971b4 Fix wrong power limit decimal place in IronOS (#134902) 2025-01-06 23:40:03 +00:00
Manu 9288dce7ed Add bring_api to loggers in Bring integration (#134897)
Add bring-api to loggers
2025-01-06 23:39:59 +00:00
Steven B. 4867d3a187 Bump python-kasa to 0.9.1 (#134893)
Bump tplink python-kasa dependency to 0.9.1
2025-01-06 23:39:55 +00:00
Norbert Rittel c40771ba6a Use uppercase for "ID" and sentence-case for "name" / "icon" (#134890) 2025-01-06 23:39:51 +00:00
Luke Lashley 2fc489d17d Add extra failure exceptions during roborock setup (#134889)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-01-06 23:39:47 +00:00
Robin Wohlers-Reichel 279785b22e Bump solax to 3.2.3 (#134876) 2025-01-06 23:39:42 +00:00
Joakim Sørensen e5c986171b Log cloud backup upload response status (#134871)
Log the status of the upload response
2025-01-06 23:39:38 +00:00
Joakim Sørensen 58805f721c 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 23:39:33 +00:00
Allen Porter 29989e9034 Update Roborock config flow message when an account is already configured (#134854) 2025-01-06 23:39:28 +00:00
Avi Miller fbd031a03d Bump aiolifx-themes to update colors (#134846) 2025-01-06 23:39:23 +00:00
J. Diego Rodríguez Royo fe1ce39831 Fix how function arguments are passed on actions at Home Connect (#134845) 2025-01-06 23:39:19 +00:00
J. Nick Koston 914c6459dc Bump habluetooth to 3.7.0 (#134833) 2025-01-06 23:39:14 +00:00
Raphael Hehl 43ffdd0eef Bump uiprotect to version 7.4.1 (#134829) 2025-01-06 23:39:10 +00:00
Norbert Rittel 39d16ed5ce Fix a few typos or grammar issues in asus_wrt (#134813) 2025-01-06 23:39:06 +00:00
Norbert Rittel 07f3d939e3 Replace "id" with "ID" for consistency across HA (#134798) 2025-01-06 23:39:01 +00:00
G Johansson eda60073ee Raise ImportError in python_script (#134792) 2025-01-06 23:38:57 +00:00
Norbert Rittel 09ffa38ddf Fix missing sentence-casing etc. in several strings (#134775) 2025-01-06 23:38:53 +00:00
jb101010-2 b32a791ea4 Bump pysuezV2 to 2.0.1 (#134769) 2025-01-06 23:38:48 +00:00
Michael a4ea25631a 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-06 23:38:44 +00:00
Duco Sebel bd8ea646a9 Bumb python-homewizard-energy to 7.0.1 (#134753) 2025-01-06 23:38:38 +00:00
Norbert Rittel 538a2ea057 Fix swapped letter order in "°F" and "°C" temperature units (#134750)
Fixes the wrong order "F°" and "C°" for the temperature units.
2025-01-06 23:38:34 +00:00
Sid b461bc2fb5 Bump openwebifpy to 4.3.1 (#134746) 2025-01-06 23:38:29 +00:00
TheJulianJES 103960e0a7 Bump ZHA to 0.0.45 (#134726) 2025-01-06 23:37:24 +00:00
dontinelli 1c4273ce91 Change from host to ip in zeroconf discovery for slide_local (#134709) 2025-01-06 23:34:17 +00:00
J. Diego Rodríguez Royo 0f0209d4bb Iterate over a copy of the list of programs at Home Connect select setup entry (#134684) 2025-01-06 23:34:13 +00:00
Cyrill Raccaud 27b8b8458b Cookidoo exotic domains (#134676) 2025-01-06 23:34:08 +00:00
Franck Nijhof c022d91baa Update demetriek to 1.1.1 (#134663) 2025-01-06 23:34:02 +00:00
Cyrill Raccaud 0daac09008 Bump cookidoo-api library to 0.11.1 of for Cookidoo (#134661) 2025-01-06 23:33:56 +00:00
Franck Nijhof ca8416fe50 Update peblar to 0.3.3 (#134658) 2025-01-06 23:33:50 +00:00
starkillerOG a14f6faaaf Fix Reolink playback of recodings (#134652) 2025-01-06 23:33:45 +00:00
Franck Nijhof a9a14381d3 Update twentemilieu to 2.2.1 (#134651) 2025-01-06 23:33:39 +00:00
Joost Lekkerkerker a4d0794fe4 Remove call to remove slide (#134647) 2025-01-06 23:33:33 +00:00
Cyrill Raccaud 9ead6fe362 Set logging in manifest for Cookidoo (#134645) 2025-01-06 23:33:28 +00:00
epenet 017679abe1 Fix hive color tunable light (#134628) 2025-01-06 23:33:23 +00:00
Brynley McDonald 0bd7b793fe Fix Flick Electric authentication (#134611) 2025-01-06 23:33:19 +00:00
Teemu R. c46a70fdcf Mention case-sensitivity in tplink credentials prompt (#134606) 2025-01-06 23:33:13 +00:00
Raphael Hehl 8c2ec5e7c8 Bump uiprotect to version 7.2.0 (#134587) 2025-01-06 23:33:09 +00:00
J. Nick Koston 3063f0b565 Bump bleak-esphome to 2.0.0 (#134580) 2025-01-06 23:33:04 +00:00
peteS-UK aafc1ff074 Small fix to allow playing of expandable favorites on Squeezebox (#134572) 2025-01-06 23:33:00 +00:00
Ludovic BOUÉ 45142b0cc0 Matter Battery replacement icon (#134460) 2025-01-06 23:32:54 +00:00
Franck Nijhof a412acec0e 2025.1.0 (#134529)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Teemu R. <tpr@iki.fi>
Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: OzGav <gavnosp@hotmail.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
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: Sander Hoentjen <sander@hoentjen.eu>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Richard Kroegel <42204099+rikroe@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Álvaro Fernández Rojas <noltari@gmail.com>
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Matthias Alphart <farmio@alphart.net>
Co-authored-by: Tom <CoMPaTech@users.noreply.github.com>
Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>
Co-authored-by: Glenn Vandeuren (aka Iondependent) <vandeurenglenn@gmail.com>
Co-authored-by: Austin Mroczek <austin@mroczek.org>
Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
Co-authored-by: PierreAronnax <pierre@trionax.com>
Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com>
Co-authored-by: jesperraemaekers <146726232+jesperraemaekers@users.noreply.github.com>
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: Barry vd. Heuvel <barry@fruitcake.nl>
Co-authored-by: Raphael Hehl <7577984+RaHehl@users.noreply.github.com>
Co-authored-by: Andre Lengwenus <alengwenus@gmail.com>
Co-authored-by: dontinelli <73341522+dontinelli@users.noreply.github.com>
Co-authored-by: Noah Husby <32528627+noahhusby@users.noreply.github.com>
Co-authored-by: Lucas Gasenzer <lucasgasenzer@mac.com>
Co-authored-by: jb101010-2 <168106462+jb101010-2@users.noreply.github.com>
Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: Martin Weinelt <mweinelt@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: jon6fingrs <53415122+jon6fingrs@users.noreply.github.com>
Co-authored-by: mrtlhfr <10065880+mrtlhfr@users.noreply.github.com>
Co-authored-by: Matrix <justin@yosmart.com>
Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com>
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Omni Flux <omni.hyper.flux@gmail.com>
Co-authored-by: starkillerOG <starkiller.og@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com>
Co-authored-by: karwosts <32912880+karwosts@users.noreply.github.com>
Co-authored-by: Jordi <Jordi1990@users.noreply.github.com>
Co-authored-by: Martin Mrazik <mmrazik@users.noreply.github.com>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: G-Two <7310260+G-Two@users.noreply.github.com>
Co-authored-by: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com>
Co-authored-by: Khole <29937485+KJonline@users.noreply.github.com>
Co-authored-by: Philipp Danner <philipp@danner-web.de>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Cyrill Raccaud <miaucl@users.noreply.github.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Aaron Bach <bachya1208@gmail.com>
Co-authored-by: Michael Hansen <mike@rhasspy.org>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Paul Daumlechner <paul.daumlechner@live.de>
Co-authored-by: Adam Goode <agoode@google.com>
Co-authored-by: Alberto Geniola <albertogeniola@users.noreply.github.com>
Co-authored-by: tronikos <tronikos@users.noreply.github.com>
Co-authored-by: Arne Keller <arne.keller@posteo.de>
Co-authored-by: Andrew Jackson <andrew@codechimp.org>
Co-authored-by: Brynley McDonald <brynley+github@zephire.nz>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
Co-authored-by: Niels Mündler <niels.muendler@inf.ethz.ch>
Co-authored-by: Craig Andrews <candrews@integralblue.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Matthew FitzGerald-Chamberlain <mattfitzgeraldchamberlain@proton.me>
Co-authored-by: Adam Štrauch <cx@initd.cz>
Co-authored-by: cdnninja <jaydenaphillips@gmail.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Kenny Root <kenny@the-b.org>
Co-authored-by: Krzysztof Dąbrowski <krzysdabro@live.com>
Co-authored-by: Andrea Arcangeli <aagit@users.noreply.github.com>
Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
Fix section translations check (#133683)
Fix test coverage in workday (#133616)
Fix spelling of "Gateway PIN" and remove two excessive spaces (#133716)
Fix Peblar current limit user setting value (#133753)
Fix binary_sensor typing in Overkiz (#133782)
Fix errors in HitachiDHW in Overkiz (#133765)
Fix typo in ElevenLabs (#133819)
fixture from LCN tests (#133821)
fix yesterday sensor extra_state invalid typing (#133425)
Fix TypeError in maxcube climate action inference logic (#133853)
Fix tplink camera entity unique id (#133880)
Fix a history stats bug when window and tracked state change simultaneously (#133770)
fixes #133904
Fix duplicate call to async_register_preload_platform (#133909)
Fix missing % in string for generic camera (#133925)
Fix Peblar import in data coordinator (#133926)
Fix reload modbus component issue (#133820)
Fix error when device goes offline (#133848)
fix "Slow" response leads to "Could not find a charging station" #124129 (#133889)
fix #124129
Fix swiss public transport line field none (#133964)
fix #133116
Fix Nord Pool empty response (#134033)
Fix KNX config flow translations and add data descriptions (#134078)
Fix Wake on LAN Port input as Box instead of Slider (#134216)
Fix duplicate sensor disk entities in Systemmonitor (#134139)
Fix Onkyo volume rounding (#134157)
Fix 400 This voice does not support speaking rate or pitch parameters at this time for Google Cloud Journey voices (#134255)
Fix SQL sensor name (#134414)
Fix a few small typos in peblar (#134481)
Fix input_datetime.set_datetime not accepting 0 timestamp value (#134489)
Fix backup dir not existing (#134506)
Fix activating backup retention config on startup (#134523)
fix generic component tests (#134569)
2025-01-03 19:19:01 +01:00
Franck Nijhof ac4bd32137 Bump version to 2025.1.0 2025-01-03 17:31:21 +00:00
Abílio Costa 7e1e63374f Bump whirlpool-sixth-sense to 0.18.11 (#134562) 2025-01-03 17:31:05 +00:00
Robert Resch 03fd6a901b Cherry pick single file from #134020 to fix generic component tests (#134569) 2025-01-03 18:24:46 +01:00
Franck Nijhof 46b2830699 Bump version to 2025.1.0b9 2025-01-03 15:41:14 +00:00
Bram Kragten b416ae1387 Update frontend to 20250103.0 (#134561) 2025-01-03 15:41:06 +00:00
Erik Montnemery 962b880146 Log cloud backup agent file list (#134556) 2025-01-03 15:41:03 +00:00
Erik Montnemery 9c98125d20 Avoid early COMPLETED event when restoring backup (#134546) 2025-01-03 15:41:00 +00:00
Joost Lekkerkerker c9f1fee6bb Set Ituran to silver (#134538) 2025-01-03 15:40:57 +00:00
Erik Montnemery 9b8ed9643f Add backup as after_dependency of frontend (#134534) 2025-01-03 15:40:54 +00:00
Erik Montnemery 7ea7178aa9 Simplify error handling when creating backup (#134528) 2025-01-03 15:40:51 +00:00
starkillerOG c5746291cc Add Reolink proxy for playback (#133916) 2025-01-03 15:40:46 +00:00
Franck Nijhof 1af384bc0a Bump version to 2025.1.0b8 2025-01-03 09:56:51 +00:00
Franck Nijhof ea82c1b73e Only load Peblar customization update entity when present (#134526) 2025-01-03 09:56:39 +00:00
Franck Nijhof 96936f5f4a Update peblar to v0.3.2 (#134524) 2025-01-03 09:56:36 +00:00
Erik Montnemery 316f93f208 Fix activating backup retention config on startup (#134523) 2025-01-03 09:56:33 +00:00
Robert Svensson f719a14537 Handle deCONZ color temp 0 is never used when calculating kelvin CT (#134521) 2025-01-03 09:56:30 +00:00
Erik Montnemery a830a14342 Improve recorder schema migration error test (#134518) 2025-01-03 09:56:27 +00:00
Erik Montnemery 1b67d51e24 Add error prints for recorder fatal errors (#134517) 2025-01-03 09:56:23 +00:00
Paulus Schoutsen e1f6475623 Fix backup dir not existing (#134506) 2025-01-03 09:56:20 +00:00
Josef Zweck 59a3fe857b Bump aioacaia to 0.1.13 (#134496) 2025-01-03 09:56:17 +00:00
Franck Nijhof f364e29148 Fix input_datetime.set_datetime not accepting 0 timestamp value (#134489) 2025-01-03 09:56:13 +00:00
Franck Nijhof 47190e4ac1 Bump version to 2025.1.0b7 2025-01-02 22:23:54 +00:00
Franck Nijhof 7fa1983da0 Update peblar to 0.3.1 (#134486) 2025-01-02 22:21:44 +00:00
Norbert Rittel 9b906e94c7 Fix a few small typos in peblar (#134481) 2025-01-02 22:21:16 +00:00
Robert Resch 5ac4d5bef7 Bump deebot-client to 10.1.0 (#134470) 2025-01-02 21:36:44 +00:00
Erik Montnemery 995e222959 Don't start recorder if a database from the future is used (#134467) 2025-01-02 21:36:41 +00:00
Duco Sebel 61ac8e7e8c Include host in Peblar EV-Charger discovery setup description (#133954)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-01-02 21:36:38 +00:00
Andrea Arcangeli 67ec71031d open_meteo: correct UTC timezone handling in hourly forecast (#129664)
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-01-02 21:36:33 +00:00
186 changed files with 4536 additions and 675 deletions
+1 -1
View File
@@ -26,5 +26,5 @@
"iot_class": "local_push",
"loggers": ["aioacaia"],
"quality_scale": "platinum",
"requirements": ["aioacaia==0.1.12"]
"requirements": ["aioacaia==0.1.13"]
}
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.7"]
"requirements": ["aioairzone==0.9.9"]
}
@@ -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"
}
}
@@ -44,7 +44,10 @@ class APCUPSdData(dict[str, str]):
@property
def serial_no(self) -> str | None:
"""Return the unique serial number of the UPS, if available."""
return self.get("SERIALNO")
sn = self.get("SERIALNO")
# We had user reports that some UPS models simply return "Blank" as serial number, in
# which case we fall back to `None` to indicate that it is actually not available.
return None if sn == "Blank" else sn
class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
@@ -50,7 +50,7 @@ async def async_setup_entry(
descriptions: list[AprilaireHumidifierDescription] = []
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2):
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (1, 2):
descriptions.append(
AprilaireHumidifierDescription(
key="humidifier",
@@ -67,7 +67,7 @@ async def async_setup_entry(
)
)
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1):
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) == 1:
descriptions.append(
AprilaireHumidifierDescription(
key="dehumidifier",
@@ -1017,9 +1017,18 @@ class PipelineRun:
raise RuntimeError("Recognize intent was not prepared")
if self.pipeline.conversation_language == MATCH_ALL:
# LLMs support all languages ('*') so use pipeline language for
# intent fallback.
input_language = self.pipeline.language
# LLMs support all languages ('*') so use languages from the
# pipeline for intent fallback.
#
# We prioritize the STT and TTS languages because they may be more
# specific, such as "zh-CN" instead of just "zh". This is necessary
# for languages whose intents are split out by region when
# preferring local intent matching.
input_language = (
self.pipeline.stt_language
or self.pipeline.tts_language
or self.pipeline.language
)
else:
input_language = self.pipeline.conversation_language
@@ -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)"
}
}
@@ -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"]
}
+13
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
@@ -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:
@@ -323,6 +334,8 @@ class BackupSchedule:
except Exception: # noqa: BLE001
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
)
+15 -25
View File
@@ -435,6 +435,7 @@ class BackupManager:
# 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
continue
if isinstance(result, Exception):
@@ -800,12 +801,10 @@ class BackupManager:
"""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:
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
if with_automatic_settings:
self._update_issue_backup_failed()
@@ -831,33 +830,15 @@ class BackupManager:
agent_ids=agent_ids,
open_stream=written_backup.open_stream,
)
except BaseException:
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
raise # manager or unexpected error
finally:
try:
await written_backup.release_stream()
except Exception:
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
raise
await written_backup.release_stream()
self.known_backups.add(written_backup.backup, agent_errors)
if agent_errors:
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
)
else:
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()
self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED)
)
backup_success = True
if with_automatic_settings:
self._update_issue_after_agent_upload(agent_errors)
@@ -868,6 +849,14 @@ class BackupManager:
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(
@@ -1294,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
@@ -1386,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:
@@ -20,6 +20,6 @@
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.20.0",
"dbus-fast==2.24.3",
"habluetooth==3.6.0"
"habluetooth==3.7.0"
]
}
@@ -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"]
}
+4 -4
View File
@@ -111,7 +111,7 @@
"services": {
"send_message": {
"name": "[%key:component::notify::services::notify::name%]",
"description": "Send a mobile push notification to members of a shared Bring! list.",
"description": "Sends a mobile push notification to members of a shared Bring! list.",
"fields": {
"entity_id": {
"name": "List",
@@ -122,8 +122,8 @@
"description": "Type of push notification to send to list members."
},
"item": {
"name": "Article (Required if message type `Urgent Message` selected)",
"description": "Article name to include in an urgent message e.g. `Urgent Message - Please buy Cilantro urgently`"
"name": "Article (Required if notification type `Urgent message` is selected)",
"description": "Article name to include in an urgent message e.g. `Urgent message - Please buy Cilantro urgently`"
}
}
}
@@ -134,7 +134,7 @@
"going_shopping": "I'm going shopping! - Last chance to make changes",
"changed_list": "List updated - Take a look at the articles",
"shopping_done": "Shopping done - The fridge is well stocked",
"urgent_message": "Urgent Message - Please buy `Article name` urgently"
"urgent_message": "Urgent message - Please buy `Article` urgently"
}
}
}
@@ -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%]"
+21 -5
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()
+78 -23
View File
@@ -2,9 +2,12 @@
from __future__ import annotations
import asyncio
import base64
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
import hashlib
import logging
import random
from typing import Any, Self
from aiohttp import ClientError, ClientTimeout, StreamReader
@@ -23,7 +26,11 @@ 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:
@@ -136,13 +143,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 +208,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 +262,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
@@ -2,7 +2,7 @@
from __future__ import annotations
from cookidoo_api import Cookidoo, CookidooConfig, CookidooLocalizationConfig
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
from homeassistant.const import (
CONF_COUNTRY,
@@ -22,15 +22,17 @@ PLATFORMS: list[Platform] = [Platform.TODO]
async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool:
"""Set up Cookidoo from a config entry."""
localizations = await get_localization_options(
country=entry.data[CONF_COUNTRY].lower(),
language=entry.data[CONF_LANGUAGE],
)
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],
),
localization=localizations[0],
),
)
@@ -10,7 +10,6 @@ from cookidoo_api import (
Cookidoo,
CookidooAuthException,
CookidooConfig,
CookidooLocalizationConfig,
CookidooRequestException,
get_country_options,
get_localization_options,
@@ -219,18 +218,19 @@ 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
localizations = await get_localization_options(
country=data_input[CONF_COUNTRY].lower(),
language=data_input[CONF_LANGUAGE],
)
session = async_get_clientsession(self.hass)
cookidoo = Cookidoo(
session,
async_get_clientsession(self.hass),
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],
),
localization=localizations[0],
),
)
try:
@@ -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"]
}
@@ -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 = (
+1 -1
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)
@@ -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==11.0.0"]
}
+1 -1
View File
@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"loggers": ["elkm1_lib"],
"requirements": ["elkm1-lib==2.2.10"]
"requirements": ["elkm1-lib==2.2.11"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.3.0"]
"requirements": ["openwebifpy==4.3.1"]
}
@@ -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"]
}
@@ -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
@@ -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.
+1 -3
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 (
@@ -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."]
}
+4 -2
View File
@@ -8,7 +8,7 @@ from pyezviz.exceptions import PyEzvizError
from pyezviz.utils import decrypt_image
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -57,7 +57,9 @@ class EzvizLastMotion(EzvizEntity, ImageEntity):
)
camera = hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, serial)
self.alarm_image_password = (
camera.data[CONF_PASSWORD] if camera is not None else None
camera.data[CONF_PASSWORD]
if camera and camera.source != SOURCE_IGNORE
else None
)
async def _async_load_image_from_url(self, url: str) -> Image | None:
@@ -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
@@ -93,16 +94,22 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return True
class HassFlickAuth(AbstractFlickAuth):
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
@@ -119,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)
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyflick"],
"requirements": ["PyFlick==1.1.2"]
"requirements": ["PyFlick==1.1.3"]
}
@@ -51,19 +51,19 @@ class FlickPricingSensor(CoordinatorEntity[FlickElectricDataCoordinator], Sensor
_LOGGER.warning(
"Unexpected quantity for unit price: %s", self.coordinator.data
)
return self.coordinator.data.cost
return self.coordinator.data.cost * 100
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
components: dict[str, Decimal] = {}
components: dict[str, float] = {}
for component in self.coordinator.data.components:
if component.charge_setter not in ATTR_COMPONENTS:
_LOGGER.warning("Found unknown component: %s", component.charge_setter)
continue
components[component.charge_setter] = component.value
components[component.charge_setter] = float(component.value * 100)
return {
ATTR_START_AT: self.coordinator.data.start_at,
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/freebox",
"iot_class": "local_polling",
"loggers": ["freebox_api"],
"requirements": ["freebox-api==1.2.1"],
"requirements": ["freebox-api==1.2.2"],
"zeroconf": ["_fbx-api._tcp.local."]
}
@@ -214,6 +214,18 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self._options = options
await self.hass.async_add_executor_job(self.setup)
device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
configuration_url=f"http://{self.host}",
connections={(dr.CONNECTION_NETWORK_MAC, self.mac)},
identifiers={(DOMAIN, self.unique_id)},
manufacturer="AVM",
model=self.model,
name=self.config_entry.title,
sw_version=self.current_firmware,
)
def setup(self) -> None:
"""Set up FritzboxTools class."""
+1 -10
View File
@@ -68,23 +68,14 @@ class FritzBoxBaseEntity:
"""Init device info class."""
self._avm_wrapper = avm_wrapper
self._device_name = device_name
@property
def mac_address(self) -> str:
"""Return the mac address of the main device."""
return self._avm_wrapper.mac
self.mac_address = self._avm_wrapper.mac
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
configuration_url=f"http://{self._avm_wrapper.host}",
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
identifiers={(DOMAIN, self._avm_wrapper.unique_id)},
manufacturer="AVM",
model=self._avm_wrapper.model,
name=self._device_name,
sw_version=self._avm_wrapper.current_firmware,
)
@@ -1,6 +1,7 @@
{
"domain": "frontend",
"name": "Home Assistant Frontend",
"after_dependencies": ["backup"],
"codeowners": ["@home-assistant/frontend"],
"dependencies": [
"api",
@@ -20,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250102.0"]
"requirements": ["home-assistant-frontend==20250109.2"]
}
+6 -5
View File
@@ -227,11 +227,12 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
include_addons_set = supervisor_backups.AddonSet.ALL
elif include_addons:
include_addons_set = set(include_addons)
include_folders_set = (
{supervisor_backups.Folder(folder) for folder in include_folders}
if include_folders
else None
)
include_folders_set = {
supervisor_backups.Folder(folder) for folder in include_folders or []
}
# Always include SSL if Home Assistant is included
if include_homeassistant:
include_folders_set.add(supervisor_backups.Folder.SSL)
hassio_agents: list[SupervisorBackupAgent] = [
cast(SupervisorBackupAgent, manager.backup_agents[agent_id])
+1
View File
@@ -114,6 +114,7 @@ class HiveDeviceLight(HiveEntity, LightEntity):
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
self._attr_color_mode = ColorMode.HS
else:
color_temp = self.device["status"].get("color_temp")
self._attr_color_temp_kelvin = (
None
if color_temp is None
+1 -1
View File
@@ -9,5 +9,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"],
"requirements": ["pyhiveapi==0.5.16"]
"requirements": ["pyhive-integration==1.0.1"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.63", "babel==2.15.0"]
"requirements": ["holidays==0.65", "babel==2.15.0"]
}
@@ -168,7 +168,7 @@ async def _run_appliance_service[*_Ts](
error_translation_placeholders: dict[str, str],
) -> None:
try:
await hass.async_add_executor_job(getattr(appliance, method), args)
await hass.async_add_executor_job(getattr(appliance, method), *args)
except api.HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -220,7 +220,7 @@ async def async_setup_entry(
with contextlib.suppress(HomeConnectError):
programs = device.appliance.get_programs_available()
if programs:
for program in programs:
for program in programs.copy():
if program not in PROGRAMS_TRANSLATION_KEYS_MAP:
programs.remove(program)
if program not in programs_not_found:
@@ -52,6 +52,7 @@ from .const import (
PROP_MIN_VALUE,
SERV_LIGHTBULB,
)
from .util import get_min_max
_LOGGER = logging.getLogger(__name__)
@@ -120,12 +121,14 @@ class Light(HomeAccessory):
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if CHAR_COLOR_TEMPERATURE in self.chars:
self.min_mireds = color_temperature_kelvin_to_mired(
min_mireds = color_temperature_kelvin_to_mired(
attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP)
)
self.max_mireds = color_temperature_kelvin_to_mired(
max_mireds = color_temperature_kelvin_to_mired(
attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP)
)
# Ensure min is less than max
self.min_mireds, self.max_mireds = get_min_max(min_mireds, max_mireds)
if not self.color_temp_supported and not self.rgbww_supported:
self.max_mireds = self.min_mireds
self.char_color_temp = serv_light.configure_char(
@@ -282,7 +285,11 @@ class Light(HomeAccessory):
hue, saturation = color_temperature_to_hs(color_temp)
elif color_mode == ColorMode.WHITE:
hue, saturation = 0, 0
elif hue_sat := attributes.get(ATTR_HS_COLOR):
elif (
(hue_sat := attributes.get(ATTR_HS_COLOR))
and isinstance(hue_sat, (list, tuple))
and len(hue_sat) == 2
):
hue, saturation = hue_sat
else:
hue = None
@@ -14,6 +14,7 @@ from homeassistant.components.climate import (
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_HUMIDITY,
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MIN_TEMP,
@@ -21,6 +22,7 @@ from homeassistant.components.climate import (
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_HUMIDITY,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_HUMIDITY,
DEFAULT_MIN_TEMP,
@@ -90,7 +92,7 @@ from .const import (
SERV_FANV2,
SERV_THERMOSTAT,
)
from .util import temperature_to_homekit, temperature_to_states
from .util import get_min_max, temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
@@ -208,7 +210,10 @@ class Thermostat(HomeAccessory):
self.fan_chars: list[str] = []
attributes = state.attributes
min_humidity = attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)
min_humidity, _ = get_min_max(
attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY),
attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY),
)
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
@@ -839,6 +844,9 @@ def _get_temperature_range_from_state(
else:
max_temp = default_max
# Handle reversed temperature range
min_temp, max_temp = get_min_max(min_temp, max_temp)
# Homekit only supports 10-38, overwriting
# the max to appears to work, but less than 0 causes
# a crash on the home app
+11
View File
@@ -655,3 +655,14 @@ def state_changed_event_is_same_state(event: Event[EventStateChangedData]) -> bo
old_state = event_data["old_state"]
new_state = event_data["new_state"]
return bool(new_state and old_state and new_state.state == old_state.state)
def get_min_max(value1: float, value2: float) -> tuple[float, float]:
"""Return the minimum and maximum of two values.
HomeKit will go unavailable if the min and max are reversed
so we make sure the min is always the min and the max is always the max
as any mistakes made in integrations will cause the entire
bridge to go unavailable.
"""
return min(value1, value2), max(value1, value2)
@@ -12,6 +12,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==v7.0.0"],
"requirements": ["python-homewizard-energy==v7.0.1"],
"zeroconf": ["_hwenergy._tcp.local."]
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2024.12.0"]
"requirements": ["aioautomower==2025.1.0"]
}
@@ -68,7 +68,7 @@ ZONE_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
)
SCHEMA_START_WATERING: VolDictType = {
vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=90)),
vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=1440)),
}
SCHEMA_SUSPEND: VolDictType = {
vol.Required("until"): cv.datetime,
@@ -10,7 +10,7 @@ start_watering:
selector:
number:
min: 0
max: 90
max: 1440
unit_of_measurement: min
mode: box
suspend:
@@ -385,7 +385,7 @@ class InputDatetime(collection.CollectionEntity, RestoreEntity):
@callback
def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None):
"""Set a new date / time."""
if timestamp:
if timestamp is not None:
datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp))
if datetime:
+2 -2
View File
@@ -188,8 +188,8 @@ PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
characteristic=CharSetting.POWER_LIMIT,
mode=NumberMode.BOX,
native_min_value=0,
native_max_value=12,
native_step=0.1,
native_max_value=120,
native_step=5,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfPower.WATT,
entity_registry_enabled_default=False,
@@ -128,8 +128,8 @@
"temp_unit": {
"name": "Temperature display unit",
"state": {
"celsius": "Celsius (C°)",
"fahrenheit": "Fahrenheit (F°)"
"celsius": "Celsius (°C)",
"fahrenheit": "Fahrenheit (°F)"
}
},
"desc_scroll_speed": {
@@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/ituran",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["pyituran==0.1.4"]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"requirements": [
"xknx==3.4.0",
"xknxproject==3.8.1",
"knx-frontend==2024.12.26.233449"
"knx-frontend==2025.1.18.164225"
],
"single_config_entry": true
}
@@ -13,7 +13,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["demetriek"],
"requirements": ["demetriek==1.1.0"],
"requirements": ["demetriek==1.2.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
+20 -8
View File
@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from demetriek import Device, LaMetricDevice
from demetriek import Device, LaMetricDevice, Range
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
@@ -25,6 +25,7 @@ class LaMetricNumberEntityDescription(NumberEntityDescription):
"""Class describing LaMetric number entities."""
value_fn: Callable[[Device], int | None]
range_fn: Callable[[Device], Range | None]
has_fn: Callable[[Device], bool] = lambda device: True
set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]]
@@ -33,11 +34,9 @@ NUMBERS = [
LaMetricNumberEntityDescription(
key="brightness",
translation_key="brightness",
name="Brightness",
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
native_max_value=100,
range_fn=lambda device: device.display.brightness_limit,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.display.brightness,
set_value_fn=lambda device, bri: device.display(brightness=int(bri)),
@@ -45,12 +44,11 @@ NUMBERS = [
LaMetricNumberEntityDescription(
key="volume",
translation_key="volume",
name="Volume",
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
native_max_value=100,
has_fn=lambda device: bool(device.audio),
range_fn=lambda device: device.audio.volume_range if device.audio else None,
native_unit_of_measurement=PERCENTAGE,
has_fn=lambda device: bool(device.audio and device.audio.available),
value_fn=lambda device: device.audio.volume if device.audio else 0,
set_value_fn=lambda api, volume: api.audio(volume=int(volume)),
),
@@ -93,6 +91,20 @@ class LaMetricNumberEntity(LaMetricEntity, NumberEntity):
"""Return the number value."""
return self.entity_description.value_fn(self.coordinator.data)
@property
def native_min_value(self) -> int:
"""Return the min range."""
if limits := self.entity_description.range_fn(self.coordinator.data):
return limits.range_min
return 0
@property
def native_max_value(self) -> int:
"""Return the max range."""
if limits := self.entity_description.range_fn(self.coordinator.data):
return limits.range_max
return 100
@lametric_exception_handler
async def async_set_native_value(self, value: float) -> None:
"""Change to new number value."""
@@ -66,6 +66,14 @@
"name": "Dismiss all notifications"
}
},
"number": {
"brightness": {
"name": "Brightness"
},
"volume": {
"name": "Volume"
}
},
"sensor": {
"rssi": {
"name": "Wi-Fi signal"
+1 -1
View File
@@ -53,6 +53,6 @@
"requirements": [
"aiolifx==1.1.2",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.5.5"
"aiolifx-themes==0.6.0"
]
}
+76 -5
View File
@@ -354,7 +354,7 @@ def filter_turn_off_params(
if not params:
return params
supported_features = light.supported_features
supported_features = light.supported_features_compat
if LightEntityFeature.FLASH not in supported_features:
params.pop(ATTR_FLASH, None)
@@ -366,7 +366,7 @@ def filter_turn_off_params(
def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]:
"""Filter out params not supported by the light."""
supported_features = light.supported_features
supported_features = light.supported_features_compat
if LightEntityFeature.EFFECT not in supported_features:
params.pop(ATTR_EFFECT, None)
@@ -1093,7 +1093,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
data: dict[str, Any] = {}
supported_features = self.supported_features
supported_features = self.supported_features_compat
supported_color_modes = self._light_internal_supported_color_modes
if ColorMode.COLOR_TEMP in supported_color_modes:
@@ -1255,11 +1255,12 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
data: dict[str, Any] = {}
supported_features = self.supported_features
supported_features = self.supported_features_compat
supported_color_modes = self.supported_color_modes
legacy_supported_color_modes = (
supported_color_modes or self._light_internal_supported_color_modes
)
supported_features_value = supported_features.value
_is_on = self.is_on
color_mode = self._light_internal_color_mode if _is_on else None
@@ -1278,6 +1279,13 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
data[ATTR_BRIGHTNESS] = self.brightness
else:
data[ATTR_BRIGHTNESS] = None
elif supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value:
# Backwards compatibility for ambiguous / incomplete states
# Warning is printed by supported_features_compat, remove in 2025.1
if _is_on:
data[ATTR_BRIGHTNESS] = self.brightness
else:
data[ATTR_BRIGHTNESS] = None
if color_temp_supported(supported_color_modes):
if color_mode == ColorMode.COLOR_TEMP:
@@ -1292,6 +1300,21 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else:
data[ATTR_COLOR_TEMP_KELVIN] = None
data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None
elif supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value:
# Backwards compatibility
# Warning is printed by supported_features_compat, remove in 2025.1
if _is_on:
color_temp_kelvin = self.color_temp_kelvin
data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
if color_temp_kelvin:
data[_DEPRECATED_ATTR_COLOR_TEMP.value] = (
color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)
)
else:
data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None
else:
data[ATTR_COLOR_TEMP_KELVIN] = None
data[_DEPRECATED_ATTR_COLOR_TEMP.value] = None
if color_supported(legacy_supported_color_modes) or color_temp_supported(
legacy_supported_color_modes
@@ -1329,7 +1352,24 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
type(self),
report_issue,
)
return {ColorMode.ONOFF}
supported_features = self.supported_features_compat
supported_features_value = supported_features.value
supported_color_modes: set[ColorMode] = set()
if supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value:
supported_color_modes.add(ColorMode.COLOR_TEMP)
if supported_features_value & _DEPRECATED_SUPPORT_COLOR.value:
supported_color_modes.add(ColorMode.HS)
if (
not supported_color_modes
and supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value
):
supported_color_modes = {ColorMode.BRIGHTNESS}
if not supported_color_modes:
supported_color_modes = {ColorMode.ONOFF}
return supported_color_modes
@cached_property
def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
@@ -1341,6 +1381,37 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Flag supported features."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> LightEntityFeature:
"""Return the supported features as LightEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is not int: # noqa: E721
return features
new_features = LightEntityFeature(features)
if self._deprecated_supported_features_reported is True:
return new_features
self._deprecated_supported_features_reported = True
report_issue = self._suggest_report_issue()
report_issue += (
" and reference "
"https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation"
)
_LOGGER.warning(
(
"Entity %s (%s) is using deprecated supported features"
" values which will be removed in HA Core 2025.1. Instead it should use"
" %s and color modes, please %s"
),
self.entity_id,
type(self),
repr(new_features),
report_issue,
)
return new_features
def __should_report_light_issue(self) -> bool:
"""Return if light color mode issues should be reported."""
if not self.platform:
+7 -1
View File
@@ -44,9 +44,15 @@ class LinkPlayBaseEntity(Entity):
if model != MANUFACTURER_GENERIC:
model_id = bridge.device.properties["project"]
connections: set[tuple[str, str]] = set()
if "MAC" in bridge.device.properties:
connections.add(
(dr.CONNECTION_NETWORK_MAC, bridge.device.properties["MAC"])
)
self._attr_device_info = dr.DeviceInfo(
configuration_url=bridge.endpoint,
connections={(dr.CONNECTION_NETWORK_MAC, bridge.device.properties["MAC"])},
connections=connections,
hw_version=bridge.device.properties["hardware"],
identifiers={(DOMAIN, bridge.device.uuid)},
manufacturer=manufacturer,
@@ -57,6 +57,9 @@
},
"valve_position": {
"default": "mdi:valve"
},
"battery_replacement_description": {
"default": "mdi:battery-sync-outline"
}
}
}
@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2024.12.23"],
"requirements": ["yt-dlp[default]==2025.01.15"],
"single_config_entry": true
}
@@ -773,6 +773,19 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Flag media player features that are supported."""
return self._attr_supported_features
@property
def supported_features_compat(self) -> MediaPlayerEntityFeature:
"""Return the supported features as MediaPlayerEntityFeature.
Remove this compatibility shim in 2025.1 or later.
"""
features = self.supported_features
if type(features) is int: # noqa: E721
new_features = MediaPlayerEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
def turn_on(self) -> None:
"""Turn the media player on."""
raise NotImplementedError
@@ -912,85 +925,87 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def support_play(self) -> bool:
"""Boolean if play is supported."""
return MediaPlayerEntityFeature.PLAY in self.supported_features
return MediaPlayerEntityFeature.PLAY in self.supported_features_compat
@final
@property
def support_pause(self) -> bool:
"""Boolean if pause is supported."""
return MediaPlayerEntityFeature.PAUSE in self.supported_features
return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat
@final
@property
def support_stop(self) -> bool:
"""Boolean if stop is supported."""
return MediaPlayerEntityFeature.STOP in self.supported_features
return MediaPlayerEntityFeature.STOP in self.supported_features_compat
@final
@property
def support_seek(self) -> bool:
"""Boolean if seek is supported."""
return MediaPlayerEntityFeature.SEEK in self.supported_features
return MediaPlayerEntityFeature.SEEK in self.supported_features_compat
@final
@property
def support_volume_set(self) -> bool:
"""Boolean if setting volume is supported."""
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
@final
@property
def support_volume_mute(self) -> bool:
"""Boolean if muting volume is supported."""
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat
@final
@property
def support_previous_track(self) -> bool:
"""Boolean if previous track command supported."""
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat
@final
@property
def support_next_track(self) -> bool:
"""Boolean if next track command supported."""
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat
@final
@property
def support_play_media(self) -> bool:
"""Boolean if play media command supported."""
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat
@final
@property
def support_select_source(self) -> bool:
"""Boolean if select source command supported."""
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat
@final
@property
def support_select_sound_mode(self) -> bool:
"""Boolean if select sound mode command supported."""
return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features
return (
MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat
)
@final
@property
def support_clear_playlist(self) -> bool:
"""Boolean if clear playlist command supported."""
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat
@final
@property
def support_shuffle_set(self) -> bool:
"""Boolean if shuffle is supported."""
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat
@final
@property
def support_grouping(self) -> bool:
"""Boolean if player grouping is supported."""
return MediaPlayerEntityFeature.GROUPING in self.supported_features
return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat
async def async_toggle(self) -> None:
"""Toggle the power on the media player."""
@@ -1019,7 +1034,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if (
self.volume_level is not None
and self.volume_level < 1
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
):
await self.async_set_volume_level(
min(1, self.volume_level + self.volume_step)
@@ -1037,7 +1052,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if (
self.volume_level is not None
and self.volume_level > 0
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
):
await self.async_set_volume_level(
max(0, self.volume_level - self.volume_step)
@@ -1080,7 +1095,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
data: dict[str, Any] = {}
supported_features = self.supported_features
supported_features = self.supported_features_compat
if (
source_list := self.source_list
@@ -1286,7 +1301,7 @@ async def websocket_browse_media(
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
return
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features:
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
connection.send_message(
websocket_api.error_message(
msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"
@@ -6,6 +6,7 @@ import logging
from meteofrance_api.client import MeteoFranceClient
from meteofrance_api.helpers import is_valid_warning_department
from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain
from requests import RequestException
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
@@ -83,7 +84,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_method=_async_update_data_rain,
update_interval=SCAN_INTERVAL_RAIN,
)
await coordinator_rain.async_config_entry_first_refresh()
try:
await coordinator_rain._async_refresh(log_failures=False) # noqa: SLF001
except RequestException:
_LOGGER.warning(
"1 hour rain forecast not available: %s is not in covered zone",
entry.title,
)
department = coordinator_forecast.data.position.get("dept")
_LOGGER.debug(
@@ -128,8 +135,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = {
UNDO_UPDATE_LISTENER: undo_listener,
COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain,
}
if coordinator_rain and coordinator_rain.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_RAIN] = coordinator_rain
if coordinator_alert and coordinator_alert.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
@@ -187,7 +187,7 @@ async def async_setup_entry(
"""Set up the Meteo-France sensor platform."""
data = hass.data[DOMAIN][entry.entry_id]
coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
coordinator_rain: DataUpdateCoordinator[Rain] | None = data.get(COORDINATOR_RAIN)
coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
COORDINATOR_ALERT
)
+4 -1
View File
@@ -79,7 +79,10 @@ class BasePlatform(Entity):
"""Initialize the Modbus binary sensor."""
self._hub = hub
self._slave = entry.get(CONF_SLAVE) or entry.get(CONF_DEVICE_ADDRESS, 0)
if (conf_slave := entry.get(CONF_SLAVE)) is not None:
self._slave = conf_slave
else:
self._slave = entry.get(CONF_DEVICE_ADDRESS, 1)
self._address = int(entry[CONF_ADDRESS])
self._input_type = entry[CONF_INPUT_TYPE]
self._value: str | None = None
+3 -1
View File
@@ -368,7 +368,9 @@ class ModbusHub:
self, slave: int | None, address: int, value: int | list[int], use_call: str
) -> ModbusPDU | None:
"""Call sync. pymodbus."""
kwargs = {"slave": slave} if slave else {}
kwargs: dict[str, Any] = (
{ATTR_SLAVE: slave} if slave is not None else {ATTR_SLAVE: 1}
)
entry = self._pb_request[use_call]
try:
result: ModbusPDU = await entry.func(address, value, **kwargs)
+3 -3
View File
@@ -179,14 +179,14 @@ class MqttNumber(MqttEntity, RestoreNumber):
return
if num_value is not None and (
num_value < self.min_value or num_value > self.max_value
num_value < self.native_min_value or num_value > self.native_max_value
):
_LOGGER.error(
"Invalid value for %s: %s (range %s - %s)",
self.entity_id,
num_value,
self.min_value,
self.max_value,
self.native_min_value,
self.native_max_value,
)
return
+2 -2
View File
@@ -325,10 +325,10 @@ class MyUplinkEnumSensor(MyUplinkDevicePointSensor):
}
@property
def native_value(self) -> str:
def native_value(self) -> str | None:
"""Sensor state value for enum sensor."""
device_point = self.coordinator.data.points[self.device_id][self.point_id]
return self.options_map[str(int(device_point.value))] # type: ignore[no-any-return]
return self.options_map.get(str(int(device_point.value)))
class MyUplinkEnumRawSensor(MyUplinkDevicePointSensor):
@@ -112,7 +112,7 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity):
def turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
self._action.turn_on(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)
self._action.turn_on(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55))
def turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/niko_home_control",
"iot_class": "local_push",
"loggers": ["nikohomecontrol"],
"requirements": ["nhc==0.3.2"]
"requirements": ["nhc==0.3.4"]
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/nmap_tracker",
"iot_class": "local_polling",
"loggers": ["nmap"],
"requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.7"]
"requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.9"]
}
+2 -1
View File
@@ -61,7 +61,8 @@ MODEL_NAMES = [ # https://ollama.com/library
"goliath",
"granite-code",
"granite3-dense",
"granite3-guardian" "granite3-moe",
"granite3-guardian",
"granite3-moe",
"hermes3",
"internlm2",
"llama-guard3",
+16 -10
View File
@@ -263,16 +263,22 @@ class ONVIFDevice:
LOGGER.warning("%s: Could not retrieve date/time on this camera", self.name)
return
cam_date = dt.datetime(
cdate.Date.Year,
cdate.Date.Month,
cdate.Date.Day,
cdate.Time.Hour,
cdate.Time.Minute,
cdate.Time.Second,
0,
tzone,
)
try:
cam_date = dt.datetime(
cdate.Date.Year,
cdate.Date.Month,
cdate.Date.Day,
cdate.Time.Hour,
cdate.Time.Minute,
cdate.Time.Second,
0,
tzone,
)
except ValueError as err:
LOGGER.warning(
"%s: Could not parse date/time from camera: %s", self.name, err
)
return
cam_date_utc = cam_date.astimezone(dt_util.UTC)
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
"requirements": ["onvif-zeep-async==3.1.13", "WSDiscovery==2.0.0"]
"requirements": ["onvif-zeep-async==3.2.3", "WSDiscovery==2.0.0"]
}
@@ -2,6 +2,8 @@
from __future__ import annotations
from datetime import datetime, time
from open_meteo import Forecast as OpenMeteoForecast
from homeassistant.components.weather import (
@@ -107,8 +109,9 @@ class OpenMeteoWeatherEntity(
daily = self.coordinator.data.daily
for index, date in enumerate(self.coordinator.data.daily.time):
_datetime = datetime.combine(date=date, time=time(0), tzinfo=dt_util.UTC)
forecast = Forecast(
datetime=date.isoformat(),
datetime=_datetime.isoformat(),
)
if daily.weathercode is not None:
@@ -155,12 +158,14 @@ class OpenMeteoWeatherEntity(
today = dt_util.utcnow()
hourly = self.coordinator.data.hourly
for index, datetime in enumerate(self.coordinator.data.hourly.time):
if dt_util.as_utc(datetime) < today:
for index, _datetime in enumerate(self.coordinator.data.hourly.time):
if _datetime.tzinfo is None:
_datetime = _datetime.replace(tzinfo=dt_util.UTC)
if _datetime < today:
continue
forecast = Forecast(
datetime=datetime.isoformat(),
datetime=_datetime.isoformat(),
)
if hourly.weather_code is not None:
@@ -119,5 +119,5 @@ class HitachiAirToWaterHeatingZone(OverkizEntity, ClimateEntity):
temperature = cast(float, kwargs.get(ATTR_TEMPERATURE))
await self.executor.async_execute_command(
OverkizCommand.SET_THERMOSTAT_SETTING_CONTROL_ZONE_1, int(temperature)
OverkizCommand.SET_THERMOSTAT_SETTING_CONTROL_ZONE_1, float(temperature)
)
+2 -2
View File
@@ -6,7 +6,7 @@ from typing import Any, cast
from urllib.parse import urlparse
from pyoverkiz.enums import OverkizCommand, Protocol
from pyoverkiz.exceptions import OverkizException
from pyoverkiz.exceptions import BaseOverkizException
from pyoverkiz.models import Command, Device, StateDefinition
from pyoverkiz.types import StateType as OverkizStateType
@@ -105,7 +105,7 @@ class OverkizExecutor:
"Home Assistant",
)
# Catch Overkiz exceptions to support `continue_on_error` functionality
except OverkizException as exception:
except BaseOverkizException as exception:
raise HomeAssistantError(exception) from exception
# ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here
+16 -5
View File
@@ -27,7 +27,7 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
_host: str
_discovery_info: zeroconf.ZeroconfServiceInfo
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -137,8 +137,15 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(sn)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
self._host = discovery_info.host
self.context.update({"configuration_url": f"http://{discovery_info.host}"})
self._discovery_info = discovery_info
self.context.update(
{
"title_placeholders": {
"name": discovery_info.name.replace("._http._tcp.local.", "")
},
"configuration_url": f"http://{discovery_info.host}",
},
)
return await self.async_step_zeroconf_confirm()
async def async_step_zeroconf_confirm(
@@ -149,7 +156,7 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
peblar = Peblar(
host=self._host,
host=self._discovery_info.host,
session=async_create_clientsession(
self.hass, cookie_jar=CookieJar(unsafe=True)
),
@@ -165,7 +172,7 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title="Peblar",
data={
CONF_HOST: self._host,
CONF_HOST: self._discovery_info.host,
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
@@ -179,6 +186,10 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
),
}
),
description_placeholders={
"hostname": self._discovery_info.name.replace("._http._tcp.local.", ""),
"host": self._discovery_info.host,
},
errors=errors,
)
+1 -1
View File
@@ -23,7 +23,7 @@ PEBLAR_CHARGE_LIMITER_TO_HOME_ASSISTANT = {
ChargeLimiter.INSTALLATION_LIMIT: "installation_limit",
ChargeLimiter.LOCAL_MODBUS_API: "local_modbus_api",
ChargeLimiter.LOCAL_REST_API: "local_rest_api",
ChargeLimiter.LOCAL_SCHEDULED: "local_scheduled",
ChargeLimiter.LOCAL_SCHEDULED_CHARGING: "local_scheduled_charging",
ChargeLimiter.OCPP_SMART_CHARGING: "ocpp_smart_charging",
ChargeLimiter.OVERCURRENT_PROTECTION: "overcurrent_protection",
ChargeLimiter.PHASE_IMBALANCE: "phase_imbalance",
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["peblar==0.3.0"],
"requirements": ["peblar==0.4.0"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "pblr-*" }]
}
+6 -5
View File
@@ -20,7 +20,7 @@
"data_description": {
"password": "[%key:component::peblar::config::step::user::data_description::password%]"
},
"description": "Reauthenticate with your Peblar EV charger.\n\nTo do so, you will need to enter your new password you use to log into Peblar EV charger' web interface."
"description": "Reauthenticate with your Peblar EV charger.\n\nTo do so, you will need to enter your new password you use to log in to the Peblar EV charger's web interface."
},
"reconfigure": {
"data": {
@@ -31,7 +31,7 @@
"host": "[%key:component::peblar::config::step::user::data_description::host%]",
"password": "[%key:component::peblar::config::step::user::data_description::password%]"
},
"description": "Reconfigure your Peblar EV charger.\n\nThis allows you to change the IP address of your Peblar EV charger and the password you use to log into its web interface."
"description": "Reconfigure your Peblar EV charger.\n\nThis allows you to change the IP address of your Peblar EV charger and the password you use to log in to its web interface."
},
"user": {
"data": {
@@ -40,9 +40,9 @@
},
"data_description": {
"host": "The hostname or IP address of your Peblar EV charger on your home network.",
"password": "The same password as you use to log in to the Peblar EV charger' local web interface."
"password": "The same password as you use to log in to the Peblar EV charger's local web interface."
},
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar EV charger and the password you use to log into its web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need to get the IP address of your Peblar EV charger and the password you use to log in to its web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
},
"zeroconf_confirm": {
"data": {
@@ -51,7 +51,7 @@
"data_description": {
"password": "[%key:component::peblar::config::step::user::data_description::password%]"
},
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar EV charger' web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
"description": "Set up your Peblar EV charger {hostname}, on IP address {host}, to integrate with Home Assistant\n\nTo do so, you will need the password you use to log in to the Peblar EV charger's web interface.\n\nHome Assistant will automatically configure your Peblar EV charger for use with Home Assistant."
}
}
},
@@ -96,6 +96,7 @@
"installation_limit": "Installation limit",
"local_modbus_api": "Modbus API",
"local_rest_api": "REST API",
"local_scheduled_charging": "Scheduled charging",
"ocpp_smart_charging": "OCPP smart charging",
"overcurrent_protection": "Overcurrent protection",
"phase_imbalance": "Phase imbalance",
+6 -2
View File
@@ -27,8 +27,9 @@ PARALLEL_UPDATES = 1
class PeblarUpdateEntityDescription(UpdateEntityDescription):
"""Describe an Peblar update entity."""
installed_fn: Callable[[PeblarVersionInformation], str | None]
available_fn: Callable[[PeblarVersionInformation], str | None]
has_fn: Callable[[PeblarVersionInformation], bool] = lambda _: True
installed_fn: Callable[[PeblarVersionInformation], str | None]
DESCRIPTIONS: tuple[PeblarUpdateEntityDescription, ...] = (
@@ -36,13 +37,15 @@ DESCRIPTIONS: tuple[PeblarUpdateEntityDescription, ...] = (
key="firmware",
device_class=UpdateDeviceClass.FIRMWARE,
installed_fn=lambda x: x.current.firmware,
has_fn=lambda x: x.current.firmware is not None,
available_fn=lambda x: x.available.firmware,
),
PeblarUpdateEntityDescription(
key="customization",
translation_key="customization",
installed_fn=lambda x: x.current.customization,
available_fn=lambda x: x.available.customization,
has_fn=lambda x: x.current.customization is not None,
installed_fn=lambda x: x.current.customization,
),
)
@@ -60,6 +63,7 @@ async def async_setup_entry(
description=description,
)
for description in DESCRIPTIONS
if description.has_fn(entry.runtime_data.version_coordinator.data)
)
@@ -7,6 +7,7 @@ from powerfox import (
Powerfox,
PowerfoxAuthenticationError,
PowerfoxConnectionError,
PowerfoxNoDataError,
Poweropti,
)
@@ -45,5 +46,5 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
return await self.client.device(device_id=self.device.id)
except PowerfoxAuthenticationError as err:
raise ConfigEntryAuthFailed(err) from err
except PowerfoxConnectionError as err:
except (PowerfoxConnectionError, PowerfoxNoDataError) as err:
raise UpdateFailed(err) from err
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/powerfox",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["powerfox==1.0.0"],
"requirements": ["powerfox==1.2.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",
@@ -180,7 +180,7 @@ def guarded_import(
# Allow import of _strptime needed by datetime.datetime.strptime
if name == "_strptime":
return __import__(name, globals, locals, fromlist, level)
raise ScriptError(f"Not allowed to import {name}")
raise ImportError(f"Not allowed to import {name}")
def guarded_inplacevar(op: str, target: Any, operand: Any) -> Any:
@@ -6,7 +6,7 @@
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
"iot_class": "local_polling",
"requirements": ["aioraven==0.7.0"],
"requirements": ["aioraven==0.7.1"],
"usb": [
{
"vid": "0403",
+12
View File
@@ -712,12 +712,24 @@ class Recorder(threading.Thread):
setup_result = self._setup_recorder()
if not setup_result:
_LOGGER.error("Recorder setup failed, recorder shutting down")
# Give up if we could not connect
return
schema_status = migration.validate_db_schema(self.hass, self, self.get_session)
if schema_status is None:
# Give up if we could not validate the schema
_LOGGER.error("Failed to validate schema, recorder shutting down")
return
if schema_status.current_version > SCHEMA_VERSION:
_LOGGER.error(
"The database schema version %s is newer than %s which is the maximum "
"database schema version supported by the installed version of "
"Home Assistant Core, either upgrade Home Assistant Core or restore "
"the database from a backup compatible with this version",
schema_status.current_version,
SCHEMA_VERSION,
)
return
self.schema_version = schema_status.current_version
@@ -27,6 +27,7 @@ from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin
from .host import ReolinkHost
from .services import async_setup_services
from .util import ReolinkConfigEntry, ReolinkData, get_device_uid_and_ch
from .views import PlaybackProxyView
_LOGGER = logging.getLogger(__name__)
@@ -189,6 +190,8 @@ async def async_setup_entry(
migrate_entity_ids(hass, config_entry.entry_id, host)
hass.http.register_view(PlaybackProxyView(hass))
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(
@@ -3,7 +3,7 @@
"name": "Reolink",
"codeowners": ["@starkillerOG"],
"config_flow": true,
"dependencies": ["webhook"],
"dependencies": ["http", "webhook"],
"dhcp": [
{
"hostname": "reolink*"
@@ -23,8 +23,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
from .host import ReolinkHost
from .util import ReolinkConfigEntry
from .util import get_host
from .views import async_generate_playback_proxy_url
_LOGGER = logging.getLogger(__name__)
@@ -47,15 +47,6 @@ def res_name(stream: str) -> str:
return "Low res."
def get_host(hass: HomeAssistant, config_entry_id: str) -> ReolinkHost:
"""Return the Reolink host from the config entry id."""
config_entry: ReolinkConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
assert config_entry is not None
return config_entry.runtime_data.host
class ReolinkVODMediaSource(MediaSource):
"""Provide Reolink camera VODs as media sources."""
@@ -90,22 +81,22 @@ class ReolinkVODMediaSource(MediaSource):
vod_type = get_vod_type()
if vod_type in [VodRequestType.DOWNLOAD, VodRequestType.PLAYBACK]:
proxy_url = async_generate_playback_proxy_url(
config_entry_id, channel, filename, stream_res, vod_type.value
)
return PlayMedia(proxy_url, "video/mp4")
mime_type, url = await host.api.get_vod_source(
channel, filename, stream_res, vod_type
)
if _LOGGER.isEnabledFor(logging.DEBUG):
url_log = url
if "&user=" in url_log:
url_log = f"{url_log.split('&user=')[0]}&user=xxxxx&password=xxxxx"
elif "&token=" in url_log:
url_log = f"{url_log.split('&token=')[0]}&token=xxxxx"
_LOGGER.debug(
"Opening VOD stream from %s: %s", host.api.camera_name(channel), url_log
"Opening VOD stream from %s: %s",
host.api.camera_name(channel),
host.api.hide_password(url),
)
if mime_type == "video/mp4":
return PlayMedia(url, mime_type)
stream = create_stream(self.hass, url, {}, DynamicStreamSettings())
stream.add_provider("hls", timeout=3600)
stream_url: str = stream.endpoint_url("hls")
+15 -1
View File
@@ -22,6 +22,7 @@ from reolink_aio.exceptions import (
)
from homeassistant import config_entries
from homeassistant.components.media_source import Unresolvable
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
@@ -51,6 +52,18 @@ def is_connected(hass: HomeAssistant, config_entry: config_entries.ConfigEntry)
)
def get_host(hass: HomeAssistant, config_entry_id: str) -> ReolinkHost:
"""Return the Reolink host from the config entry id."""
config_entry: ReolinkConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
if config_entry is None:
raise Unresolvable(
f"Could not find Reolink config entry id '{config_entry_id}'."
)
return config_entry.runtime_data.host
def get_device_uid_and_ch(
device: dr.DeviceEntry, host: ReolinkHost
) -> tuple[list[str], int | None, bool]:
@@ -69,7 +82,8 @@ def get_device_uid_and_ch(
ch = int(device_uid[1][5:])
is_chime = True
else:
ch = host.api.channel_for_uid(device_uid[1])
device_uid_part = "_".join(device_uid[1:])
ch = host.api.channel_for_uid(device_uid_part)
return (device_uid, ch, is_chime)
+147
View File
@@ -0,0 +1,147 @@
"""Reolink Integration views."""
from __future__ import annotations
from base64 import urlsafe_b64decode, urlsafe_b64encode
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientTimeout, web
from reolink_aio.enums import VodRequestType
from reolink_aio.exceptions import ReolinkError
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_source import Unresolvable
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.ssl import SSLCipherList
from .util import get_host
_LOGGER = logging.getLogger(__name__)
@callback
def async_generate_playback_proxy_url(
config_entry_id: str, channel: int, filename: str, stream_res: str, vod_type: str
) -> str:
"""Generate proxy URL for event video."""
url_format = PlaybackProxyView.url
return url_format.format(
config_entry_id=config_entry_id,
channel=channel,
filename=urlsafe_b64encode(filename.encode("utf-8")).decode("utf-8"),
stream_res=stream_res,
vod_type=vod_type,
)
class PlaybackProxyView(HomeAssistantView):
"""View to proxy playback video from Reolink."""
requires_auth = True
url = "/api/reolink/video/{config_entry_id}/{channel}/{stream_res}/{vod_type}/{filename}"
name = "api:reolink_playback"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a proxy view."""
self.hass = hass
self.session = async_get_clientsession(
hass,
verify_ssl=False,
ssl_cipher=SSLCipherList.INSECURE,
)
async def get(
self,
request: web.Request,
config_entry_id: str,
channel: str,
stream_res: str,
vod_type: str,
filename: str,
retry: int = 2,
) -> web.StreamResponse:
"""Get playback proxy video response."""
retry = retry - 1
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
ch = int(channel)
try:
host = get_host(self.hass, config_entry_id)
except Unresolvable:
err_str = f"Reolink playback proxy could not find config entry id: {config_entry_id}"
_LOGGER.warning(err_str)
return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST)
try:
mime_type, reolink_url = await host.api.get_vod_source(
ch, filename_decoded, stream_res, VodRequestType(vod_type)
)
except ReolinkError as err:
_LOGGER.warning("Reolink playback proxy error: %s", str(err))
return web.Response(body=str(err), status=HTTPStatus.BAD_REQUEST)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Opening VOD stream from %s: %s",
host.api.camera_name(ch),
host.api.hide_password(reolink_url),
)
try:
reolink_response = await self.session.get(
reolink_url,
timeout=ClientTimeout(
connect=15, sock_connect=15, sock_read=5, total=None
),
)
except ClientError as err:
err_str = host.api.hide_password(
f"Reolink playback error while getting mp4: {err!s}"
)
if retry <= 0:
_LOGGER.warning(err_str)
return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST)
_LOGGER.debug("%s, renewing token", err_str)
await host.api.expire_session(unsubscribe=False)
return await self.get(
request, config_entry_id, channel, stream_res, vod_type, filename, retry
)
# Reolink typo "apolication/octet-stream" instead of "application/octet-stream"
if reolink_response.content_type not in [
"video/mp4",
"application/octet-stream",
"apolication/octet-stream",
]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
_LOGGER.error(err_str)
return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST)
response = web.StreamResponse(
status=200,
reason="OK",
headers={
"Content-Type": "video/mp4",
},
)
if reolink_response.content_length is not None:
response.content_length = reolink_response.content_length
await response.prepare(request)
try:
async for chunk in reolink_response.content.iter_chunked(65536):
await response.write(chunk)
except TimeoutError:
_LOGGER.debug(
"Timeout while reading Reolink playback from %s, writing EOF",
host.api.nvr_name,
)
reolink_response.release()
await response.write_eof()
return response
@@ -209,10 +209,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
except ValueError:
errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
try:
off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10)
except ValueError:
errors[CONF_OFF_DELAY] = "invalid_input_off_delay"
off_delay = user_input.get(CONF_OFF_DELAY)
if not errors:
devices = {}
@@ -252,11 +249,11 @@ class RfxtrxOptionsFlow(OptionsFlow):
vol.Optional(
CONF_OFF_DELAY,
description={"suggested_value": device_data[CONF_OFF_DELAY]},
): str,
): int,
}
else:
off_delay_schema = {
vol.Optional(CONF_OFF_DELAY): str,
vol.Optional(CONF_OFF_DELAY): int,
}
data_schema.update(off_delay_schema)
@@ -68,7 +68,6 @@
"invalid_event_code": "Invalid event code",
"invalid_input_2262_on": "Invalid input for command on",
"invalid_input_2262_off": "Invalid input for command off",
"invalid_input_off_delay": "Invalid input for off delay",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
+18 -1
View File
@@ -9,7 +9,13 @@ from datetime import timedelta
import logging
from typing import Any
from roborock import HomeDataRoom, RoborockException, RoborockInvalidCredentials
from roborock import (
HomeDataRoom,
RoborockException,
RoborockInvalidCredentials,
RoborockInvalidUserAgreement,
RoborockNoUserAgreement,
)
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, UserData
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
from roborock.version_a01_apis import RoborockMqttClientA01
@@ -60,12 +66,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
translation_domain=DOMAIN,
translation_key="invalid_credentials",
) from err
except RoborockInvalidUserAgreement as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="invalid_user_agreement",
) from err
except RoborockNoUserAgreement as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="no_user_agreement",
) from err
except RoborockException as err:
raise ConfigEntryNotReady(
"Failed to get Roborock home data",
translation_domain=DOMAIN,
translation_key="home_data_fail",
) from err
_LOGGER.debug("Got home data %s", home_data)
all_devices: list[HomeDataDevice] = home_data.devices + home_data.received_devices
device_map: dict[str, HomeDataDevice] = {
@@ -60,7 +60,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
username = user_input[CONF_USERNAME]
await self.async_set_unique_id(username.lower())
self._abort_if_unique_id_configured()
self._abort_if_unique_id_configured(error="already_configured_account")
self._username = username
_LOGGER.debug("Requesting code for Roborock account")
self._client = RoborockApiClient(username)
@@ -28,7 +28,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
@@ -422,6 +422,12 @@
},
"update_options_failed": {
"message": "Failed to update Roborock options"
},
"invalid_user_agreement": {
"message": "User agreement must be accepted again. Open your Roborock app and accept the agreement."
},
"no_user_agreement": {
"message": "You have not valid user agreement. Open your Roborock app and accept the agreement."
}
},
"services": {
@@ -73,7 +73,6 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
return {}
# API version 2 is not working, try API version 1 instead
await slide.slide_del(user_input[CONF_HOST])
await slide.slide_add(
user_input[CONF_HOST],
user_input.get(CONF_PASSWORD, ""),
@@ -185,14 +184,15 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(self._mac)
self._abort_if_unique_id_configured(
{CONF_HOST: discovery_info.host}, reload_on_update=True
)
ip = str(discovery_info.ip_address)
_LOGGER.debug("Slide device discovered, ip %s", ip)
self._abort_if_unique_id_configured({CONF_HOST: ip}, reload_on_update=True)
errors = {}
if errors := await self.async_test_connection(
{
CONF_HOST: self._host,
CONF_HOST: ip,
}
):
return self.async_abort(
@@ -202,7 +202,7 @@ class SlideConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
self._host = discovery_info.host
self._host = ip
return await self.async_step_zeroconf_confirm()

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