Compare commits

...

205 Commits

Author SHA1 Message Date
Franck Nijhof
ef89d1cd3d 2023.11.3 (#104348) 2023-11-22 12:51:36 +01:00
Allen Porter
9c4fd88a3d Bump ical to 6.1.0 (#103759) 2023-11-22 09:45:45 +01:00
Allen Porter
f5783cd3b5 Bump ical to 6.0.0 (#103482) 2023-11-22 09:45:40 +01:00
Franck Nijhof
1200ded24c Bumped version to 2023.11.3 2023-11-22 09:12:24 +01:00
Erik Montnemery
da992e9f45 Bump pychromecast to 13.0.8 (#104320) 2023-11-22 09:11:21 +01:00
Allen Porter
40326385ae Bump pyrainbird to 4.0.1 (#104293) 2023-11-22 09:11:17 +01:00
Richard Kroegel
da04c32893 Bump bimmer_connected to 0.14.3 (#104282)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2023-11-22 09:11:13 +01:00
Jan Bouwhuis
ae2ff926c1 Restore removed guard for non-string inputs in Alexa (#104263) 2023-11-22 09:11:10 +01:00
epenet
a5d48da07a Catch ClientOSError in renault integration (#104248) 2023-11-22 09:11:06 +01:00
Anton Tolchanov
669daabfdb Handle attributes set to None in prometheus (#104247)
Better handle attributes set to None
2023-11-22 09:11:02 +01:00
Jan Bouwhuis
b64ef24f20 Fix mqtt json light allows to set brightness value >255 (#104220) 2023-11-22 09:10:58 +01:00
Jan Bouwhuis
86beb9d135 Fix imap does not decode text body correctly (#104217) 2023-11-22 09:10:06 +01:00
Rene Nemec
64297aeb8f Increase Tomato request timeout (#104203)
* tomato integration timeout fixed

* update tests in tomato integration
2023-11-22 09:10:01 +01:00
mkmer
5650df5cfb Bump aiosomecomfort to 0.0.22 (#104202)
* Bump aiosomecomfort to 0.0.20

* Bump aiosomecomfort to 0.0.22
2023-11-22 09:09:56 +01:00
Allen Porter
83c59d4154 Fix Local To-do list bug renaming items (#104182)
* Fix Local To-do bug renaming items

* Fix renaming
2023-11-22 09:09:53 +01:00
Thomas Schamm
4680ac0cbf Bump boschshcpy to 0.2.75 (#104159)
Bumped to boschshcpy==0.2.75
2023-11-22 09:08:37 +01:00
J. Nick Koston
8b79d38497 Prevent Bluetooth reconnects from blocking shutdown (#104150) 2023-11-22 09:08:33 +01:00
J. Nick Koston
35b1051c67 Add debug logging for which adapter is used to connect bluetooth devices (#103264)
Log which adapter is used to connect bluetooth devices

This is a debug logging improvement to help users find problems
with their setup
2023-11-22 09:08:28 +01:00
J. Nick Koston
fcc7020946 Fix memory leak in ESPHome disconnect callbacks (#104149) 2023-11-22 08:35:58 +01:00
J. Nick Koston
d69d9863b5 Fix ESPHome BLE client raising confusing error when not connected (#104146) 2023-11-22 08:35:55 +01:00
Arie Catsman
885152df81 Bump pyenphase to 1.14.3 (#104101)
fix(101354):update pyenphase to 1.14.3
2023-11-22 08:35:51 +01:00
Martin Hjelmare
7ff1bdb098 Fix device tracker see gps accuracy selector (#104022) 2023-11-22 08:35:48 +01:00
deosrc
399299c13c Fix netatmo authentication when using cloud authentication credentials (#104021)
* Fix netatmo authentication loop

* Update unit tests

* Move logic to determine api scopes

* Add unit tests for new method

* Use pyatmo scope list (#1)

* Exclude scopes not working with cloud

* Fix linting error

---------

Co-authored-by: Tobias Sauerwein <cgtobi@users.noreply.github.com>
2023-11-22 08:35:44 +01:00
J. Nick Koston
c241c2f79c Fix emulated_hue with None values (#104020) 2023-11-22 08:35:40 +01:00
Martin Hjelmare
b010c6b793 Fix openexchangerates form data description (#103974) 2023-11-22 08:35:36 +01:00
Chuck Foster
2f380d4b75 Fix duplicate Ban file entries (#103953) 2023-11-22 08:35:33 +01:00
Matt Zimmerman
19f268a1e1 Update smarttub to 0.0.36 (#103948) 2023-11-22 08:35:29 +01:00
Raman Gupta
bcd371ac2b Bump zwave-js-server-python to 0.54.0 (#103943) 2023-11-22 08:35:25 +01:00
Tom Brien
a5a8d38d08 Fix Coinbase for new API Structure (#103930) 2023-11-22 08:35:22 +01:00
Simone Chemelli
56298b2c88 fix Comelit cover stop (#103911) 2023-11-22 08:35:18 +01:00
Allen Porter
cf35e9b154 Update Fitbit to avoid a KeyError when restingHeartRate is not present (#103872)
* Update Fitbit to avoid a KeyError when `restingHeartRate` is not present

* Explicitly handle none response values
2023-11-22 08:35:15 +01:00
Allen Porter
29a65d5620 Fix for Google Calendar API returning invalid RRULE:DATE rules (#103870) 2023-11-22 08:35:11 +01:00
Allen Porter
c352cf0bd8 Fix bug in Fitbit config flow, and switch to prefer display name (#103869) 2023-11-22 08:35:08 +01:00
Allen Porter
e89b47138d Bump gcal_sync to 6.0.1 (#103861) 2023-11-22 08:35:04 +01:00
suaveolent
339e9e7b48 Bump lupupy to 0.3.1 (#103835)
Co-authored-by: suaveolent <suaveolent@users.noreply.github.com>
2023-11-22 08:35:00 +01:00
J. Nick Koston
92780dd217 Bump pyunifiprotect to 4.21.0 (#103832)
changelog: https://github.com/AngellusMortis/pyunifiprotect/compare/v4.20.0...v4.21.0
2023-11-22 08:34:56 +01:00
Maikel Punie
6133ce0258 Bump velbusaio to 2023.11.0 (#103798) 2023-11-22 08:34:53 +01:00
Simone Chemelli
57c76b2ea3 Bump aiocomelit to 0.5.2 (#103791)
* Bump aoicomelit to 0.5.0

* bump to 0.5.2
2023-11-22 08:34:49 +01:00
Mick Vleeshouwer
149aef9a12 Bump pyOverkiz to 1.13.2 (#103790) 2023-11-22 08:34:45 +01:00
Mick Vleeshouwer
3dddf6b9f6 Bump pyOverkiz to 1.13.0 (#103582) 2023-11-22 08:34:41 +01:00
Allen Porter
2a26dea587 Fix Rainbird unique to use a more reliable source (mac address) (#101603)
* Fix rainbird unique id to use a mac address for new entries

* Fix typo

* Normalize mac address before using as unique id

* Apply suggestions from code review

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

* Update test check and remove dead code

* Update all config entries to the new format

* Update config entry tests for migration

* Fix rainbird entity unique ids

* Add test coverage for repair failure

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Remove unnecessary migration failure checks

* Remove invalid config entries

* Update entry when entering a different hostname for an existing host.

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-11-22 08:34:36 +01:00
Jan Rieger
31ac03fe50 Fix typo in calendar translation (#103789) 2023-11-22 08:31:14 +01:00
Marcel van der Veldt
fb1dfb016e Fix race condition in Matter unsubscribe method (#103770) 2023-11-22 08:31:10 +01:00
Jan Bouwhuis
8a152a68d8 Fix raising vol.Invalid during mqtt config validation instead of ValueError (#103764) 2023-11-22 08:31:06 +01:00
Marcel van der Veldt
df3e49b24f Fix discovery schema for Matter switches (#103762)
* Fix discovery schema for matter switches

* fix typo in function that generates device name

* add test for switchunit
2023-11-22 08:31:02 +01:00
G-Two
db604170ba Bump subarulink to 0.7.9 (#103761) 2023-11-22 08:30:58 +01:00
J. Nick Koston
d8a6d3e1bc Bump python-matter-server to 4.0.2 (#103760)
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
2023-11-22 08:30:54 +01:00
Maciej Bieniek
6f086a27d4 Bump accuweather to version 2.1.0 (#103744) 2023-11-22 08:30:51 +01:00
Joost Lekkerkerker
3993c14f1d Lock Withings token refresh (#103688)
Lock Withings refresh
2023-11-22 08:30:44 +01:00
Allen Porter
d63d7841c3 Remove rainbird yaml config test fixtures (#103607) 2023-11-22 08:30:38 +01:00
Maciej Bieniek
e555671765 Bump accuweather to version 2.0.1 (#103532) 2023-11-22 08:30:33 +01:00
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
322 changed files with 9102 additions and 6989 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

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"quality_scale": "platinum",
"requirements": ["accuweather==2.0.0"]
"requirements": ["accuweather==2.1.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

@@ -857,16 +857,18 @@ class AlexaInputController(AlexaCapability):
def inputs(self) -> list[dict[str, str]] | None:
"""Return the list of valid supported inputs."""
source_list: list[str] = self.entity.attributes.get(
source_list: list[Any] = self.entity.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST, []
)
return AlexaInputController.get_valid_inputs(source_list)
@staticmethod
def get_valid_inputs(source_list: list[str]) -> list[dict[str, str]]:
def get_valid_inputs(source_list: list[Any]) -> list[dict[str, str]]:
"""Return list of supported inputs."""
input_list: list[dict[str, str]] = []
for source in source_list:
if not isinstance(source, str):
continue
formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "")
)

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

@@ -124,6 +124,7 @@ class BluetoothManager:
"storage",
"slot_manager",
"_debug",
"shutdown",
)
def __init__(
@@ -165,6 +166,7 @@ class BluetoothManager:
self.storage = storage
self.slot_manager = slot_manager
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
self.shutdown = False
@property
def supports_passive_scan(self) -> bool:
@@ -259,6 +261,7 @@ class BluetoothManager:
def async_stop(self, event: Event) -> None:
"""Stop the Bluetooth integration at shutdown."""
_LOGGER.debug("Stopping bluetooth manager")
self.shutdown = True
if self._cancel_unavailable_tracking:
self._cancel_unavailable_tracking()
self._cancel_unavailable_tracking = None

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

@@ -270,6 +270,10 @@ class HaBleakClientWrapper(BleakClient):
"""Connect to the specified GATT server."""
assert models.MANAGER is not None
manager = models.MANAGER
if manager.shutdown:
raise BleakError("Bluetooth is already shutdown")
if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("%s: Looking for backend to connect", self.__address)
wrapped_backend = self._async_get_best_available_backend_and_device(manager)
device = wrapped_backend.device
scanner = wrapped_backend.scanner
@@ -281,12 +285,14 @@ class HaBleakClientWrapper(BleakClient):
timeout=self.__timeout,
hass=manager.hass,
)
if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG):
if debug_logging:
# Only lookup the description if we are going to log it
description = ble_device_description(device)
_, adv = scanner.discovered_devices_and_advertisement_data[device.address]
rssi = adv.rssi
_LOGGER.debug("%s: Connecting (last rssi: %s)", description, rssi)
_LOGGER.debug(
"%s: Connecting via %s (last rssi: %s)", description, scanner.name, rssi
)
connected = None
try:
connected = await super().connect(**kwargs)
@@ -301,7 +307,9 @@ class HaBleakClientWrapper(BleakClient):
manager.async_release_connection_slot(device)
if debug_logging:
_LOGGER.debug("%s: Connected (last rssi: %s)", description, rssi)
_LOGGER.debug(
"%s: Connected via %s (last rssi: %s)", description, scanner.name, rssi
)
return connected
@hass_callback

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected==0.14.2"]
"requirements": ["bimmer-connected==0.14.3"]
}

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.57"],
"requirements": ["boschshcpy==0.2.75"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -24,7 +24,7 @@
"location": {
"name": "Location"
},
"messages": {
"message": {
"name": "Message"
},
"start_time": {

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==13.0.7"],
"requirements": ["PyChromecast==13.0.8"],
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv
from . import get_accounts
from .const import (
API_ACCOUNT_CURRENCY,
API_ACCOUNT_CURRENCY_CODE,
API_RATES,
API_RESOURCE_TYPE,
API_TYPE_VAULT,
@@ -81,7 +82,7 @@ async def validate_options(
accounts = await hass.async_add_executor_job(get_accounts, client)
accounts_currencies = [
account[API_ACCOUNT_CURRENCY]
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
for account in accounts
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
]

View File

@@ -12,14 +12,16 @@ DOMAIN = "coinbase"
API_ACCOUNT_AMOUNT = "amount"
API_ACCOUNT_BALANCE = "balance"
API_ACCOUNT_CURRENCY = "currency"
API_ACCOUNT_CURRENCY_CODE = "code"
API_ACCOUNT_ID = "id"
API_ACCOUNT_NATIVE_BALANCE = "native_balance"
API_ACCOUNT_NATIVE_BALANCE = "balance"
API_ACCOUNT_NAME = "name"
API_ACCOUNTS_DATA = "data"
API_RATES = "rates"
API_RESOURCE_PATH = "resource_path"
API_RESOURCE_TYPE = "type"
API_TYPE_VAULT = "vault"
API_USD = "USD"
WALLETS = {
"1INCH": "1INCH",

View File

@@ -14,9 +14,9 @@ from .const import (
API_ACCOUNT_AMOUNT,
API_ACCOUNT_BALANCE,
API_ACCOUNT_CURRENCY,
API_ACCOUNT_CURRENCY_CODE,
API_ACCOUNT_ID,
API_ACCOUNT_NAME,
API_ACCOUNT_NATIVE_BALANCE,
API_RATES,
API_RESOURCE_TYPE,
API_TYPE_VAULT,
@@ -55,7 +55,7 @@ async def async_setup_entry(
entities: list[SensorEntity] = []
provided_currencies: list[str] = [
account[API_ACCOUNT_CURRENCY]
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
for account in instance.accounts
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
]
@@ -106,26 +106,28 @@ class AccountSensor(SensorEntity):
self._currency = currency
for account in coinbase_data.accounts:
if (
account[API_ACCOUNT_CURRENCY] != currency
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
):
continue
self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}"
self._attr_unique_id = (
f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
f"{account[API_ACCOUNT_CURRENCY]}"
f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}"
)
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY]
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][
API_ACCOUNT_CURRENCY_CODE
]
self._attr_icon = CURRENCY_ICONS.get(
account[API_ACCOUNT_CURRENCY], DEFAULT_COIN_ICON
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE],
DEFAULT_COIN_ICON,
)
self._native_balance = round(
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
/ float(coinbase_data.exchange_rates[API_RATES][currency]),
2,
)
self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
API_ACCOUNT_AMOUNT
]
self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
API_ACCOUNT_CURRENCY
]
break
self._attr_state_class = SensorStateClass.TOTAL
@@ -141,7 +143,7 @@ class AccountSensor(SensorEntity):
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes of the sensor."""
return {
ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._native_currency}",
ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}",
}
def update(self) -> None:
@@ -149,17 +151,17 @@ class AccountSensor(SensorEntity):
self._coinbase_data.update()
for account in self._coinbase_data.accounts:
if (
account[API_ACCOUNT_CURRENCY] != self._currency
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
!= self._currency
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
):
continue
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
API_ACCOUNT_AMOUNT
]
self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
API_ACCOUNT_CURRENCY
]
self._native_balance = round(
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
/ float(self._coinbase_data.exchange_rates[API_RATES][self._currency]),
2,
)
break

View File

@@ -109,7 +109,7 @@ class ComelitCoverEntity(
if not self.is_closing and not self.is_opening:
return
action = STATE_OFF if self.is_closing else STATE_ON
action = STATE_ON if self.is_closing else STATE_OFF
await self._api.set_device_status(COVER, self._device.index, action)
@callback

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.3.0"]
"requirements": ["aiocomelit==0.5.2"]
}

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

@@ -25,9 +25,9 @@ see:
gps_accuracy:
selector:
number:
min: 1
max: 100
unit_of_measurement: "%"
min: 0
mode: box
unit_of_measurement: "m"
battery:
selector:
number:

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

@@ -676,19 +676,20 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]:
@lru_cache(maxsize=512)
def _build_entity_state_dict(entity: State) -> dict[str, Any]:
"""Build a state dict for an entity."""
is_on = entity.state != STATE_OFF
data: dict[str, Any] = {
STATE_ON: entity.state != STATE_OFF,
STATE_ON: is_on,
STATE_BRIGHTNESS: None,
STATE_HUE: None,
STATE_SATURATION: None,
STATE_COLOR_TEMP: None,
}
if data[STATE_ON]:
attributes = entity.attributes
if is_on:
data[STATE_BRIGHTNESS] = hass_to_hue_brightness(
entity.attributes.get(ATTR_BRIGHTNESS, 0)
attributes.get(ATTR_BRIGHTNESS) or 0
)
hue_sat = entity.attributes.get(ATTR_HS_COLOR)
if hue_sat is not None:
if (hue_sat := attributes.get(ATTR_HS_COLOR)) is not None:
hue = hue_sat[0]
sat = hue_sat[1]
# Convert hass hs values back to hue hs values
@@ -697,7 +698,7 @@ def _build_entity_state_dict(entity: State) -> dict[str, Any]:
else:
data[STATE_HUE] = HUE_API_STATE_HUE_MIN
data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0)
data[STATE_COLOR_TEMP] = attributes.get(ATTR_COLOR_TEMP) or 0
else:
data[STATE_BRIGHTNESS] = 0
@@ -706,25 +707,23 @@ def _build_entity_state_dict(entity: State) -> dict[str, Any]:
data[STATE_COLOR_TEMP] = 0
if entity.domain == climate.DOMAIN:
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
temperature = attributes.get(ATTR_TEMPERATURE, 0)
# Convert 0-100 to 0-254
data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100)
elif entity.domain == humidifier.DOMAIN:
humidity = entity.attributes.get(ATTR_HUMIDITY, 0)
humidity = attributes.get(ATTR_HUMIDITY, 0)
# Convert 0-100 to 0-254
data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100)
elif entity.domain == media_player.DOMAIN:
level = entity.attributes.get(
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
)
level = attributes.get(ATTR_MEDIA_VOLUME_LEVEL, 1.0 if is_on else 0.0)
# Convert 0.0-1.0 to 0-254
data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
elif entity.domain == fan.DOMAIN:
percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0
percentage = attributes.get(ATTR_PERCENTAGE) or 0
# Convert 0-100 to 0-254
data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100)
elif entity.domain == cover.DOMAIN:
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
level = attributes.get(ATTR_CURRENT_POSITION, 0)
data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
_clamp_values(data)
return data

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.3"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -75,15 +75,13 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
self: ESPHomeClient, *args: Any, **kwargs: Any
) -> Any:
# pylint: disable=protected-access
if not self._is_connected:
raise BleakError(f"{self._description} is not connected")
loop = self._loop
disconnected_futures = self._disconnected_futures
disconnected_future = loop.create_future()
disconnected_futures.add(disconnected_future)
ble_device = self._ble_device
disconnect_message = (
f"{self._source_name }: {ble_device.name} - {ble_device.address}: "
"Disconnected during operation"
)
disconnect_message = f"{self._description}: Disconnected during operation"
try:
async with interrupt(disconnected_future, BleakError, disconnect_message):
return await func(self, *args, **kwargs)
@@ -115,10 +113,8 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
if ex.error.error == -1:
# pylint: disable=protected-access
_LOGGER.debug(
"%s: %s - %s: BLE device disconnected during %s operation",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: BLE device disconnected during %s operation",
self._description,
func.__name__,
)
self._async_ble_device_disconnected()
@@ -140,7 +136,7 @@ class ESPHomeClientData:
api_version: APIVersion
title: str
scanner: ESPHomeScanner | None
disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list)
disconnect_callbacks: set[Callable[[], None]] = field(default_factory=set)
class ESPHomeClient(BaseBleakClient):
@@ -159,10 +155,11 @@ class ESPHomeClient(BaseBleakClient):
assert isinstance(address_or_ble_device, BLEDevice)
super().__init__(address_or_ble_device, *args, **kwargs)
self._loop = asyncio.get_running_loop()
self._ble_device = address_or_ble_device
self._address_as_int = mac_to_int(self._ble_device.address)
assert self._ble_device.details is not None
self._source = self._ble_device.details["source"]
ble_device = address_or_ble_device
self._ble_device = ble_device
self._address_as_int = mac_to_int(ble_device.address)
assert ble_device.details is not None
self._source = ble_device.details["source"]
self._cache = client_data.cache
self._bluetooth_device = client_data.bluetooth_device
self._client = client_data.client
@@ -177,8 +174,11 @@ class ESPHomeClient(BaseBleakClient):
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
client_data.api_version
)
self._address_type = address_or_ble_device.details["address_type"]
self._address_type = ble_device.details["address_type"]
self._source_name = f"{client_data.title} [{self._source}]"
self._description = (
f"{self._source_name}: {ble_device.name} - {ble_device.address}"
)
scanner = client_data.scanner
assert scanner is not None
self._scanner = scanner
@@ -196,12 +196,10 @@ class ESPHomeClient(BaseBleakClient):
except (AssertionError, ValueError) as ex:
_LOGGER.debug(
(
"%s: %s - %s: Failed to unsubscribe from connection state (likely"
"%s: Failed to unsubscribe from connection state (likely"
" connection dropped): %s"
),
self._source_name,
self._ble_device.name,
self._ble_device.address,
self._description,
ex,
)
self._cancel_connection_state = None
@@ -217,6 +215,7 @@ class ESPHomeClient(BaseBleakClient):
if not future.done():
future.set_result(None)
self._disconnected_futures.clear()
self._disconnect_callbacks.discard(self._async_esp_disconnected)
self._unsubscribe_connection_state()
def _async_ble_device_disconnected(self) -> None:
@@ -224,23 +223,15 @@ class ESPHomeClient(BaseBleakClient):
was_connected = self._is_connected
self._async_disconnected_cleanup()
if was_connected:
_LOGGER.debug(
"%s: %s - %s: BLE device disconnected",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: BLE device disconnected", self._description)
self._async_call_bleak_disconnected_callback()
def _async_esp_disconnected(self) -> None:
"""Handle the esp32 client disconnecting from us."""
_LOGGER.debug(
"%s: %s - %s: ESP device disconnected",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
self._disconnect_callbacks.remove(self._async_esp_disconnected)
_LOGGER.debug("%s: ESP device disconnected", self._description)
# Calling _async_ble_device_disconnected calls
# _async_disconnected_cleanup which will also remove
# the disconnect callbacks
self._async_ble_device_disconnected()
def _async_call_bleak_disconnected_callback(self) -> None:
@@ -258,10 +249,8 @@ class ESPHomeClient(BaseBleakClient):
) -> None:
"""Handle a connect or disconnect."""
_LOGGER.debug(
"%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: Connection state changed to connected=%s mtu=%s error=%s",
self._description,
connected,
mtu,
error,
@@ -300,12 +289,10 @@ class ESPHomeClient(BaseBleakClient):
return
_LOGGER.debug(
"%s: %s - %s: connected, registering for disconnected callbacks",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: connected, registering for disconnected callbacks",
self._description,
)
self._disconnect_callbacks.append(self._async_esp_disconnected)
self._disconnect_callbacks.add(self._async_esp_disconnected)
connected_future.set_result(connected)
@api_error_as_bleak_error
@@ -403,10 +390,8 @@ class ESPHomeClient(BaseBleakClient):
if bluetooth_device.ble_connections_free:
return
_LOGGER.debug(
"%s: %s - %s: Out of connection slots, waiting for a free one",
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: Out of connection slots, waiting for a free one",
self._description,
)
async with asyncio.timeout(timeout):
await bluetooth_device.wait_for_ble_connections_free()
@@ -434,7 +419,7 @@ class ESPHomeClient(BaseBleakClient):
if response.paired:
return True
_LOGGER.error(
"Pairing with %s failed due to error: %s", self.address, response.error
"%s: Pairing failed due to error: %s", self._description, response.error
)
return False
@@ -451,7 +436,7 @@ class ESPHomeClient(BaseBleakClient):
if response.success:
return True
_LOGGER.error(
"Unpairing with %s failed due to error: %s", self.address, response.error
"%s: Unpairing failed due to error: %s", self._description, response.error
)
return False
@@ -486,30 +471,14 @@ class ESPHomeClient(BaseBleakClient):
self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING
or dangerous_use_bleak_cache
) and (cached_services := cache.get_gatt_services_cache(address_as_int)):
_LOGGER.debug(
"%s: %s - %s: Cached services hit",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services hit", self._description)
self.services = cached_services
return self.services
_LOGGER.debug(
"%s: %s - %s: Cached services miss",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services miss", self._description)
esphome_services = await self._client.bluetooth_gatt_get_services(
address_as_int
)
_LOGGER.debug(
"%s: %s - %s: Got services: %s",
self._source_name,
self._ble_device.name,
self._ble_device.address,
esphome_services,
)
_LOGGER.debug("%s: Got services: %s", self._description, esphome_services)
max_write_without_response = self.mtu_size - GATT_HEADER_SIZE
services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
for service in esphome_services.services:
@@ -538,12 +507,7 @@ class ESPHomeClient(BaseBleakClient):
raise BleakError("Failed to get services from remote esp")
self.services = services
_LOGGER.debug(
"%s: %s - %s: Cached services saved",
self._source_name,
self._ble_device.name,
self._ble_device.address,
)
_LOGGER.debug("%s: Cached services saved", self._description)
cache.set_gatt_services_cache(address_as_int, services)
return services
@@ -552,13 +516,15 @@ class ESPHomeClient(BaseBleakClient):
) -> BleakGATTCharacteristic:
"""Resolve a characteristic specifier to a BleakGATTCharacteristic object."""
if (services := self.services) is None:
raise BleakError("Services have not been resolved")
raise BleakError(f"{self._description}: Services have not been resolved")
if not isinstance(char_specifier, BleakGATTCharacteristic):
characteristic = services.get_characteristic(char_specifier)
else:
characteristic = char_specifier
if not characteristic:
raise BleakError(f"Characteristic {char_specifier} was not found!")
raise BleakError(
f"{self._description}: Characteristic {char_specifier} was not found!"
)
return characteristic
@verify_connected
@@ -579,8 +545,8 @@ class ESPHomeClient(BaseBleakClient):
if response.success:
return True
_LOGGER.error(
"Clear cache failed with %s failed due to error: %s",
self.address,
"%s: Clear cache failed due to error: %s",
self._description,
response.error,
)
return False
@@ -692,7 +658,7 @@ class ESPHomeClient(BaseBleakClient):
ble_handle = characteristic.handle
if ble_handle in self._notify_cancels:
raise BleakError(
"Notifications are already enabled on "
f"{self._description}: Notifications are already enabled on "
f"service:{characteristic.service_uuid} "
f"characteristic:{characteristic.uuid} "
f"handle:{ble_handle}"
@@ -702,8 +668,8 @@ class ESPHomeClient(BaseBleakClient):
and "indicate" not in characteristic.properties
):
raise BleakError(
f"Characteristic {characteristic.uuid} does not have notify or indicate"
" property set."
f"{self._description}: Characteristic {characteristic.uuid} "
"does not have notify or indicate property set."
)
self._notify_cancels[
@@ -725,18 +691,13 @@ class ESPHomeClient(BaseBleakClient):
cccd_descriptor = characteristic.get_descriptor(CCCD_UUID)
if not cccd_descriptor:
raise BleakError(
f"Characteristic {characteristic.uuid} does not have a "
"characteristic client config descriptor."
f"{self._description}: Characteristic {characteristic.uuid} "
"does not have a characteristic client config descriptor."
)
_LOGGER.debug(
(
"%s: %s - %s: Writing to CCD descriptor %s for notifications with"
" properties=%s"
),
self._source_name,
self._ble_device.name,
self._ble_device.address,
"%s: Writing to CCD descriptor %s for notifications with properties=%s",
self._description,
cccd_descriptor.handle,
characteristic.properties,
)
@@ -774,12 +735,10 @@ class ESPHomeClient(BaseBleakClient):
if self._cancel_connection_state:
_LOGGER.warning(
(
"%s: %s - %s: ESPHomeClient bleak client was not properly"
"%s: ESPHomeClient bleak client was not properly"
" disconnected before destruction"
),
self._source_name,
self._ble_device.name,
self._ble_device.address,
self._description,
)
if not self._loop.is_closed():
self._loop.call_soon_threadsafe(self._async_disconnected_cleanup)

View File

@@ -107,7 +107,7 @@ class RuntimeEntryData:
bluetooth_device: ESPHomeBluetoothDevice | None = None
api_version: APIVersion = field(default_factory=APIVersion)
cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list)
disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list)
disconnect_callbacks: set[Callable[[], None]] = field(default_factory=set)
state_subscriptions: dict[
tuple[type[EntityState], int], Callable[[], None]
] = field(default_factory=dict)
@@ -427,3 +427,19 @@ class RuntimeEntryData:
if self.original_options == entry.options:
return
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
@callback
def async_on_disconnect(self) -> None:
"""Call when the entry has been disconnected.
Safe to call multiple times.
"""
self.available = False
# Make a copy since calling the disconnect callbacks
# may also try to discard/remove themselves.
for disconnect_cb in self.disconnect_callbacks.copy():
disconnect_cb()
# Make sure to clear the set to give up the reference
# to it and make sure all the callbacks can be GC'd.
self.disconnect_callbacks.clear()
self.disconnect_callbacks = set()

View File

@@ -294,7 +294,7 @@ class ESPHomeManager:
event.data["entity_id"], attribute, new_state
)
self.entry_data.disconnect_callbacks.append(
self.entry_data.disconnect_callbacks.add(
async_track_state_change_event(
hass, [entity_id], send_home_assistant_state_event
)
@@ -439,7 +439,7 @@ class ESPHomeManager:
reconnect_logic.name = device_info.name
if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version):
entry_data.disconnect_callbacks.append(
entry_data.disconnect_callbacks.add(
await async_connect_scanner(
hass, entry, cli, entry_data, self.domain_data.bluetooth_cache
)
@@ -459,7 +459,7 @@ class ESPHomeManager:
await cli.subscribe_home_assistant_states(self.async_on_state_subscription)
if device_info.voice_assistant_version:
entry_data.disconnect_callbacks.append(
entry_data.disconnect_callbacks.add(
await cli.subscribe_voice_assistant(
self._handle_pipeline_start,
self._handle_pipeline_stop,
@@ -487,10 +487,7 @@ class ESPHomeManager:
host,
expected_disconnect,
)
for disconnect_cb in entry_data.disconnect_callbacks:
disconnect_cb()
entry_data.disconnect_callbacks = []
entry_data.available = False
entry_data.async_on_disconnect()
entry_data.expected_disconnect = expected_disconnect
# Mark state as stale so that we will always dispatch
# the next state update of that type when the device reconnects
@@ -596,6 +593,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 +607,7 @@ def _async_setup_device_registry(
model=model,
sw_version=sw_version,
hw_version=hw_version,
suggested_area=suggested_area,
)
return device_entry.id
@@ -750,10 +752,7 @@ async def cleanup_instance(hass: HomeAssistant, entry: ConfigEntry) -> RuntimeEn
"""Cleanup the esphome client if it exists."""
domain_data = DomainData.get(hass)
data = domain_data.pop_entry_data(entry)
data.available = False
for disconnect_cb in data.disconnect_callbacks:
disconnect_cb()
data.disconnect_callbacks = []
data.async_on_disconnect()
for cleanup_callback in data.cleanup_callbacks:
cleanup_callback()
await data.async_cleanup()

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

@@ -69,7 +69,7 @@ class FitbitApi(ABC):
profile = response["user"]
self._profile = FitbitProfile(
encoded_id=profile["encodedId"],
full_name=profile["fullName"],
display_name=profile["displayName"],
locale=profile.get("locale"),
)
return self._profile

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."""
@@ -75,7 +90,7 @@ class OAuth2FlowHandler(
await self.async_set_unique_id(profile.encoded_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=profile.full_name, data=data)
return self.async_create_entry(title=profile.display_name, data=data)
async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
"""Handle import from YAML."""

View File

@@ -14,8 +14,8 @@ class FitbitProfile:
encoded_id: str
"""The ID representing the Fitbit user."""
full_name: str
"""The first name value specified in the user's account settings."""
display_name: str
"""The name shown when the user's friends look at their Fitbit profile."""
locale: str | None
"""The locale defined in the user's Fitbit account settings."""

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 (
@@ -132,6 +134,17 @@ def _water_unit(unit_system: FitbitUnitSystem) -> UnitOfVolume:
return UnitOfVolume.MILLILITERS
def _int_value_or_none(field: str) -> Callable[[dict[str, Any]], int | None]:
"""Value function that will parse the specified field if present."""
def convert(result: dict[str, Any]) -> int | None:
if (value := result["value"].get(field)) is not None:
return int(value)
return None
return convert
@dataclass
class FitbitSensorEntityDescription(SensorEntityDescription):
"""Describes Fitbit sensor entity."""
@@ -204,7 +217,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
name="Resting Heart Rate",
native_unit_of_measurement="bpm",
icon="mdi:heart-pulse",
value_fn=lambda result: int(result["value"]["restingHeartRate"]),
value_fn=_int_value_or_none("restingHeartRate"),
scope=FitbitScope.HEART_RATE,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -567,34 +580,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

@@ -521,8 +521,13 @@ class GoogleCalendarEntity(
def _get_calendar_event(event: Event) -> CalendarEvent:
"""Return a CalendarEvent from an API event."""
rrule: str | None = None
if len(event.recurrence) == 1:
rrule = event.recurrence[0].lstrip(RRULE_PREFIX)
# Home Assistant expects a single RRULE: and all other rule types are unsupported or ignored
if (
len(event.recurrence) == 1
and (raw_rule := event.recurrence[0])
and raw_rule.startswith(RRULE_PREFIX)
):
rrule = raw_rule.removeprefix(RRULE_PREFIX)
return CalendarEvent(
uid=event.ical_uuid,
recurrence_id=event.id if event.recurring_event_id else None,

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==5.0.0", "oauth2client==4.1.3"]
"requirements": ["gcal-sync==6.0.1", "oauth2client==4.1.3"]
}

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

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/honeywell",
"iot_class": "cloud_polling",
"loggers": ["somecomfort"],
"requirements": ["AIOSomecomfort==0.0.17"]
"requirements": ["AIOSomecomfort==0.0.22"]
}

View File

@@ -242,5 +242,6 @@ class IpBanManager:
async def async_add_ban(self, remote_addr: IPv4Address | IPv6Address) -> None:
"""Add a new IP address to the banned list."""
new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr)
await self.hass.async_add_executor_job(self._add_ban, new_ban)
if remote_addr not in self.ip_bans_lookup:
new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr)
await self.hass.async_add_executor_job(self._add_ban, new_ban)

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

@@ -6,6 +6,7 @@ from collections.abc import Mapping
from datetime import datetime, timedelta
import email
from email.header import decode_header, make_header
from email.message import Message
from email.utils import parseaddr, parsedate_to_datetime
import logging
from typing import Any
@@ -96,8 +97,9 @@ async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL:
class ImapMessage:
"""Class to parse an RFC822 email message."""
def __init__(self, raw_message: bytes) -> None:
def __init__(self, raw_message: bytes, charset: str = "utf-8") -> None:
"""Initialize IMAP message."""
self._charset = charset
self.email_message = email.message_from_bytes(raw_message)
@property
@@ -157,18 +159,30 @@ class ImapMessage:
message_html: str | None = None
message_untyped_text: str | None = None
def _decode_payload(part: Message) -> str:
"""Try to decode text payloads.
Common text encodings are quoted-printable or base64.
Falls back to the raw content part if decoding fails.
"""
try:
return str(part.get_payload(decode=True).decode(self._charset))
except Exception: # pylint: disable=broad-except
return str(part.get_payload())
part: Message
for part in self.email_message.walk():
if part.get_content_type() == CONTENT_TYPE_TEXT_PLAIN:
if message_text is None:
message_text = part.get_payload()
message_text = _decode_payload(part)
elif part.get_content_type() == "text/html":
if message_html is None:
message_html = part.get_payload()
message_html = _decode_payload(part)
elif (
part.get_content_type().startswith("text")
and message_untyped_text is None
):
message_untyped_text = part.get_payload()
message_untyped_text = str(part.get_payload())
if message_text is not None:
return message_text
@@ -223,7 +237,9 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
"""Send a event for the last message if the last message was changed."""
response = await self.imap_client.fetch(last_message_uid, "BODY.PEEK[]")
if response.result == "OK":
message = ImapMessage(response.lines[1])
message = ImapMessage(
response.lines[1], charset=self.config_entry.data[CONF_CHARSET]
)
# Set `initial` to `False` if the last message is triggered again
initial: bool = True
if (message_id := message.message_id) == self._last_message_id:

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

@@ -9,9 +9,9 @@ from typing import Any
from ical.calendar import Calendar
from ical.calendar_stream import IcsCalendarStream
from ical.event import Event
from ical.exceptions import CalendarParseError
from ical.store import EventStore, EventStoreError
from ical.types import Range, Recur
from pydantic import ValidationError
import voluptuous as vol
from homeassistant.components.calendar import (
@@ -178,8 +178,8 @@ def _parse_event(event: dict[str, Any]) -> Event:
event[key] = dt_util.as_local(value).replace(tzinfo=None)
try:
return Event.parse_obj(event)
except ValidationError as err:
return Event(**event)
except CalendarParseError as err:
_LOGGER.debug("Error parsing event input fields: %s (%s)", event, str(err))
raise vol.Invalid("Error parsing event input fields") from err

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==5.1.0"]
"requirements": ["ical==6.1.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==5.1.0"]
"requirements": ["ical==6.1.0"]
}

View File

@@ -7,9 +7,9 @@ from typing import Any
from ical.calendar import Calendar
from ical.calendar_stream import IcsCalendarStream
from ical.exceptions import CalendarParseError
from ical.store import TodoStore
from ical.todo import Todo, TodoStatus
from pydantic import ValidationError
from homeassistant.components.todo import (
TodoItem,
@@ -63,9 +63,11 @@ def _todo_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
"""Convert TodoItem dataclass items to dictionary of attributes for ical consumption."""
result: dict[str, str] = {}
for name, value in obj:
if value is None:
continue
if name == "status":
result[name] = ICS_TODO_STATUS_MAP_INV[value]
elif value is not None:
else:
result[name] = value
return result
@@ -74,7 +76,7 @@ def _convert_item(item: TodoItem) -> Todo:
"""Convert a HomeAssistant TodoItem to an ical Todo."""
try:
return Todo(**dataclasses.asdict(item, dict_factory=_todo_dict_factory))
except ValidationError as err:
except CalendarParseError as err:
_LOGGER.debug("Error parsing todo input fields: %s (%s)", item, err)
raise HomeAssistantError("Error parsing todo input fields") from err
@@ -139,20 +141,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

@@ -27,7 +27,7 @@ def setup_platform(
data = hass.data[LUPUSEC_DOMAIN]
device_types = [CONST.TYPE_OPENING]
device_types = CONST.TYPE_OPENING
devices = []
for device in data.lupusec.get_devices(generic_type=device_types):

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/lupusec",
"iot_class": "local_polling",
"loggers": ["lupupy"],
"requirements": ["lupupy==0.3.0"]
"requirements": ["lupupy==0.3.1"]
}

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

@@ -145,9 +145,7 @@ class MatterAdapter:
get_clean_name(basic_info.nodeLabel)
or get_clean_name(basic_info.productLabel)
or get_clean_name(basic_info.productName)
or device_type.__name__
if device_type
else None
or (device_type.__name__ if device_type else None)
)
# handle bridged devices

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from abc import abstractmethod
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Any, cast
@@ -110,7 +111,9 @@ class MatterEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
for unsub in self._unsubscribes:
unsub()
with suppress(ValueError):
# suppress ValueError to prevent race conditions
unsub()
@callback
def _on_matter_event(self, event: EventType, data: Any = None) -> None:

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

@@ -6,5 +6,5 @@
"dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push",
"requirements": ["python-matter-server==4.0.0"]
"requirements": ["python-matter-server==4.0.2"]
}

View File

@@ -67,7 +67,15 @@ DISCOVERY_SCHEMAS = [
),
entity_class=MatterSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
# restrict device type to prevent discovery by the wrong platform
device_type=(device_types.OnOffPlugInUnit,),
),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=SwitchEntityDescription(
key="MatterSwitch", device_class=SwitchDeviceClass.SWITCH, name=None
),
entity_class=MatterSwitch,
required_attributes=(clusters.OnOff.Attributes.OnOff,),
not_device_type=(
device_types.ColorTemperatureLight,
device_types.DimmableLight,
@@ -76,7 +84,6 @@ DISCOVERY_SCHEMAS = [
device_types.DoorLock,
device_types.ColorDimmerSwitch,
device_types.DimmerSwitch,
device_types.OnOffLightSwitch,
device_types.Thermostat,
),
),

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