Compare commits

...

154 Commits

Author SHA1 Message Date
Franck Nijhof
a3319262ac 2023.11.2 (#103737) 2023-11-10 12:41:51 +01:00
Franck Nijhof
eaf711335d Bumped version to 2023.11.2 2023-11-10 10:04:50 +01:00
Bram Kragten
f120558750 Update frontend to 20231030.2 (#103706) 2023-11-10 10:04:33 +01:00
Joost Lekkerkerker
30dc05cdd7 Add name to Withings coordinator (#103692) 2023-11-10 10:04:29 +01:00
J. Nick Koston
8ce746972f Incease tplink setup timeout (#103671)
It can take longer than 5s to do the first update of the device
especially when the device is overloaded as seen in #103668

Wait 10 seconds for the discovery since when the power strips are loaded they cannot respond in time
2023-11-10 10:04:26 +01:00
starkillerOG
f946ed9e16 Fix Reolink DHCP IP update (#103654) 2023-11-10 10:04:23 +01:00
Joost Lekkerkerker
0ffc1bae76 Bump yt-dlp to 2023.10.13 (#103616) 2023-11-10 10:04:20 +01:00
Joost Lekkerkerker
d1a3a5895b Raise exception when data can't be fetched in Opensky (#103596) 2023-11-10 10:04:17 +01:00
suaveolent
f9c70fd3c8 fix: get_devices only checks for the first type (#103583) 2023-11-10 10:04:14 +01:00
dupondje
70f0ee81c9 Update dsmr-parser to 1.3.1 to fix parsing issues (#103572) 2023-11-10 10:04:10 +01:00
Charles Garwood
95d4254074 Bump pyenphase to 1.14.2 (#103553) 2023-11-10 10:04:07 +01:00
J. Nick Koston
c8d3e377f0 Bump aioesphomeapi to 18.2.4 (#103552) 2023-11-10 10:04:04 +01:00
epenet
da1c282c1b Fix invalid MAC in samsungtv (#103512)
* Fix invalid MAC in samsungtv

* Also adjust __init__
2023-11-10 10:04:01 +01:00
dupondje
35c0c9958d Fix 5B Gas meter in dsmr (#103506)
* Fix 5B Gas meter in dsmr

In commit 1b73219 the gas meter broke for 5B.
As the change can't be reverted easily without removing the peak usage
sensors, we implement a workaround.

The first MBUS_METER_READING2 value will contain the gas meter data just
like the previous BELGIUM_5MIN_GAS_METER_READING did.
But this without the need to touch dsmr_parser (version).

Fixes: #103306, #103293

* Use parametrize

* Apply suggestions from code review

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

* Add additional tests + typo fix

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2023-11-10 10:03:57 +01:00
mkmer
93a0bd351a Bump blinkpy to 0.22.3 (#103438) 2023-11-10 10:00:11 +01:00
J. Nick Koston
dbdd9d74cf Bump bluetooth-data-tools to 0.14.0 (#103413)
changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.13.0...v1.14.0
2023-11-10 10:00:08 +01:00
J. Nick Koston
3cac87cf30 Bump aioesphomeapi to 18.2.1 (#103156) 2023-11-10 10:00:05 +01:00
Jesse Hills
d019045199 ESPHome: Add suggested_area from device info (#102834) 2023-11-10 10:00:00 +01:00
Robert Resch
8f684ab102 Revert binary_sensor part of #103210 (#103499) 2023-11-06 17:56:53 +01:00
Matt Zimmerman
c17def27fc Fix litterrobot test failure due to time zone dependence (#103444)
* fix litterrobot test

* use a date in northern hemisehpere summer
2023-11-06 17:03:07 +01:00
dupondje
27d8d1011e Use right equipment identifier in DSMR setup (#103494) 2023-11-06 14:51:33 +01:00
Joost Lekkerkerker
e2270a305d Sort Withings sleep data on end date (#103454)
* Sort Withings sleep data on end date

* Sort Withings sleep data on end date
2023-11-06 14:51:30 +01:00
Matthias Alphart
6fd8973a00 Fix KNX expose default value when attribute is None (#103446)
Fix KNX expose default value when attribute is `null`
2023-11-06 14:51:27 +01:00
jan iversen
9a37868244 Modbus set device_class in slaves (#103442) 2023-11-06 14:51:24 +01:00
jan iversen
9327c51115 modbus Allow swap: byte for datatype: string. (#103441) 2023-11-06 14:51:21 +01:00
Michael
e56e75114a Fix serial in Flo device information (#103427) 2023-11-06 14:51:17 +01:00
Tobias Sauerwein
f45114371e Bump pyatmo to v7.6.0 (#103410)
Signed-off-by: Tobias Sauerwein <cgtobi@gmail.com>
2023-11-06 14:51:14 +01:00
Franck Nijhof
7e2c12b0a9 Update tailscale to 0.6.0 (#103409) 2023-11-06 14:51:10 +01:00
J. Nick Koston
050f1085d0 Pin jaraco.functools to fix builds and CI (#103406) 2023-11-06 14:51:07 +01:00
Matt Zimmerman
334a02bc2b Handle smarttub sensor values being None (#103385)
* [smarttub] handle sensor values being None

* empty commit to rerun CI

* lint

* use const in test

* reorder checks

* use None instead of STATE_UNKNOWN

* empty commit to rerun CI

* check for STATE_UNKNOWN in test

* empty commit to rerun CI
2023-11-06 14:51:04 +01:00
Nathan Spencer
412fa4c65a Handle null data in WeatherFlow sensors (#103349)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-11-06 14:51:01 +01:00
Álvaro Fernández Rojas
2b36befe95 Update aioairzone-cloud to v0.3.5 (#103315)
* Update aioairzone-cloud to v0.3.3

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Update aioairzone-cloud to v0.3.4

Reverts accidental TaskGroup changes.

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Update aioairzone-cloud to v0.3.5

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>

* Trigger Github CI

---------

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-11-06 14:50:56 +01:00
Álvaro Fernández Rojas
aa623cc15c Update aioairzone-cloud to v0.3.2 (#103258)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-11-06 14:50:51 +01:00
Maciej Bieniek
b0bb91ec08 Don't assume that the sleep value is a dictionary in Tractive integration (#103138)
* Sleep value can be null

* Catch TypeError
2023-11-06 14:49:10 +01:00
Franck Nijhof
ce12d82624 2023.11.1 (#103301) 2023-11-04 14:27:14 +01:00
Raman Gupta
9eff9ee374 Fix zwave_js cover bug for Window Covering CC values (#103289)
* Fix cover bug for Window Covering CC values

* update test

* Fix fixture

* Remove no-op line from test
2023-11-04 12:53:50 +01:00
Rami Mosleh
1ef460cffe Fix sensor unique id in Islamic prayer times (#103356) 2023-11-04 12:53:24 +01:00
Robert Svensson
42243f1433 Handle UniFi traffic rules not supported on older versions (#103346) 2023-11-04 12:53:21 +01:00
Jan Rieger
8a07c10d88 Report correct weather condition at night for Met (#103334)
* Report correct weather condition at night for Met, fixes #68369, fixes #89001

* Update homeassistant/components/met/weather.py

Co-authored-by: Jan-Philipp Benecke <github@bnck.me>

---------

Co-authored-by: Jan-Philipp Benecke <github@bnck.me>
2023-11-04 12:53:18 +01:00
Joakim Sørensen
730a3f7870 Remove extra from traccar webhook (#103319) 2023-11-04 12:53:15 +01:00
Jan-Philipp Benecke
718901d2ad Fix typo in Todoist config flow (#103317) 2023-11-04 12:53:12 +01:00
Raman Gupta
d95d4d0184 Add script to convert zwave_js device diagnostics to fixture (#102799) 2023-11-04 12:53:06 +01:00
Ian
67ce51899f Bump py_nextbusnext to v1.0.2 to fix TypeError (#103214)
* Bump py_nextbusnext to v1.0.1 to fix TypeError

Currently throwing an error as a set is passed into the method that is currently
expecting a Sequence. That method is technically compatible with Iterable, so the
latest patch relaxes that restriction.

* Bump version to v1.0.2 to fix error message
2023-11-04 12:46:05 +01:00
starkillerOG
810681b357 Bump reolink-aio to 0.7.14 and improve typing of Reolink (#103129)
* Improve typing

* fix mypy

* Further improve typing

* Restore Literal typing

* Bump reolink_aio to 0.7.13

* Bump reolink-aio to 0.7.14
2023-11-04 12:46:01 +01:00
Franck Nijhof
0b0f099d27 Bumped version to 2023.11.1 2023-11-03 13:02:22 +01:00
tronikos
4a56d0ec1d Bump opower to 0.0.39 (#103292) 2023-11-03 13:02:04 +01:00
Raman Gupta
910654bf78 Fix firmware update failure (#103277) 2023-11-03 13:02:01 +01:00
Marcel van der Veldt
1a823376d8 Fix Matter 1.2 locks with specific unlatch/unbolt support (#103275) 2023-11-03 13:01:56 +01:00
Pedro Januário
ba634ac346 add library logger info on ecoforest integration manifest (#103274) 2023-11-03 13:01:24 +01:00
J. Nick Koston
92486b1ff0 Bump yalexs-ble to 2.3.2 (#103267) 2023-11-03 12:59:19 +01:00
Tom
06d26b7c7f Fix Plugwise Schedule selection (#103262) 2023-11-03 12:59:16 +01:00
Joakim Plate
1dcd66d75c Remove measurement flag from timestamp in gardena bluetooth (#103245)
Remove measurement flag from timestamp
2023-11-03 12:59:13 +01:00
Charles Garwood
c811e0db49 Bump pyenphase to 1.14.1 (#103239) 2023-11-03 12:59:09 +01:00
Matthias Alphart
dc30ddc24b Fix Fronius entity initialisation (#103211)
* Use None instead of raising ValueError if value invalid

* use async_dispatcher_send
2023-11-03 12:59:06 +01:00
Jan Bouwhuis
239fa04d02 Fix mqtt config validation error handling (#103210)
* Fix MQTT config check

* Fix handling invalid enity_category for sensors

* Improve docstr

* Update comment

* Use correct util for yaml dump
2023-11-03 12:59:03 +01:00
Xitee
2be229c5b5 Fix roomba error if battery stats are not available (#103196) 2023-11-03 12:59:00 +01:00
Xitee
5b4df0f7ff Fix roomba translation key mismatch (#103191) 2023-11-03 12:58:57 +01:00
mkmer
355b51d4c8 Catch unexpected response in Honeywell (#103169)
catch unexpected response
2023-11-03 12:58:54 +01:00
Joost Lekkerkerker
0c8074bab4 Bump aiowaqi to 3.0.0 (#103166) 2023-11-03 12:58:51 +01:00
G Johansson
acd98e9b40 Bump python-holidays to 0.35 (#103092) 2023-11-03 12:58:45 +01:00
Franck Nijhof
0b8d4235c3 2023.11.0 (#103164) 2023-11-01 15:59:51 +01:00
Franck Nijhof
4ce859b4e4 Bumped version to 2023.11.0 2023-11-01 11:24:41 +01:00
Franck Nijhof
18acec32b8 Bumped version to 2023.11.0b6 2023-11-01 11:22:25 +01:00
Bram Kragten
cfa2f2ce61 Update frontend to 20231030.1 (#103163) 2023-11-01 11:22:16 +01:00
Jan Bouwhuis
aa5ea5ebc3 Fix mqtt is not reloading without yaml config (#103159) 2023-11-01 11:22:13 +01:00
J. Nick Koston
bcea021c14 Allow non-admins to subscribe to the issue registry updated event (#103145) 2023-11-01 11:22:10 +01:00
Allen Porter
ea2d2ba7b7 Improve fitbit oauth token error handling in config flow (#103131)
* Improve fitbit oauth token error handling in config flow

* Apply suggestions from code review

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

* Update tests with updated error reason

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-11-01 11:22:07 +01:00
Teemu R
c5f21fefbe Bump python-kasa to 0.5.4 for tplink (#103038) 2023-11-01 11:22:03 +01:00
Franck Nijhof
9910f9e0ae Bumped version to 2023.11.0b5 2023-10-31 19:43:21 +01:00
J. Nick Koston
f0a06efa1f Fix race in starting reauth flows (#103130) 2023-10-31 19:43:05 +01:00
J. Nick Koston
8992d15ffc Bump aiohomekit to 3.0.9 (#103123) 2023-10-31 19:43:02 +01:00
Paul Bottein
e097dc02dd Don't try to load resources in safe mode (#103122) 2023-10-31 19:42:59 +01:00
starkillerOG
bfae1468d6 Bump reolink-aio to 0.7.12 (#103120) 2023-10-31 19:42:52 +01:00
Christopher Fenner
09ed6e9f9b Handle exception introduced with recent PyViCare update (#103110) 2023-10-31 19:42:48 +01:00
Erik Montnemery
040ecb74e0 Add todo to core files (#103102) 2023-10-31 19:42:45 +01:00
Erik Montnemery
a48e63aa28 Fix todoist todo tests (#103101) 2023-10-31 19:42:41 +01:00
Erik Montnemery
19479b2a68 Fix local_todo todo tests (#103099) 2023-10-31 19:42:36 +01:00
Franck Nijhof
9ae29e243d Bumped version to 2023.11.0b4 2023-10-31 13:30:10 +01:00
Joost Lekkerkerker
e309bd764b Abort config flow if Google Tasks API is not enabled (#103114)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-31 13:29:52 +01:00
Christopher Fenner
777ffe6946 Fix client id label in ViCare integration (#103111) 2023-10-31 13:29:49 +01:00
Robert Resch
fa0f679a9a Fix todo.remove_item frontend (#103108) 2023-10-31 13:29:46 +01:00
Erik Montnemery
26b7e94c4f Fix shopping_list todo tests (#103100) 2023-10-31 13:29:43 +01:00
Erik Montnemery
957998ea8d Fix google_tasks todo tests (#103098) 2023-10-31 13:29:40 +01:00
Erik Montnemery
abaeacbd6b Fix restore state for light when saved attribute is None (#103096) 2023-10-31 13:29:36 +01:00
Bram Kragten
d76c16fa3a Update frontend to 20231030.0 (#103086) 2023-10-31 13:29:33 +01:00
G Johansson
67edb98e59 Fix Met Device Info (#103082) 2023-10-31 13:29:30 +01:00
Robert Resch
376a79eb42 Refactor todo services and their schema (#103079) 2023-10-31 13:29:27 +01:00
Rami Mosleh
41500cbe9b Code cleanup for transmission integration (#103078) 2023-10-31 13:29:24 +01:00
Paul Manzotti
06f27e7e74 Update geniushub-client to v0.7.1 (#103071) 2023-10-31 13:29:19 +01:00
Franck Nijhof
a3ebfaebe7 Bumped version to 2023.11.0b3 2023-10-30 19:59:32 +01:00
Joost Lekkerkerker
8d781ff063 Add 2 properties to Withings diagnostics (#103067) 2023-10-30 19:59:21 +01:00
Joost Lekkerkerker
bac39f0061 Show a warning when no Withings data found (#103066) 2023-10-30 19:59:17 +01:00
David Knowles
c7b702f3c2 Bump pyschlage to 2023.10.0 (#103065) 2023-10-30 19:59:14 +01:00
Christopher Fenner
3728f3da69 Update PyViCare to v2.28.1 for ViCare integration (#103064) 2023-10-30 19:59:11 +01:00
tronikos
31d8f4b35d Fix Opower not refreshing statistics when there are no forecast entities (#103058)
Ensure _insert_statistics is periodically called
2023-10-30 19:59:08 +01:00
Mike Woudenberg
f113d9aa71 Use correct config entry field to update when IP changes in loqed (#103051) 2023-10-30 19:59:05 +01:00
Jack Boswell
891ad0b1be Bump starlink-grpc-core to 1.1.3 (#103043) 2023-10-30 19:59:02 +01:00
Jirka
5c16a8247a Update MQTT QoS description string (#103036)
Update strings.json
2023-10-30 19:58:58 +01:00
Allen Porter
483671bf9f Bump google-nest-sdm to 3.0.3 (#103035) 2023-10-30 19:58:54 +01:00
G-Two
6f73d2aac5 Bump to subarulink 0.7.8 (#103033) 2023-10-30 19:58:50 +01:00
Allen Porter
f5b3661836 Fix bug in fitbit credential import for expired tokens (#103024)
* Fix bug in fitbit credential import on token refresh

* Use stable test ids

* Update homeassistant/components/fitbit/sensor.py

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

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-30 19:58:47 +01:00
kpine
f70c13214c Revert "Fix temperature setting for multi-setpoint z-wave device (#102395)" (#103022)
This reverts commit 2d6dc2bccc.
2023-10-30 19:58:43 +01:00
Raman Gupta
70e8978123 Fix zwave_js siren name (#103016)
* Fix zwave_js.siren name

* Fix test
2023-10-30 19:58:40 +01:00
Diogo Gomes
031b1c26ce Fix utility_meter reset when DST change occurs (#103012) 2023-10-30 19:58:37 +01:00
Nortonko
13580a334f Bump python-androidtv to 0.0.73 (#102999)
* Update manifest.json

Bump python-androidtv to version 0.0.73

* bump androidtv 0.0.73

* bump androidtv 0.0.73
2023-10-30 19:58:34 +01:00
Michael
e81bfb959e Fix proximity entity id (#102992)
* fix proximity entity id

* extend test to cover entity id
2023-10-30 19:58:31 +01:00
Tom Puttemans
fefe930506 DSMR Gas currently delivered device state class conflict (#102991)
Fixes #102985
2023-10-30 19:58:28 +01:00
David Bonnes
5ac7e8b1ac Harden evohome against failures to retrieve high-precision temps (#102989)
fix hass-logger-period
2023-10-30 19:58:24 +01:00
tronikos
36512f7157 Bump opower to 0.0.38 (#102983) 2023-10-30 19:58:21 +01:00
Bouwe Westerdijk
cc3ae9e103 Correct total state_class of huisbaasje sensors (#102945)
* Change all cumulative-interval sensors to TOTAL
2023-10-30 19:58:18 +01:00
Robert Hillis
12482216f6 Fix Google Mail expired authorization (#102735)
* Fix Google Mail expired authorization

* add test

* raise HomeAssistantError

* handle in api module

* uno mas
2023-10-30 19:58:14 +01:00
David Knowles
20409d0124 Make Hydrawise initialize data immediately (#101936) 2023-10-30 19:58:11 +01:00
mkmer
a741bc9951 Add retry before unavailable to Honeywell (#101702)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-30 19:58:08 +01:00
Erwin Douna
59d2bce369 Enable dry mode for Tado AC's V3 (#99568) 2023-10-30 19:58:04 +01:00
Paulus Schoutsen
eef318f63c Bumped version to 2023.11.0b2 2023-10-28 23:29:03 -04:00
Paulus Schoutsen
9c8a4bb4eb Fix proximity zone handling (#102971)
* fix proximity zone

* fix test
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
9c9f1ea685 Fix error message strings for Todoist configuration flow (#102968)
* Fix error message strings for Todoist configuration flow

* Update error code in test
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
85d999b020 Add gas device class to dsmr_reader sensor (#102953)
DSMR reader integration - can't configure gas meter in energy dashboard posible due to missing device_class
Fixes #102367
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
bcddf52364 Update xknxproject to 3.4.0 (#102946) 2023-10-28 23:29:03 -04:00
Paulus Schoutsen
07e4e1379a Improve diagnostic handling in HomeWizard Energy (#102935) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
f9f010643a Handle/extend number entity availability property in HomeWizard Energy (#102934) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
974c34e2b6 Small base entity cleanup for HomeWizard Energy entities (#102933) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
1c3de76b04 Move HomeWizard Energy identify button to config entity category (#102932) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
bee63ca654 Hide mac address from HomeWizard Energy config entry/discovery titles (#102931) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
29c99f419f Bump velbusaio to 2023.10.2 (#102919) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
3d321c5ca7 Update frontend to 20231027.0 (#102913) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
4617c16a96 Update aioairzone-cloud to v0.3.1 (#102899) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
a60656bf29 Improve fitbit oauth import robustness (#102833)
* Improve fitbit oauth import robustness

* Improve sensor tests and remove unnecessary client check

* Fix oauth client id/secret config key checks

* Add executor for sync call
2023-10-28 23:29:02 -04:00
Paulus Schoutsen
2eb2a65197 Use new API for Vasttrafik (#102570) 2023-10-28 23:29:02 -04:00
Franck Nijhof
867aaf10ee Bumped version to 2023.11.0b1 2023-10-27 14:02:42 +02:00
Franck Nijhof
7fe1ac901f Some textual fixes for todo (#102895) 2023-10-27 14:02:24 +02:00
Bram Kragten
5dca3844ef Add redirect from shopping list to todo (#102894) 2023-10-27 14:02:20 +02:00
Erik Montnemery
b5c75a2f2f Allow missing components in safe mode (#102891) 2023-10-27 14:02:17 +02:00
Erik Montnemery
62fc9dfd6c Allow missing components in safe mode (#102888) 2023-10-27 14:02:14 +02:00
Jan Bouwhuis
0573981d6f Fix mqtt schema import not available for mqtt_room (#102866) 2023-10-27 14:02:09 +02:00
Paul Bottein
cc7a4d01e3 Don't return resources in safe mode (#102865) 2023-10-27 14:02:06 +02:00
Paul Bottein
293025ab6c Update frontend to 20231026.0 (#102857) 2023-10-27 14:02:02 +02:00
Jan-Philipp Benecke
a490b5e286 Add connections to PassiveBluetoothProcessorEntity (#102854) 2023-10-27 14:01:58 +02:00
Joost Lekkerkerker
7e4da1d03b Bump aiowithings to 1.0.2 (#102852) 2023-10-27 14:01:53 +02:00
Ravaka Razafimanantsoa
9e140864eb Address late review of switchbot cloud (#102842)
For Martin's review
2023-10-27 14:01:47 +02:00
Kevin Worrel
a6f88fb123 Bump screenlogicpy to v0.9.4 (#102836) 2023-10-27 14:01:43 +02:00
J. Nick Koston
386c5ecc3e Bump bleak-retry-connector to 3.3.0 (#102825)
changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v3.2.1...v3.3.0
2023-10-27 14:01:39 +02:00
Erik Montnemery
0d7fb5b026 Use real devices in automation blueprint tests (#102824) 2023-10-27 14:01:35 +02:00
Erik Montnemery
767b7ba4d6 Correct logic for picking bluetooth local name (#102823)
* Correct logic for picking bluetooth local name

* make test more robust

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-27 14:01:31 +02:00
William Scanlon
f2cef7245a Bump pyeconet to 0.1.22 to handle breaking API change (#102820) 2023-10-27 14:01:27 +02:00
J. Nick Koston
701a5d7758 Bump HAP-python 4.9.1 (#102811) 2023-10-27 14:01:23 +02:00
mkmer
244fccdae6 Move coordinator first refresh in Blink (#102805)
Move coordinator first refresh
2023-10-27 14:01:18 +02:00
Erik Montnemery
10e6a26717 Fix fan device actions (#102797) 2023-10-27 14:01:15 +02:00
Allen Porter
5fe5013198 Change todo move API to reference previous uid (#102795) 2023-10-27 14:01:11 +02:00
Marc Mueller
0a0584b053 Fix velbus import (#102780) 2023-10-27 14:01:07 +02:00
Erik Montnemery
62733e830f Improve validation of device automations (#102766)
* Improve validation of device automations

* Improve comments

* Address review comment
2023-10-27 14:01:02 +02:00
Simone Chemelli
bbcfb5f30e Improve exception handling for Vodafone Station (#102761)
* improve exception handling for Vodafone Station

* address review comment

* apply review comment

* better except handling (bump library)

* cleanup
2023-10-27 14:00:55 +02:00
Amit Finkelstein
5b0e0b07b3 Apple TV: Use replacement commands for deprecated ones (#102056)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-27 14:00:45 +02:00
Franck Nijhof
05fd64fe80 Bumped version to 2023.11.0b0 2023-10-25 17:41:53 +02:00
244 changed files with 7402 additions and 6373 deletions

View File

@@ -45,6 +45,7 @@ base_platforms: &base_platforms
- homeassistant/components/switch/**
- homeassistant/components/text/**
- homeassistant/components/time/**
- homeassistant/components/todo/**
- homeassistant/components/tts/**
- homeassistant/components/update/**
- homeassistant/components/vacuum/**

View File

@@ -19,6 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
# These are events that do not contain any sensitive data
# Except for state_changed, which is handled accordingly.
@@ -28,6 +29,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[str]] = {
EVENT_CORE_CONFIG_UPDATE,
EVENT_DEVICE_REGISTRY_UPDATED,
EVENT_ENTITY_REGISTRY_UPDATED,
EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
EVENT_LOVELACE_UPDATED,
EVENT_PANELS_UPDATED,
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==3.3.0"]
"requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.3.0"]
"requirements": ["aioairzone-cloud==0.3.5"]
}

View File

@@ -9,7 +9,7 @@
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
"requirements": [
"adb-shell[async]==0.4.4",
"androidtv[async]==0.0.72",
"androidtv[async]==0.0.73",
"pure-python-adb[async]==0.3.0.dev0"
]
}

View File

@@ -21,6 +21,15 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
COMMAND_TO_ATTRIBUTE = {
"wakeup": ("power", "turn_on"),
"suspend": ("power", "turn_off"),
"turn_on": ("power", "turn_on"),
"turn_off": ("power", "turn_off"),
"volume_up": ("audio", "volume_up"),
"volume_down": ("audio", "volume_down"),
"home_hold": ("remote_control", "home"),
}
async def async_setup_entry(
@@ -61,7 +70,13 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
for _ in range(num_repeats):
for single_command in command:
attr_value = getattr(self.atv.remote_control, single_command, None)
attr_value = None
if attributes := COMMAND_TO_ATTRIBUTE.get(single_command):
attr_value = self.atv
for attr_name in attributes:
attr_value = getattr(attr_value, attr_name, None)
if not attr_value:
attr_value = getattr(self.atv.remote_control, single_command, None)
if not attr_value:
raise ValueError("Command not found. Exiting sequence")

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.1"]
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.2"]
}

View File

@@ -86,8 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
blink.auth = Auth(auth_data, no_prompt=True, session=session)
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
coordinator = BlinkUpdateCoordinator(hass, blink)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
try:
await blink.start()
@@ -101,6 +99,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not blink.available:
raise ConfigEntryNotReady
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.22.2"]
"requirements": ["blinkpy==0.22.3"]
}

View File

@@ -330,7 +330,7 @@ class BaseHaRemoteScanner(BaseHaScanner):
prev_manufacturer_data = prev_advertisement.manufacturer_data
prev_name = prev_device.name
if local_name and prev_name and len(prev_name) > len(local_name):
if prev_name and (not local_name or len(prev_name) > len(local_name)):
local_name = prev_name
if service_uuids and service_uuids != prev_service_uuids:

View File

@@ -15,10 +15,10 @@
"quality_scale": "internal",
"requirements": [
"bleak==0.21.1",
"bleak-retry-connector==3.2.1",
"bleak-retry-connector==3.3.0",
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.13.0",
"bluetooth-data-tools==1.14.0",
"dbus-fast==2.12.0"
]
}

View File

@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast
from homeassistant import config_entries
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_IDENTIFIERS,
ATTR_NAME,
CONF_ENTITY_CATEGORY,
@@ -16,7 +17,7 @@ from homeassistant.const import (
EntityCategory,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_platform import async_get_current_platform
from homeassistant.helpers.event import async_track_time_interval
@@ -644,6 +645,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce
self._attr_unique_id = f"{address}-{key}"
if ATTR_NAME not in self._attr_device_info:
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
if device_id is None:
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)}
self._attr_name = processor.entity_names.get(entity_key)
@property

View File

@@ -5,9 +5,9 @@ from typing import cast
import voluptuous as vol
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, Platform
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from . import DeviceAutomationType, async_get_device_automation_platform
@@ -55,31 +55,42 @@ async def async_validate_device_automation_config(
platform = await async_get_device_automation_platform(
hass, validated_config[CONF_DOMAIN], automation_type
)
# Make sure the referenced device and optional entity exist
device_registry = dr.async_get(hass)
if not (device := device_registry.async_get(validated_config[CONF_DEVICE_ID])):
# The device referenced by the device automation does not exist
raise InvalidDeviceAutomationConfig(
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
)
if entity_id := validated_config.get(CONF_ENTITY_ID):
try:
er.async_validate_entity_id(er.async_get(hass), entity_id)
except vol.Invalid as err:
raise InvalidDeviceAutomationConfig(
f"Unknown entity '{entity_id}'"
) from err
if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]):
# Pass the unvalidated config to avoid mutating the raw config twice
return cast(
ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config)
)
# Bypass checks for entity platforms
# Devices are not linked to config entries from entity platform domains, skip
# the checks below which look for a config entry matching the device automation
# domain
if (
automation_type == DeviceAutomationType.ACTION
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
):
# Pass the unvalidated config to avoid mutating the raw config twice
return cast(
ConfigType,
await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config),
)
# Only call the dynamic validator if the referenced device exists and the relevant
# config entry is loaded
registry = dr.async_get(hass)
if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])):
# The device referenced by the device automation does not exist
raise InvalidDeviceAutomationConfig(
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
)
# Find a config entry with the same domain as the device automation
device_config_entry = None
for entry_id in device.config_entries:
if (
@@ -91,7 +102,7 @@ async def async_validate_device_automation_config(
break
if not device_config_entry:
# The config entry referenced by the device automation does not exist
# There's no config entry with the same domain as the device automation
raise InvalidDeviceAutomationConfig(
f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from "
f"domain '{validated_config[CONF_DOMAIN]}'"

View File

@@ -53,6 +53,8 @@ class DSMRConnection:
self._protocol = protocol
self._telegram: dict[str, DSMRObject] = {}
self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER
if dsmr_version == "5B":
self._equipment_identifier = obis_ref.BELGIUM_EQUIPMENT_IDENTIFIER
if dsmr_version == "5L":
self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER
if dsmr_version == "Q3D":

View File

@@ -34,6 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
DSMR_PROTOCOL = "dsmr_protocol"
RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol"
# Temp obis until sensors replaced by mbus variants
BELGIUM_5MIN_GAS_METER_READING = r"\d-\d:24\.2\.3.+?\r\n"

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.3.0"]
"requirements": ["dsmr-parser==1.3.1"]
}

View File

@@ -44,7 +44,6 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util import Throttle
from .const import (
BELGIUM_5MIN_GAS_METER_READING,
CONF_DSMR_VERSION,
CONF_PRECISION,
CONF_PROTOCOL,
@@ -382,16 +381,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading",
obis_reference=BELGIUM_5MIN_GAS_METER_READING,
dsmr_versions={"5B"},
is_gas=True,
force_update=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
DSMRSensorEntityDescription(
key="gas_meter_reading",
translation_key="gas_meter_reading",
@@ -405,6 +394,31 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
)
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
"""Return correct entity for 5B Gas meter."""
ref = None
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS2_METER_READING2
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS3_METER_READING2
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
ref = obis_references.BELGIUM_MBUS4_METER_READING2
elif ref is None:
ref = obis_references.BELGIUM_MBUS1_METER_READING2
return DSMRSensorEntityDescription(
key="belgium_5min_gas_meter_reading",
translation_key="gas_meter_reading",
obis_reference=ref,
dsmr_versions={"5B"},
is_gas=True,
force_update=True,
device_class=SensorDeviceClass.GAS,
state_class=SensorStateClass.TOTAL_INCREASING,
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@@ -438,6 +452,10 @@ async def async_setup_entry(
return (entity_description.device_class, UNIT_CONVERSION[uom])
return (entity_description.device_class, uom)
all_sensors = SENSORS
if dsmr_version == "5B":
all_sensors += (add_gas_sensor_5B(telegram),)
entities.extend(
[
DSMREntity(
@@ -448,7 +466,7 @@ async def async_setup_entry(
telegram, description
), # type: ignore[arg-type]
)
for description in SENSORS
for description in all_sensors
if (
description.dsmr_versions is None
or dsmr_version in description.dsmr_versions

View File

@@ -141,6 +141,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
translation_key="gas_meter_usage",
entity_registry_enabled_default=False,
icon="mdi:fire",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
@@ -283,6 +284,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
key="dsmr/day-consumption/gas",
translation_key="daily_gas_usage",
icon="mdi:counter",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
),
DSMRReaderSensorEntityDescription(
@@ -460,6 +462,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
key="dsmr/current-month/gas",
translation_key="current_month_gas_usage",
icon="mdi:counter",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
),
DSMRReaderSensorEntityDescription(
@@ -538,6 +541,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
key="dsmr/current-year/gas",
translation_key="current_year_gas_usage",
icon="mdi:counter",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
),
DSMRReaderSensorEntityDescription(

View File

@@ -5,5 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecoforest",
"iot_class": "local_polling",
"loggers": ["pyecoforest"],
"requirements": ["pyecoforest==0.3.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/econet",
"iot_class": "cloud_push",
"loggers": ["paho_mqtt", "pyeconet"],
"requirements": ["pyeconet==0.1.20"]
"requirements": ["pyeconet==0.1.22"]
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.13.1"],
"requirements": ["pyenphase==1.14.2"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -596,6 +596,10 @@ def _async_setup_device_registry(
model = project_name[1]
hw_version = device_info.project_version
suggested_area = None
if device_info.suggested_area:
suggested_area = device_info.suggested_area
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
@@ -606,6 +610,7 @@ def _async_setup_device_registry(
model=model,
sw_version=sw_version,
hw_version=hw_version,
suggested_area=suggested_area,
)
return device_entry.id

View File

@@ -16,8 +16,8 @@
"loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [
"async-interrupt==1.1.1",
"aioesphomeapi==18.1.0",
"bluetooth-data-tools==1.13.0",
"aioesphomeapi==18.2.4",
"bluetooth-data-tools==1.14.0",
"esphome-dashboard-api==1.2.3"
],
"zeroconf": ["_esphomelib._tcp.local."]

View File

@@ -487,6 +487,18 @@ class EvoBroker:
)
self.temps = None # these are now stale, will fall back to v2 temps
except KeyError as err:
_LOGGER.warning(
(
"Unable to obtain high-precision temperatures. "
"It appears the JSON schema is not as expected, "
"so the high-precision feature will be disabled until next restart."
"Message is: %s"
),
err,
)
self.client_v1 = self.temps = None
else:
if (
str(self.client_v1.location_id)
@@ -495,7 +507,7 @@ class EvoBroker:
_LOGGER.warning(
"The v2 API's configured location doesn't match "
"the v1 API's default location (there is more than one location), "
"so the high-precision feature will be disabled"
"so the high-precision feature will be disabled until next restart"
)
self.client_v1 = self.temps = None
else:

View File

@@ -3,14 +3,24 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation import toggle_entity
from homeassistant.components.device_automation import (
async_validate_entity_schema,
toggle_entity,
)
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
_ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
async def async_validate_action_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return async_validate_entity_schema(hass, config, _ACTION_SCHEMA)
async def async_get_actions(

View File

@@ -59,13 +59,16 @@ class FitbitOAuth2Implementation(AuthImplementation):
resp = await session.post(self.token_url, data=data, headers=self._headers)
resp.raise_for_status()
except aiohttp.ClientResponseError as err:
error_body = await resp.text()
_LOGGER.debug("Client response error body: %s", error_body)
if _LOGGER.isEnabledFor(logging.DEBUG):
error_body = await resp.text() if not session.closed else ""
_LOGGER.debug(
"Client response error status=%s, body=%s", err.status, error_body
)
if err.status == HTTPStatus.UNAUTHORIZED:
raise FitbitAuthException from err
raise FitbitApiException from err
raise FitbitAuthException(f"Unauthorized error: {err}") from err
raise FitbitApiException(f"Server error response: {err}") from err
except aiohttp.ClientError as err:
raise FitbitApiException from err
raise FitbitApiException(f"Client connection error: {err}") from err
return cast(dict, await resp.json())
@property

View File

@@ -53,6 +53,21 @@ class OAuth2FlowHandler(
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
async def async_step_creation(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Create config entry from external data with Fitbit specific error handling."""
try:
return await super().async_step_creation()
except FitbitAuthException as err:
_LOGGER.error(
"Failed to authenticate when creating Fitbit credentials: %s", err
)
return self.async_abort(reason="invalid_auth")
except FitbitApiException as err:
_LOGGER.error("Failed to create Fitbit credentials: %s", err)
return self.async_abort(reason="cannot_connect")
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
"""Create an entry for the flow, or update existing entry."""

View File

@@ -8,6 +8,8 @@ import logging
import os
from typing import Any, Final, cast
from fitbit import Fitbit
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
import voluptuous as vol
from homeassistant.components.application_credentials import (
@@ -567,34 +569,54 @@ async def async_setup_platform(
if config_file is not None:
_LOGGER.debug("Importing existing fitbit.conf application credentials")
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
),
# Refresh the token before importing to ensure it is working and not
# expired on first initialization.
authd_client = Fitbit(
config_file[CONF_CLIENT_ID],
config_file[CONF_CLIENT_SECRET],
access_token=config_file[ATTR_ACCESS_TOKEN],
refresh_token=config_file[ATTR_REFRESH_TOKEN],
expires_at=config_file[ATTR_LAST_SAVED_AT],
refresh_cb=lambda x: None,
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
"auth_implementation": DOMAIN,
CONF_TOKEN: {
ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN],
ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN],
"expires_at": config_file[ATTR_LAST_SAVED_AT],
},
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
},
)
translation_key = "deprecated_yaml_import"
if (
result.get("type") == FlowResultType.ABORT
and result.get("reason") == "cannot_connect"
):
try:
updated_token = await hass.async_add_executor_job(
authd_client.client.refresh_token
)
except OAuth2Error as err:
_LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err)
translation_key = "deprecated_yaml_import_issue_cannot_connect"
else:
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
),
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
"auth_implementation": DOMAIN,
CONF_TOKEN: {
ATTR_ACCESS_TOKEN: updated_token[ATTR_ACCESS_TOKEN],
ATTR_REFRESH_TOKEN: updated_token[ATTR_REFRESH_TOKEN],
"expires_at": updated_token["expires_at"],
"scope": " ".join(updated_token.get("scope", [])),
},
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
},
)
translation_key = "deprecated_yaml_import"
if (
result.get("type") == FlowResultType.ABORT
and result.get("reason") == "cannot_connect"
):
translation_key = "deprecated_yaml_import_issue_cannot_connect"
else:
translation_key = "deprecated_yaml_no_import"

View File

@@ -16,9 +16,10 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"wrong_account": "The user credentials provided do not match this Fitbit account."

View File

@@ -139,9 +139,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
return self._device_information["fwVersion"]
@property
def serial_number(self) -> str:
def serial_number(self) -> str | None:
"""Return the serial number for the device."""
return self._device_information["serialNumber"]
return self._device_information.get("serialNumber")
@property
def pending_info_alerts_count(self) -> int:

View File

@@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import (
@@ -204,7 +204,7 @@ class FroniusSolarNet:
# Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
if self.config_entry.state == ConfigEntryState.LOADED:
dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
_LOGGER.debug(
"New inverter added (UID: %s)",

View File

@@ -661,7 +661,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
if new_value is None:
return self.entity_description.default_value
if self.entity_description.invalid_when_falsy and not new_value:
raise ValueError(f"Ignoring zero value for {self.entity_id}.")
return None
if isinstance(new_value, float):
return round(new_value, 4)
return new_value
@@ -671,10 +671,9 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
"""Handle updated data from the coordinator."""
try:
self._attr_native_value = self._get_entity_value()
except (KeyError, ValueError):
except KeyError:
# sets state to `None` if no default_value is defined in entity description
# KeyError: raised when omitted in response - eg. at night when no production
# ValueError: raised when invalid zero value received
self._attr_native_value = self.entity_description.default_value
self.async_write_ha_state()

View File

@@ -388,6 +388,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Can be removed in 2023
hass.http.register_redirect("/config/server_control", "/developer-tools/yaml")
# Shopping list panel was replaced by todo panel in 2023.11
hass.http.register_redirect("/shopping-list", "/todo")
hass.http.app.router.register_resource(IndexView(repo_path, hass))
async_register_built_in_panel(hass, "profile")

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20231025.1"]
"requirements": ["home-assistant-frontend==20231030.2"]
}

View File

@@ -88,7 +88,6 @@ DESCRIPTIONS = (
GardenaBluetoothSensorEntityDescription(
key=Sensor.measurement_timestamp.uuid,
translation_key="sensor_measurement_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
char=Sensor.measurement_timestamp,

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/geniushub",
"iot_class": "local_polling",
"loggers": ["geniushubclient"],
"requirements": ["geniushub-client==0.7.0"]
"requirements": ["geniushub-client==0.7.1"]
}

View File

@@ -1,12 +1,9 @@
"""Support for Google Mail."""
from __future__ import annotations
from aiohttp.client_exceptions import ClientError, ClientResponseError
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
@@ -35,16 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(session)
try:
await auth.check_and_refresh_token()
except ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
) from err
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
await auth.check_and_refresh_token()
hass.data[DOMAIN][entry.entry_id] = auth
hass.async_create_task(

View File

@@ -1,9 +1,16 @@
"""API for Google Mail bound to Home Assistant OAuth."""
from aiohttp.client_exceptions import ClientError, ClientResponseError
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import Resource, build
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import config_entry_oauth2_flow
@@ -24,14 +31,30 @@ class AsyncConfigEntryAuth:
async def check_and_refresh_token(self) -> str:
"""Check the token."""
await self.oauth_session.async_ensure_token_valid()
try:
await self.oauth_session.async_ensure_token_valid()
except (RefreshError, ClientResponseError, ClientError) as ex:
if (
self.oauth_session.config_entry.state
is ConfigEntryState.SETUP_IN_PROGRESS
):
if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
) from ex
raise ConfigEntryNotReady from ex
if (
isinstance(ex, RefreshError)
or hasattr(ex, "status")
and ex.status == 400
):
self.oauth_session.config_entry.async_start_reauth(
self.oauth_session.hass
)
raise HomeAssistantError(ex) from ex
return self.access_token
async def get_resource(self) -> Resource:
"""Get current resource."""
try:
credentials = Credentials(await self.check_and_refresh_token())
except RefreshError as ex:
self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass)
raise ex
credentials = Credentials(await self.check_and_refresh_token())
return build("gmail", "v1", credentials=credentials)

View File

@@ -2,6 +2,13 @@
import logging
from typing import Any
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import HttpRequest
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, OAUTH2_SCOPES
@@ -28,3 +35,24 @@ class OAuth2FlowHandler(
"access_type": "offline",
"prompt": "consent",
}
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
"""Create an entry for the flow."""
try:
resource = build(
"tasks",
"v1",
credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]),
)
cmd: HttpRequest = resource.tasklists().list()
await self.hass.async_add_executor_job(cmd.execute)
except HttpError as ex:
error = ex.reason
return self.async_abort(
reason="access_not_configured",
description_placeholders={"message": error},
)
except Exception as ex: # pylint: disable=broad-except
self.logger.exception("Unknown error occurred: %s", ex)
return self.async_abort(reason="unknown")
return self.async_create_entry(title=self.flow_impl.name, data=data)

View File

@@ -15,7 +15,9 @@
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"access_not_configured": "Unable to access the Google API:\n\n{message}",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"

View File

@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["pyhap"],
"requirements": [
"HAP-python==4.9.0",
"HAP-python==4.9.1",
"fnv-hash-fast==0.5.0",
"PyQRCode==1.2.1",
"base36==0.1.1"

View File

@@ -884,7 +884,9 @@ class HKDevice:
self._config_changed_callbacks.add(callback_)
return partial(self._remove_config_changed_callback, callback_)
async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
async def get_characteristics(
self, *args: Any, **kwargs: Any
) -> dict[tuple[int, int], dict[str, Any]]:
"""Read latest state from homekit accessory."""
return await self.pairing.get_characteristics(*args, **kwargs)

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.0.8"],
"requirements": ["aiohomekit==3.0.9"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -24,7 +24,7 @@ async def async_setup_entry(
class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity):
"""Representation of a identify button."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_category = EntityCategory.CONFIG
_attr_device_class = ButtonDeviceClass.IDENTIFY
def __init__(

View File

@@ -62,7 +62,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
)
self._abort_if_unique_id_configured(updates=user_input)
return self.async_create_entry(
title=f"{device_info.product_name} ({device_info.serial})",
title=f"{device_info.product_name}",
data=user_input,
)
@@ -121,14 +121,18 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {"base": ex.error_code}
else:
return self.async_create_entry(
title=f"{self.discovery.product_name} ({self.discovery.serial})",
title=self.discovery.product_name,
data={CONF_IP_ADDRESS: self.discovery.ip},
)
self._set_confirm_only()
self.context["title_placeholders"] = {
"name": f"{self.discovery.product_name} ({self.discovery.serial})"
}
# We won't be adding mac/serial to the title for devices
# that users generally don't have multiple of.
name = self.discovery.product_name
if self.discovery.product_type not in ["HWE-P1", "HWE-WTR"]:
name = f"{name} ({self.discovery.serial})"
self.context["title_placeholders"] = {"name": name}
return self.async_show_form(
step_id="discovery_confirm",

View File

@@ -28,18 +28,23 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
meter_data = {
"device": asdict(coordinator.data.device),
"data": asdict(coordinator.data.data),
"state": asdict(coordinator.data.state)
if coordinator.data.state is not None
else None,
"system": asdict(coordinator.data.system)
if coordinator.data.system is not None
else None,
}
state: dict[str, Any] | None = None
if coordinator.data.state:
state = asdict(coordinator.data.state)
return {
"entry": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(meter_data, TO_REDACT),
}
system: dict[str, Any] | None = None
if coordinator.data.system:
system = asdict(coordinator.data.system)
return async_redact_data(
{
"entry": async_redact_data(entry.data, TO_REDACT),
"data": {
"device": asdict(coordinator.data.device),
"data": asdict(coordinator.data.data),
"state": state,
"system": system,
},
},
TO_REDACT,
)

View File

@@ -18,17 +18,13 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]):
"""Initialize the HomeWizard entity."""
super().__init__(coordinator=coordinator)
self._attr_device_info = DeviceInfo(
name=coordinator.entry.title,
manufacturer="HomeWizard",
sw_version=coordinator.data.device.firmware_version,
model=coordinator.data.device.product_type,
)
if coordinator.data.device.serial is not None:
if (serial_number := coordinator.data.device.serial) is not None:
self._attr_device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, coordinator.data.device.serial)
}
self._attr_device_info[ATTR_IDENTIFIERS] = {
(DOMAIN, coordinator.data.device.serial)
(CONNECTION_NETWORK_MAC, serial_number)
}
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)}

View File

@@ -47,13 +47,17 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity):
await self.coordinator.api.state_set(brightness=int(value * (255 / 100)))
await self.coordinator.async_refresh()
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.data.state is not None
@property
def native_value(self) -> float | None:
"""Return the current value."""
if (
self.coordinator.data.state is None
or self.coordinator.data.state.brightness is None
not self.coordinator.data.state
or (brightness := self.coordinator.data.state.brightness) is None
):
return None
brightness: float = self.coordinator.data.state.brightness
return round(brightness * (100 / 255))

View File

@@ -38,6 +38,7 @@ from .const import (
CONF_COOL_AWAY_TEMPERATURE,
CONF_HEAT_AWAY_TEMPERATURE,
DOMAIN,
RETRY,
)
ATTR_FAN_ACTION = "fan_action"
@@ -155,6 +156,7 @@ class HoneywellUSThermostat(ClimateEntity):
self._cool_away_temp = cool_away_temp
self._heat_away_temp = heat_away_temp
self._away = False
self._retry = 0
self._attr_unique_id = device.deviceid
@@ -351,6 +353,11 @@ class HoneywellUSThermostat(ClimateEntity):
if mode == "heat":
await self._device.set_setpoint_heat(temperature)
except UnexpectedResponse as err:
raise HomeAssistantError(
"Honeywell set temperature failed: Invalid Response"
) from err
except SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
raise ValueError(
@@ -367,6 +374,11 @@ class HoneywellUSThermostat(ClimateEntity):
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
await self._device.set_setpoint_heat(temperature)
except UnexpectedResponse as err:
raise HomeAssistantError(
"Honeywell set temperature failed: Invalid Response"
) from err
except SomeComfortError as err:
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
raise ValueError(
@@ -483,21 +495,28 @@ class HoneywellUSThermostat(ClimateEntity):
try:
await self._device.refresh()
self._attr_available = True
self._retry = 0
except UnauthorizedError:
try:
await self._data.client.login()
await self._device.refresh()
self._attr_available = True
self._retry = 0
except (
SomeComfortError,
ClientConnectionError,
asyncio.TimeoutError,
):
self._attr_available = False
self._retry += 1
if self._retry > RETRY:
self._attr_available = False
except (ClientConnectionError, asyncio.TimeoutError):
self._attr_available = False
self._retry += 1
if self._retry > RETRY:
self._attr_available = False
except UnexpectedResponse:
pass

View File

@@ -10,3 +10,4 @@ DEFAULT_HEAT_AWAY_TEMPERATURE = 61
CONF_DEV_ID = "thermostat"
CONF_LOC_ID = "location"
_LOGGER = logging.getLogger(__name__)
RETRY = 3

View File

@@ -146,7 +146,7 @@ SENSORS_INFO = [
translation_key="energy_today",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
key=SOURCE_TYPE_ELECTRICITY,
sensor_type=SENSOR_TYPE_THIS_DAY,
precision=1,
@@ -156,7 +156,7 @@ SENSORS_INFO = [
translation_key="energy_week",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
key=SOURCE_TYPE_ELECTRICITY,
sensor_type=SENSOR_TYPE_THIS_WEEK,
precision=1,
@@ -166,7 +166,7 @@ SENSORS_INFO = [
translation_key="energy_month",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
key=SOURCE_TYPE_ELECTRICITY,
sensor_type=SENSOR_TYPE_THIS_MONTH,
precision=1,
@@ -176,7 +176,7 @@ SENSORS_INFO = [
translation_key="energy_year",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
key=SOURCE_TYPE_ELECTRICITY,
sensor_type=SENSOR_TYPE_THIS_YEAR,
precision=1,
@@ -197,7 +197,7 @@ SENSORS_INFO = [
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
key=SOURCE_TYPE_GAS,
sensor_type=SENSOR_TYPE_THIS_DAY,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
icon="mdi:counter",
precision=1,
),
@@ -207,7 +207,7 @@ SENSORS_INFO = [
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
key=SOURCE_TYPE_GAS,
sensor_type=SENSOR_TYPE_THIS_WEEK,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
icon="mdi:counter",
precision=1,
),
@@ -217,7 +217,7 @@ SENSORS_INFO = [
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
key=SOURCE_TYPE_GAS,
sensor_type=SENSOR_TYPE_THIS_MONTH,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
icon="mdi:counter",
precision=1,
),
@@ -227,7 +227,7 @@ SENSORS_INFO = [
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
key=SOURCE_TYPE_GAS,
sensor_type=SENSOR_TYPE_THIS_YEAR,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
icon="mdi:counter",
precision=1,
),

View File

@@ -2,7 +2,6 @@
from pydrawise import legacy
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@@ -13,11 +12,10 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
from .const import DOMAIN, SCAN_INTERVAL
from .coordinator import HydrawiseDataUpdateCoordinator
CONFIG_SCHEMA = vol.Schema(
@@ -53,24 +51,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Hydrawise from a config entry."""
access_token = config_entry.data[CONF_API_KEY]
try:
hydrawise = await hass.async_add_executor_job(
legacy.LegacyHydrawise, access_token
)
except (ConnectTimeout, HTTPError) as ex:
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
raise ConfigEntryNotReady(
f"Unable to connect to Hydrawise cloud service: {ex}"
) from ex
hass.data.setdefault(DOMAIN, {})[
config_entry.entry_id
] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
if not hydrawise.controller_info or not hydrawise.controller_status:
raise ConfigEntryNotReady("Hydrawise data not loaded")
# NOTE: We don't need to call async_config_entry_first_refresh() because
# data is fetched when the Hydrawiser object is instantiated.
hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False)
coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True

View File

@@ -12,12 +12,12 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN, LOGGER
from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
@@ -95,13 +95,10 @@ async def async_setup_entry(
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""A sensor implementation for Hydrawise device."""
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
def _update_attrs(self) -> None:
"""Update state attributes."""
if self.entity_description.key == "status":
self._attr_is_on = self.coordinator.last_update_success
elif self.entity_description.key == "is_watering":
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
self._attr_is_on = relay_data["timestr"] == "Now"
super()._handle_coordinator_update()

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -36,3 +37,14 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
name=data["name"],
manufacturer=MANUFACTURER,
)
self._update_attrs()
def _update_attrs(self) -> None:
"""Update state attributes."""
return # pragma: no cover
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
self._update_attrs()
super()._handle_coordinator_update()

View File

@@ -11,13 +11,13 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import DOMAIN, LOGGER
from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
@@ -82,10 +82,8 @@ async def async_setup_entry(
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""A sensor implementation for Hydrawise device."""
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the states."""
LOGGER.debug("Updating Hydrawise sensor: %s", self.name)
def _update_attrs(self) -> None:
"""Update state attributes."""
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
if self.entity_description.key == "watering_time":
if relay_data["timestr"] == "Now":
@@ -94,8 +92,6 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
self._attr_native_value = 0
else: # _sensor_type == 'next_cycle'
next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS)
LOGGER.debug("New cycle time: %s", next_cycle)
self._attr_native_value = dt_util.utc_from_timestamp(
dt_util.as_timestamp(dt_util.now()) + next_cycle
)
super()._handle_coordinator_update()

View File

@@ -13,7 +13,7 @@ from homeassistant.components.switch import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -23,7 +23,6 @@ from .const import (
CONF_WATERING_TIME,
DEFAULT_WATERING_TIME,
DOMAIN,
LOGGER,
)
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
@@ -124,14 +123,11 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
elif self.entity_description.key == "auto_watering":
self.coordinator.api.suspend_zone(365, zone_number)
@callback
def _handle_coordinator_update(self) -> None:
"""Update device state."""
def _update_attrs(self) -> None:
"""Update state attributes."""
zone_number = self.data["relay"]
LOGGER.debug("Updating Hydrawise switch: %s", self.name)
timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"]
if self.entity_description.key == "manual_watering":
self._attr_is_on = timestr == "Now"
elif self.entity_description.key == "auto_watering":
self._attr_is_on = timestr not in {"", "Now"}
super()._handle_coordinator_update()

View File

@@ -78,7 +78,7 @@ class IslamicPrayerTimeSensor(
"""Initialize the Islamic prayer time sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = description.key
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=NAME,

View File

@@ -122,12 +122,12 @@ class KNXExposeSensor:
"""Extract value from state."""
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
value = self.expose_default
elif self.expose_attribute is not None:
_attr = state.attributes.get(self.expose_attribute)
value = _attr if _attr is not None else self.expose_default
else:
value = (
state.state
if self.expose_attribute is None
else state.attributes.get(self.expose_attribute, self.expose_default)
)
value = state.state
if self.expose_type == "binary":
if value in (1, STATE_ON, "True"):
return True

View File

@@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.11.2",
"xknxproject==3.3.0",
"xknxproject==3.4.0",
"knx-frontend==2023.6.23.191712"
]
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.14.0", "ld2410-ble==0.1.1"]
}

View File

@@ -32,5 +32,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"]
"requirements": ["bluetooth-data-tools==1.14.0", "led-ble==1.0.1"]
}

View File

@@ -149,31 +149,29 @@ async def _async_reproduce_state(
service = SERVICE_TURN_ON
for attr in ATTR_GROUP:
# All attributes that are not colors
if attr in state.attributes:
service_data[attr] = state.attributes[attr]
if (attr_state := state.attributes.get(attr)) is not None:
service_data[attr] = attr_state
if (
state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN)
!= ColorMode.UNKNOWN
):
color_mode = state.attributes[ATTR_COLOR_MODE]
if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
if color_mode_attr.state_attr not in state.attributes:
if cm_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
if (cm_attr_state := state.attributes.get(cm_attr.state_attr)) is None:
_LOGGER.warning(
"Color mode %s specified but attribute %s missing for: %s",
color_mode,
color_mode_attr.state_attr,
cm_attr.state_attr,
state.entity_id,
)
return
service_data[color_mode_attr.parameter] = state.attributes[
color_mode_attr.state_attr
]
service_data[cm_attr.parameter] = cm_attr_state
else:
# Fall back to Choosing the first color that is specified
for color_attr in COLOR_GROUP:
if color_attr in state.attributes:
service_data[color_attr] = state.attributes[color_attr]
if (color_attr_state := state.attributes.get(color_attr)) is not None:
service_data[color_attr] = color_attr_state
break
elif state.state == STATE_OFF:

View File

@@ -139,20 +139,28 @@ class LocalTodoListEntity(TodoListEntity):
await self._async_save()
await self.async_update_ha_state(force_refresh=True)
async def async_move_todo_item(self, uid: str, pos: int) -> None:
async def async_move_todo_item(
self, uid: str, previous_uid: str | None = None
) -> None:
"""Re-order an item to the To-do list."""
if uid == previous_uid:
return
todos = self._calendar.todos
found_item: Todo | None = None
for idx, itm in enumerate(todos):
if itm.uid == uid:
found_item = itm
todos.pop(idx)
break
if found_item is None:
item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
if uid not in item_idx:
raise HomeAssistantError(
f"Item '{uid}' not found in todo list {self.entity_id}"
"Item '{uid}' not found in todo list {self.entity_id}"
)
todos.insert(pos, found_item)
if previous_uid and previous_uid not in item_idx:
raise HomeAssistantError(
"Item '{previous_uid}' not found in todo list {self.entity_id}"
)
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
src_idx = item_idx[uid]
src_item = todos.pop(src_idx)
if dst_idx > src_idx:
dst_idx -= 1
todos.insert(dst_idx, src_item)
await self._async_save()
await self.async_update_ha_state(force_refresh=True)

View File

@@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import webhook
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_NAME, CONF_WEBHOOK_ID
from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
@@ -95,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# Check if already exists
await self.async_set_unique_id(lock_data["bridge_mac_wifi"])
self._abort_if_unique_id_configured({CONF_HOST: host})
self._abort_if_unique_id_configured({"bridge_ip": host})
return await self.async_step_user()

View File

@@ -60,6 +60,10 @@ async def websocket_lovelace_resources(
"""Send Lovelace UI resources over WebSocket configuration."""
resources = hass.data[DOMAIN]["resources"]
if hass.config.safe_mode:
connection.send_result(msg["id"], [])
return
if not resources.loaded:
await resources.async_load()
resources.loaded = True

View File

@@ -28,9 +28,10 @@ def setup_platform(
data = hass.data[LUPUSEC_DOMAIN]
devices = []
device_types = [CONST.TYPE_SWITCH]
for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH):
devices = []
for device in data.lupusec.get_devices(generic_type=device_types):
devices.append(LupusecSwitch(data, device))
add_entities(devices)

View File

@@ -1,12 +1,15 @@
"""Matter lock."""
from __future__ import annotations
from enum import IntFlag
from typing import Any
from chip.clusters import Objects as clusters
from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.components.lock import (
LockEntity,
LockEntityDescription,
LockEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CODE, Platform
from homeassistant.core import HomeAssistant, callback
@@ -17,6 +20,8 @@ from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
DoorLockFeature = clusters.DoorLock.Bitmaps.Feature
async def async_setup_entry(
hass: HomeAssistant,
@@ -61,6 +66,14 @@ class MatterLock(MatterEntity, LockEntity):
return bool(self.features & DoorLockFeature.kDoorPositionSensor)
@property
def supports_unbolt(self) -> bool:
"""Return True if the lock supports unbolt."""
if self.features is None:
return False
return bool(self.features & DoorLockFeature.kUnbolt)
async def send_device_command(
self,
command: clusters.ClusterCommand,
@@ -92,6 +105,25 @@ class MatterLock(MatterEntity, LockEntity):
self._lock_option_default_code,
)
code_bytes = code.encode() if code else None
if self.supports_unbolt:
# if the lock reports it has separate unbolt support,
# the unlock command should unbolt only on the unlock command
# and unlatch on the HA 'open' command.
await self.send_device_command(
command=clusters.DoorLock.Commands.UnboltDoor(code_bytes)
)
else:
await self.send_device_command(
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
)
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
code: str = kwargs.get(
ATTR_CODE,
self._lock_option_default_code,
)
code_bytes = code.encode() if code else None
await self.send_device_command(
command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
)
@@ -104,6 +136,8 @@ class MatterLock(MatterEntity, LockEntity):
self.features = int(
self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
)
if self.supports_unbolt:
self._attr_supported_features = LockEntityFeature.OPEN
lock_state = self.get_matter_attribute_value(
clusters.DoorLock.Attributes.LockState
@@ -144,26 +178,6 @@ class MatterLock(MatterEntity, LockEntity):
)
class DoorLockFeature(IntFlag):
"""Temp enum that represents the features of a door lock.
Should be replaced by the library provided one once that is released.
"""
kPinCredential = 0x1 # noqa: N815
kRfidCredential = 0x2 # noqa: N815
kFingerCredentials = 0x4 # noqa: N815
kLogging = 0x8 # noqa: N815
kWeekDayAccessSchedules = 0x10 # noqa: N815
kDoorPositionSensor = 0x20 # noqa: N815
kFaceCredentials = 0x40 # noqa: N815
kCredentialsOverTheAirAccess = 0x80 # noqa: N815
kUser = 0x100 # noqa: N815
kNotification = 0x200 # noqa: N815
kYearDayAccessSchedules = 0x400 # noqa: N815
kHolidaySchedules = 0x800 # noqa: N815
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.LOCK,

View File

@@ -7,5 +7,5 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp==2023.9.24"]
"requirements": ["yt-dlp==2023.10.13"]
}

View File

@@ -20,6 +20,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -68,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
await cleanup_old_device(hass)
return True
@@ -88,6 +91,15 @@ async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry):
await hass.config_entries.async_reload(config_entry.entry_id)
async def cleanup_old_device(hass: HomeAssistant) -> None:
"""Cleanup device without proper device identifier."""
device_reg = dr.async_get(hass)
device = device_reg.async_get_device(identifiers={(DOMAIN,)}) # type: ignore[arg-type]
if device:
_LOGGER.debug("Removing improper device %s", device.name)
device_reg.async_remove_device(device.id)
class CannotConnect(HomeAssistantError):
"""Unable to connect to the web site."""

View File

@@ -31,13 +31,21 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import entity_registry as er, sun
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import MetDataUpdateCoordinator
from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP
from .const import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
ATTR_MAP,
CONDITIONS_MAP,
CONF_TRACK_HOME,
DOMAIN,
FORECAST_MAP,
)
DEFAULT_NAME = "Met.no"
@@ -60,7 +68,7 @@ async def async_setup_entry(
if TYPE_CHECKING:
assert isinstance(name, str)
entities = [MetWeather(coordinator, config_entry.data, False, name, is_metric)]
entities = [MetWeather(coordinator, config_entry, False, name, is_metric)]
# Add hourly entity to legacy config entries
if entity_registry.async_get_entity_id(
@@ -69,9 +77,7 @@ async def async_setup_entry(
_calculate_unique_id(config_entry.data, True),
):
name = f"{name} hourly"
entities.append(
MetWeather(coordinator, config_entry.data, True, name, is_metric)
)
entities.append(MetWeather(coordinator, config_entry, True, name, is_metric))
async_add_entities(entities)
@@ -114,22 +120,22 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
def __init__(
self,
coordinator: MetDataUpdateCoordinator,
config: MappingProxyType[str, Any],
config_entry: ConfigEntry,
hourly: bool,
name: str,
is_metric: bool,
) -> None:
"""Initialise the platform with a data instance and site."""
super().__init__(coordinator)
self._attr_unique_id = _calculate_unique_id(config, hourly)
self._config = config
self._attr_unique_id = _calculate_unique_id(config_entry.data, hourly)
self._config = config_entry.data
self._is_metric = is_metric
self._hourly = hourly
self._attr_entity_registry_enabled_default = not hourly
self._attr_device_info = DeviceInfo(
name="Forecast",
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer="Met.no",
model="Forecast",
configuration_url="https://www.met.no/en",
@@ -143,6 +149,10 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
condition = self.coordinator.data.current_weather_data.get("condition")
if condition is None:
return None
if condition == ATTR_CONDITION_SUNNY and not sun.is_up(self.hass):
condition = ATTR_CONDITION_CLEAR_NIGHT
return format_condition(condition)
@property

View File

@@ -168,6 +168,7 @@ class SlaveSensor(
self._attr_unique_id = f"{self._attr_unique_id}_{idx}"
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = entry.get(CONF_STATE_CLASS)
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
self._attr_available = False
super().__init__(coordinator)

View File

@@ -63,8 +63,8 @@ PARM_IS_LEGAL = namedtuple(
],
)
# PARM_IS_LEGAL defines if the keywords:
# count: ..
# structure: ..
# count:
# structure:
# swap: byte
# swap: word
# swap: word_byte (identical to swap: word)
@@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)),
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, True, False)),
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
}

View File

@@ -47,6 +47,7 @@ from .client import ( # noqa: F401
publish,
subscribe,
)
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA # noqa: F401
from .config_integration import CONFIG_SCHEMA_BASE
from .const import ( # noqa: F401
ATTR_PAYLOAD,
@@ -232,7 +233,7 @@ async def async_check_config_schema(
) -> None:
"""Validate manually configured MQTT items."""
mqtt_data = get_mqtt_data(hass)
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN]
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml.get(DOMAIN, {})
for mqtt_config_item in mqtt_config:
for domain, config_items in mqtt_config_item.items():
schema = mqtt_data.reload_schema[domain]

View File

@@ -232,16 +232,16 @@ TOPIC_KEYS = (
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the preset mode reset payload is not one of the preset modes."""
if PRESET_NONE in config[CONF_PRESET_MODES_LIST]:
raise ValueError("preset_modes must not include preset mode 'none'")
raise vol.Invalid("preset_modes must not include preset mode 'none'")
return config
def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
"""Validate a target_humidity range configuration, throws otherwise."""
if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]:
raise ValueError("target_humidity_max must be > target_humidity_min")
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
if config[CONF_HUMIDITY_MAX] > 100:
raise ValueError("max_humidity must be <= 100")
raise vol.Invalid("max_humidity must be <= 100")
return config

View File

@@ -116,16 +116,16 @@ _LOGGER = logging.getLogger(__name__)
def valid_speed_range_configuration(config: ConfigType) -> ConfigType:
"""Validate that the fan speed_range configuration is valid, throws if it isn't."""
if config[CONF_SPEED_RANGE_MIN] == 0:
raise ValueError("speed_range_min must be > 0")
raise vol.Invalid("speed_range_min must be > 0")
if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]:
raise ValueError("speed_range_max must be > speed_range_min")
raise vol.Invalid("speed_range_max must be > speed_range_min")
return config
def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the preset mode reset payload is not one of the preset modes."""
if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]:
raise ValueError("preset_modes must not contain payload_reset_preset_mode")
raise vol.Invalid("preset_modes must not contain payload_reset_preset_mode")
return config

View File

@@ -102,7 +102,7 @@ _LOGGER = logging.getLogger(__name__)
def valid_mode_configuration(config: ConfigType) -> ConfigType:
"""Validate that the mode reset payload is not one of the available modes."""
if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]:
raise ValueError("modes must not contain payload_reset_mode")
raise vol.Invalid("modes must not contain payload_reset_mode")
return config
@@ -113,9 +113,9 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
throws if it isn't.
"""
if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]:
raise ValueError("target_humidity_max must be > target_humidity_min")
raise vol.Invalid("target_humidity_max must be > target_humidity_min")
if config[CONF_TARGET_HUMIDITY_MAX] > 100:
raise ValueError("max_humidity must be <= 100")
raise vol.Invalid("max_humidity must be <= 100")
return config

View File

@@ -9,7 +9,6 @@ import logging
from typing import TYPE_CHECKING, Any, Protocol, cast, final
import voluptuous as vol
import yaml
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -28,6 +27,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
EntityCategory,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import (
@@ -63,6 +63,7 @@ from homeassistant.helpers.typing import (
UndefinedType,
)
from homeassistant.util.json import json_loads
from homeassistant.util.yaml import dump as yaml_dump
from . import debug_info, subscription
from .client import async_publish
@@ -207,6 +208,16 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType
)
def validate_sensor_entity_category(config: ConfigType) -> ConfigType:
"""Check the sensor's entity category is not set to `config` which is invalid for sensors."""
if (
CONF_ENTITY_CATEGORY in config
and config[CONF_ENTITY_CATEGORY] == EntityCategory.CONFIG
):
raise vol.Invalid("Entity category `config` is invalid")
return config
MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(
cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE),
vol.Schema(
@@ -404,8 +415,8 @@ async def async_setup_entity_entry_helper(
error = str(ex)
config_file = getattr(yaml_config, "__config_file__", "?")
line = getattr(yaml_config, "__line__", "?")
issue_id = hex(hash(frozenset(yaml_config.items())))
yaml_config_str = yaml.dump(dict(yaml_config))
issue_id = hex(hash(frozenset(yaml_config)))
yaml_config_str = yaml_dump(yaml_config)
learn_more_url = (
f"https://www.home-assistant.io/integrations/{domain}.mqtt/"
)
@@ -427,7 +438,7 @@ async def async_setup_entity_entry_helper(
translation_key="invalid_platform_config",
)
_LOGGER.error(
"%s for manual configured MQTT %s item, in %s, line %s Got %s",
"%s for manually configured MQTT %s item, in %s, line %s Got %s",
error,
domain,
config_file,

View File

@@ -44,6 +44,7 @@ from .mixins import (
MqttAvailability,
MqttEntity,
async_setup_entity_entry_helper,
validate_sensor_entity_category,
write_state_on_attr_change,
)
from .models import (
@@ -70,7 +71,6 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_FORCE_UPDATE = False
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
@@ -88,6 +88,7 @@ PLATFORM_SCHEMA_MODERN = vol.All(
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
# Removed in HA Core 2023.6.0
cv.removed(CONF_LAST_RESET_TOPIC),
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE,
)
@@ -95,6 +96,7 @@ DISCOVERY_SCHEMA = vol.All(
# Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840
# Removed in HA Core 2023.6.0
cv.removed(CONF_LAST_RESET_TOPIC),
validate_sensor_entity_category,
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
)

View File

@@ -181,7 +181,7 @@
},
"qos": {
"name": "QoS",
"description": "Quality of Service to use. O. At most once. 1: At least once. 2: Exactly once."
"description": "Quality of Service to use. 0: At most once. 1: At least once. 2: Exactly once."
},
"retain": {
"name": "Retain",

View File

@@ -71,9 +71,9 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset(
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
"""Validate that the text length configuration is valid, throws if it isn't."""
if config[CONF_MIN] >= config[CONF_MAX]:
raise ValueError("text length min must be >= max")
raise vol.Invalid("text length min must be >= max")
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
return config

View File

@@ -47,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
).extend(mqtt.config.MQTT_RO_SCHEMA.schema)
).extend(mqtt.MQTT_RO_SCHEMA.schema)
@lru_cache(maxsize=256)

View File

@@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==3.0.2"]
"requirements": ["google-nest-sdm==3.0.3"]
}

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyatmo"],
"requirements": ["pyatmo==7.5.0"]
"requirements": ["pyatmo==7.6.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling",
"loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==1.0.0"]
"requirements": ["py-nextbusnext==1.0.2"]
}

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from aiohttp import BasicAuth
from python_opensky import OpenSky
from python_opensky.exceptions import OpenSkyUnauthenticatedError
from python_opensky.exceptions import OpenSkyError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
),
contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False),
)
except OpenSkyUnauthenticatedError as exc:
except OpenSkyError as exc:
raise ConfigEntryNotReady from exc
coordinator = OpenSkyDataUpdateCoordinator(hass, client)

View File

@@ -23,7 +23,7 @@ from homeassistant.components.recorder.statistics import (
statistics_during_period,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -58,6 +58,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
entry_data.get(CONF_TOTP_SECRET),
)
@callback
def _dummy_listener() -> None:
pass
# Force the coordinator to periodically update by registering at least one listener.
# Needed when the _async_update_data below returns {} for utilities that don't provide
# forecast, which results to no sensors added, no registered listeners, and thus
# _async_update_data not periodically getting called which is needed for _insert_statistics.
self.async_add_listener(_dummy_listener)
async def _async_update_data(
self,
) -> dict[str, Forecast]:
@@ -71,6 +81,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
raise ConfigEntryAuthFailed from err
forecasts: list[Forecast] = await self.api.async_get_forecast()
_LOGGER.debug("Updating sensor data with: %s", forecasts)
# Because Opower provides historical usage/cost with a delay of a couple of days
# we need to insert data into statistics.
await self._insert_statistics()
return {forecast.account.utility_account_id: forecast for forecast in forecasts}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.0.37"]
"requirements": ["opower==0.0.39"]
}

View File

@@ -40,7 +40,7 @@ SELECT_TYPES = (
key="select_schedule",
translation_key="select_schedule",
icon="mdi:calendar-clock",
command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt),
options_key="available_schedules",
),
PlugwiseSelectEntityDescription(

View File

@@ -6,5 +6,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/private_ble_device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.13.0"]
"requirements": ["bluetooth-data-tools==1.14.0"]
}

View File

@@ -70,25 +70,25 @@ def async_setup_proximity_component(
ignored_zones: list[str] = config[CONF_IGNORED_ZONES]
proximity_devices: list[str] = config[CONF_DEVICES]
tolerance: int = config[CONF_TOLERANCE]
proximity_zone = name
proximity_zone = config[CONF_ZONE]
unit_of_measurement: str = config.get(
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
)
zone_id = f"zone.{config[CONF_ZONE]}"
zone_friendly_name = name
proximity = Proximity(
hass,
proximity_zone,
zone_friendly_name,
DEFAULT_DIST_TO_ZONE,
DEFAULT_DIR_OF_TRAVEL,
DEFAULT_NEAREST,
ignored_zones,
proximity_devices,
tolerance,
zone_id,
proximity_zone,
unit_of_measurement,
)
proximity.entity_id = f"{DOMAIN}.{proximity_zone}"
proximity.entity_id = f"{DOMAIN}.{zone_friendly_name}"
proximity.async_write_ha_state()
@@ -171,7 +171,7 @@ class Proximity(Entity):
devices_to_calculate = False
devices_in_zone = ""
zone_state = self.hass.states.get(self.proximity_zone)
zone_state = self.hass.states.get(f"zone.{self.proximity_zone}")
proximity_latitude = (
zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None
)
@@ -189,7 +189,7 @@ class Proximity(Entity):
devices_to_calculate = True
# Check the location of all devices.
if (device_state.state).lower() == (self.friendly_name).lower():
if (device_state.state).lower() == (self.proximity_zone).lower():
device_friendly = device_state.name
if devices_in_zone != "":
devices_in_zone = f"{devices_in_zone}, "

View File

@@ -10,6 +10,7 @@ from typing import Literal
from reolink_aio.api import RETRY_ATTEMPTS
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
from reolink_aio.software_version import NewSoftwareVersion
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
@@ -45,7 +46,9 @@ class ReolinkData:
host: ReolinkHost
device_coordinator: DataUpdateCoordinator[None]
firmware_coordinator: DataUpdateCoordinator[str | Literal[False]]
firmware_coordinator: DataUpdateCoordinator[
str | Literal[False] | NewSoftwareVersion
]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
@@ -86,7 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
await host.renew()
async def async_check_firmware_update() -> str | Literal[False]:
async def async_check_firmware_update() -> str | Literal[
False
] | NewSoftwareVersion:
"""Check for firmware updates."""
if not host.api.supported(None, "update"):
return False
@@ -153,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Update the configuration of the host entity."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@@ -32,7 +32,7 @@ from .entity import ReolinkChannelCoordinatorEntity
class ReolinkBinarySensorEntityDescriptionMixin:
"""Mixin values for Reolink binary sensor entities."""
value: Callable[[Host, int | None], bool]
value: Callable[[Host, int], bool]
@dataclass
@@ -43,7 +43,7 @@ class ReolinkBinarySensorEntityDescription(
icon: str = "mdi:motion-sensor"
icon_off: str = "mdi:motion-sensor-off"
supported: Callable[[Host, int | None], bool] = lambda host, ch: True
supported: Callable[[Host, int], bool] = lambda host, ch: True
BINARY_SENSORS = (
@@ -169,6 +169,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt
)
)
async def _async_handle_event(self, event):
async def _async_handle_event(self, event: str) -> None:
"""Handle incoming event for motion detection."""
self.async_write_ha_state()

View File

@@ -113,7 +113,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
raise AbortFlow("already_configured")
# check if the camera is reachable at the new IP
host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options)
new_config = dict(existing_entry.data)
new_config[CONF_HOST] = discovery_info.ip
host = ReolinkHost(self.hass, new_config, existing_entry.options)
try:
await host.api.get_state("GetLocalLink")
await host.api.logout()

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
from typing import Any, Literal
import aiohttp
from aiohttp.web import Request
@@ -81,7 +81,7 @@ class ReolinkHost:
return self._unique_id
@property
def api(self):
def api(self) -> Host:
"""Return the API object."""
return self._api
@@ -313,7 +313,7 @@ class ReolinkHost:
"""Call the API of the camera device to update the internal states."""
await self._api.get_states()
async def disconnect(self):
async def disconnect(self) -> None:
"""Disconnect from the API, so the connection will be released."""
try:
await self._api.unsubscribe()
@@ -335,7 +335,7 @@ class ReolinkHost:
err,
)
async def _async_start_long_polling(self, initial=False):
async def _async_start_long_polling(self, initial=False) -> None:
"""Start ONVIF long polling task."""
if self._long_poll_task is None:
try:
@@ -364,7 +364,7 @@ class ReolinkHost:
self._lost_subscription = False
self._long_poll_task = asyncio.create_task(self._async_long_polling())
async def _async_stop_long_polling(self):
async def _async_stop_long_polling(self) -> None:
"""Stop ONVIF long polling task."""
if self._long_poll_task is not None:
self._long_poll_task.cancel()
@@ -372,7 +372,7 @@ class ReolinkHost:
await self._api.unsubscribe(sub_type=SubType.long_poll)
async def stop(self, event=None):
async def stop(self, event=None) -> None:
"""Disconnect the API."""
if self._cancel_poll is not None:
self._cancel_poll()
@@ -433,7 +433,7 @@ class ReolinkHost:
else:
self._lost_subscription = False
async def _renew(self, sub_type: SubType) -> None:
async def _renew(self, sub_type: Literal[SubType.push, SubType.long_poll]) -> None:
"""Execute the renew of the subscription."""
if not self._api.subscribed(sub_type):
_LOGGER.debug(
@@ -512,8 +512,10 @@ class ReolinkHost:
_LOGGER.debug("Registered webhook: %s", event_id)
def unregister_webhook(self):
def unregister_webhook(self) -> None:
"""Unregister the webhook for motion events."""
if self.webhook_id is None:
return
_LOGGER.debug("Unregistering webhook %s", self.webhook_id)
webhook.async_unregister(self._hass, self.webhook_id)
self.webhook_id = None

View File

@@ -38,8 +38,8 @@ class ReolinkLightEntityDescription(
"""A class that describes light entities."""
supported_fn: Callable[[Host, int], bool] = lambda api, ch: True
get_brightness_fn: Callable[[Host, int], int] | None = None
set_brightness_fn: Callable[[Host, int, float], Any] | None = None
get_brightness_fn: Callable[[Host, int], int | None] | None = None
set_brightness_fn: Callable[[Host, int, int], Any] | None = None
LIGHT_ENTITIES = (
@@ -127,13 +127,13 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
if self.entity_description.get_brightness_fn is None:
return None
return round(
255
* (
self.entity_description.get_brightness_fn(self._host.api, self._channel)
/ 100.0
)
bright_pct = self.entity_description.get_brightness_fn(
self._host.api, self._channel
)
if bright_pct is None:
return None
return round(255 * bright_pct / 100.0)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn light off."""

View File

@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.7.11"]
"requirements": ["reolink-aio==0.7.14"]
}

View File

@@ -26,7 +26,7 @@ from .entity import ReolinkChannelCoordinatorEntity
class ReolinkNumberEntityDescriptionMixin:
"""Mixin values for Reolink number entities."""
value: Callable[[Host, int], float]
value: Callable[[Host, int], float | None]
method: Callable[[Host, int, float], Any]
@@ -354,7 +354,7 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
)
@property
def native_value(self) -> float:
def native_value(self) -> float | None:
"""State of the number entity."""
return self.entity_description.value(self._host.api, self._channel)

View File

@@ -44,7 +44,7 @@ class ReolinkSensorEntityDescription(
class ReolinkHostSensorEntityDescriptionMixin:
"""Mixin values for Reolink host sensor entities."""
value: Callable[[Host], int]
value: Callable[[Host], int | None]
@dataclass

View File

@@ -35,7 +35,8 @@ async def async_setup_entry(
class ReolinkUpdateEntity(
ReolinkBaseCoordinatorEntity[str | Literal[False]], UpdateEntity
ReolinkBaseCoordinatorEntity[str | Literal[False] | NewSoftwareVersion],
UpdateEntity,
):
"""Update entity for a Netgear device."""

View File

@@ -119,7 +119,7 @@ class IRobotEntity(Entity):
@property
def battery_stats(self):
"""Return the battery stats."""
return self.vacuum_state.get("bbchg3")
return self.vacuum_state.get("bbchg3", {})
@property
def _robot_state(self):

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