Compare commits

...

359 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
Raman Gupta 8b1cfbc46c Bump zwave-js-server-python to 0.53.1 (#102790) 2023-10-25 17:22:33 +02:00
Marcel van der Veldt bcade5fe73 Bump python-matter-server to version 4.0.0 (#102786) 2023-10-25 16:51:42 +02:00
Marc Mueller 4cac20f835 Fix google_tasks generic typing (#102778) 2023-10-25 07:22:19 -07:00
Erik Montnemery b83ada8c19 Use real devices in automation and script tests (#102785) 2023-10-25 16:09:39 +02:00
Joost Lekkerkerker e734a4bc53 Use sentence case in Random entities default name (#102788) 2023-10-25 16:09:09 +02:00
Joost Lekkerkerker cd8e3a81db Add Update coordinator to QBittorrent (#98896) 2023-10-25 15:51:52 +02:00
Erik Montnemery 8d034a85fe Small cleanup of nest tests (#102787) 2023-10-25 15:35:58 +02:00
tronikos 89e2f06304 Flume: Add flume.notifications service (#100621)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-25 15:34:48 +02:00
J. Nick Koston 4447336083 Fix hassio delaying startup to fetch container stats (#102775) 2023-10-25 15:32:43 +02:00
Erik Montnemery 6e72499f96 Use real devices in nest device trigger tests (#102692) 2023-10-25 15:13:38 +02:00
Simone Chemelli e4a1efb680 Fix Comelit comments as per late review (#102783) 2023-10-25 14:48:33 +02:00
Bram Kragten b50f5e50c3 Update frontend to 20231025.1 (#102781) 2023-10-25 08:42:44 -04:00
Jan Stienstra 2c46a975fb Add re-authentication to Jellyfin (#97442) 2023-10-25 14:02:30 +02:00
Bram Kragten edc9aba722 Update frontend to 20231025.0 (#102776) 2023-10-25 14:01:36 +02:00
Maciej Bieniek 47c9d58b5e Override the async_update() method for Shelly sleeping devices (#102516) 2023-10-25 13:48:00 +02:00
Allen Porter 476e867fe8 Add a Local To-do component (#102627)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-25 13:21:10 +02:00
Raman Gupta 35d18a9a3e Add tests for types and functions for type conversions in templates (#100807)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-25 13:20:34 +02:00
Joost Lekkerkerker 8ca5df6fcc Guard for None color mode in ZHA (#102774) 2023-10-25 13:17:41 +02:00
Joost Lekkerkerker 0658c7b307 Add config flow to random (#100858)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-25 13:01:27 +02:00
Jan-Philipp Benecke 6fae50cb75 Add connections to Xiaomi BLE and BTHome device entry (#102773) 2023-10-25 12:15:58 +02:00
Joost Lekkerkerker d2f8c527a5 Add entity translations to Tomorrow.io (#99632) 2023-10-25 12:15:09 +02:00
Joost Lekkerkerker 4812d62ccf Bring Withings activity sensor creation in line with the others (#102771) 2023-10-25 12:14:03 +02:00
Joost Lekkerkerker 45e4f71d1a Add generics to Withings (#102770) 2023-10-25 12:00:12 +02:00
Joakim Sørensen 267721af43 Bump hass-nabucasa from 0.73.0 to 0.74.0 (#102763) 2023-10-25 11:28:52 +02:00
Simone Chemelli 4328f887be Address late review comments for Comelit login (#102768) 2023-10-25 09:19:06 +00:00
Jirka dfc454d527 Remove double full stop from Vulcan translation strings (#102758) 2023-10-25 10:54:43 +02:00
Allen Porter 7f7064ce59 Add Google Tasks create and update for todo platform (#102754)
* Add Google Tasks create and update for todo platform

* Update comments

* Update comments
2023-10-25 10:51:21 +02:00
Simone Chemelli ffed1e8274 Improve exception handling for Comelit (#102762)
improve exception handling for Comelit
2023-10-25 10:28:22 +02:00
tzagim 37cde54b2b Fix typo in Todoist translations strings 'data' -> 'date' (#102760) 2023-10-25 09:48:47 +02:00
Erik Montnemery a6c5927976 Use real devices in light device condition tests (#102756) 2023-10-25 09:42:35 +02:00
Erik Montnemery b38692f3a7 Use real devices in lock device condition tests (#102757) 2023-10-25 09:42:00 +02:00
Maciej Bieniek c89acf2abe Bump nextdns to version 2.0.0 (#102674) 2023-10-25 08:59:45 +02:00
starkillerOG 93a8b60c2b Philips Hue restore brightness after transition (#101293) 2023-10-25 07:46:49 +02:00
buzz-tee 4bf475185e Fix invalid sources in media player sources list (#102646) 2023-10-25 07:12:55 +02:00
mkmer b37e9bc79a Improve camera snap performance in Blink (#102652) 2023-10-24 23:50:10 -05:00
Ravaka Razafimanantsoa 7038bd67f7 Add Climate to switchbot cloud integration (#101660) 2023-10-24 23:46:00 -05:00
Allen Porter 0cb0e3ceeb Add Google tasks integration, with initial read-only To-do list (#102629)
* Add Google Tasks integration

* Update tests and unique id

* Revert devcontainer change

* Increase test coverage

* Apply suggestions from code review

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

* Remove ternary

* Fix JSON

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-24 21:30:29 -07:00
Matthias Alphart fb13d9ce7c Set Fronius entities to "unknown" when receiving invalid zero value (#102270) 2023-10-25 06:27:46 +02:00
Erik Montnemery 704881743b Use real devices in remote device trigger tests (#102693) 2023-10-25 06:24:23 +02:00
Jesse Hills ad692f3341 ESPHome Text entities (#102742) 2023-10-24 23:14:58 -05:00
Erik Montnemery 789a00043a Use real devices in device automation tests (#102736)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-25 06:14:53 +02:00
Raman Gupta aa36229519 Remove eight_sleep integration (#102669) 2023-10-25 06:13:10 +02:00
Erik Montnemery 626123acc0 Use real devices in select device trigger tests (#102694) 2023-10-25 06:13:00 +02:00
Erik Montnemery b870933dc7 Use real devices in remote device action tests (#102725) 2023-10-25 06:11:55 +02:00
Erik Montnemery 9047dcf242 Use real devices in text device action tests (#102728) 2023-10-25 06:11:06 +02:00
Luke Lashley dd111416e7 Add cleaning binary sensor to Roborock (#102748) 2023-10-25 06:10:31 +02:00
J. Nick Koston ece7ec6a38 Disable IPV6 in the august integration (#98003) 2023-10-25 06:08:41 +02:00
AJ Jordan 2e643c0c75 Fix dead link in Kodi log message (#102743) 2023-10-25 06:07:38 +02:00
tronikos 6294339944 Improve ZHA King of Fans (#101859) 2023-10-25 05:56:08 +02:00
puddly 8d5cb20285 Bump ZHA radio dependencies (#102750) 2023-10-24 23:36:30 -04:00
TheJulianJES 7c93d4fccf Bump zha-quirks to 0.0.106 (#102741) 2023-10-24 21:57:34 -04:00
Raman Gupta ec3ee7f02c Update zwave_js/hard_reset_controller WS cmd (#102280) 2023-10-24 21:31:03 -04:00
Robert Svensson 40817dabbf Bump aiounifi to v64 (#102700) 2023-10-24 20:27:42 -05:00
Marc Mueller eb52943d27 Update pytest to 7.4.3 (#102744) 2023-10-25 02:44:51 +02:00
Erik Montnemery a1a5713e10 Abort Improv via BLE bluetooth flow if device is provisioned (#102656)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-24 19:14:23 -05:00
J. Nick Koston f91583a0fc Add support for family to aiohttp session helper (#102702) 2023-10-24 18:40:39 -05:00
Robert Svensson a691bd26cf Support Lidl christmas light effects in deCONZ (#102731) 2023-10-25 00:32:20 +02:00
Maciej Bieniek b37253b206 Bump gios to version 3.2.0 (#102675) 2023-10-24 23:45:20 +02:00
Erik Montnemery f56343f447 Use real devices in lock device action tests (#102723) 2023-10-24 23:43:50 +02:00
Erik Montnemery 3a11a6f973 Use real devices in switch device action tests (#102727) 2023-10-24 23:43:30 +02:00
Erik Montnemery 530611c44e Use real devices in fan device action tests (#102720) 2023-10-24 21:40:24 +00:00
Erik Montnemery 21d0fa640f Use real devices in cover device action tests (#102719) 2023-10-24 23:40:22 +02:00
Erik Montnemery 8da421c442 Use real devices in climate device action tests (#102718) 2023-10-24 23:40:07 +02:00
Erik Montnemery 4bb6787909 Use real devices in button device action tests (#102717) 2023-10-24 23:39:47 +02:00
Erik Montnemery a4487637ef Use real devices in alarm_control_panel device action tests (#102716) 2023-10-24 23:39:37 +02:00
Erik Montnemery 2e9a3e8c8e Use real devices in humidifier device action tests (#102721) 2023-10-24 23:39:14 +02:00
Erik Montnemery e1394d720f Use real devices in vacuum device action tests (#102729) 2023-10-24 23:38:35 +02:00
Erik Montnemery 02a83740cc Use real devices in light device action tests (#102722) 2023-10-24 21:38:24 +00:00
Erik Montnemery 69ce85d5af Use real devices in select device action tests (#102726) 2023-10-24 23:38:19 +02:00
Erik Montnemery e708faa4d6 Use real devices in vacuum device condition tests (#102715) 2023-10-24 23:38:01 +02:00
Erik Montnemery e761d5715b Use real devices in switch device condition tests (#102714) 2023-10-24 21:37:48 +00:00
Erik Montnemery b5a6e6b9d5 Use real devices in sensor device condition tests (#102713) 2023-10-24 23:37:36 +02:00
Erik Montnemery ff60a8072e Use real devices in select device condition tests (#102712) 2023-10-24 23:37:16 +02:00
Erik Montnemery 1b61cd9179 Use real devices in remote device condition tests (#102711) 2023-10-24 23:36:31 +02:00
Erik Montnemery 2049d892ba Use real devices in media_player device condition tests (#102710) 2023-10-24 23:35:34 +02:00
Erik Montnemery 0e8bd9805a Use real devices in humidifier device condition tests (#102709) 2023-10-24 23:34:31 +02:00
Erik Montnemery 3ed67f134f Use real devices in fan device condition tests (#102708) 2023-10-24 23:32:56 +02:00
Erik Montnemery 9cf9b36637 Use real devices in device_tracker device condition tests (#102707) 2023-10-24 23:31:49 +02:00
Erik Montnemery 14485af22d Use real devices in cover device condition tests (#102706) 2023-10-24 23:30:33 +02:00
Erik Montnemery bead989e7f Use real devices in climate device condition tests (#102705) 2023-10-24 23:29:44 +02:00
Erik Montnemery 9d3cdc85ca Use real devices in binary_sensor device condition tests (#102704) 2023-10-24 23:28:29 +02:00
Erik Montnemery 4d83cffb39 Use real devices in alarm_control_panel device condition tests (#102703) 2023-10-24 23:27:29 +02:00
Erik Montnemery 56ee1753ec Use real devices in number device action tests (#102724) 2023-10-24 23:26:12 +02:00
Erik Montnemery 0ce7f44294 Use real devices in water_heater device action tests (#102730) 2023-10-24 23:25:14 +02:00
TheJulianJES fd8fdba7e8 Replace ZHA quirk class matching with quirk ID matching (#102482)
* Use fixed quirk IDs for matching instead of quirk class

* Change tests for quirk id (WIP)

* Do not default `quirk_id` to `quirk_class`

* Implement test for checking if quirk ID exists

* Change `quirk_id` for test slightly (underscore instead of dot)
2023-10-24 17:18:10 -04:00
Maciej Bieniek 5ee14f7f7d Bump accuweather to version 2.0.0 (#102670) 2023-10-24 23:14:05 +02:00
Bouwe Westerdijk a5461a9a90 Bump plugwise to v0.33.2 (#102671) 2023-10-24 23:11:16 +02:00
Bram Kragten f5a6c88051 Don't load themes in safe mode (#102683) 2023-10-24 23:00:14 +02:00
Allen Porter 0b8f48205a Add Todoist To-do list support (#102633)
* Add todoist todo platform

* Fix comment in todoist todo platform

* Revert CalData cleanup and logging

* Fix bug in fetching tasks per project

* Add test coverage for creating active tasks

* Fix update behavior on startup
2023-10-24 22:47:26 +02:00
Erik Montnemery ee1007abdb Use real devices in wemo device trigger tests (#102699) 2023-10-24 22:44:50 +02:00
Erik Montnemery 2807c9eaca Use real devices in vacuum device trigger tests (#102698) 2023-10-24 22:43:34 +02:00
Erik Montnemery d8baa38751 Use real devices in update device trigger tests (#102697) 2023-10-24 22:42:44 +02:00
Erik Montnemery 8737d84d30 Use real devices in switch device trigger tests (#102696) 2023-10-24 22:41:27 +02:00
Erik Montnemery 82cc62416e Use real devices in sensor device trigger tests (#102695) 2023-10-24 22:37:44 +02:00
Erik Montnemery 952f40a181 Use real devices in alarm_control_panel device trigger tests (#102676) 2023-10-24 22:33:08 +02:00
Erik Montnemery eac1d47ec6 Use real devices in media_player device trigger tests (#102691) 2023-10-24 22:31:16 +02:00
Erik Montnemery f9fa1edabf Use real devices in lock device trigger tests (#102690) 2023-10-24 22:30:07 +02:00
Erik Montnemery 691de148cf Use real devices in light device trigger tests (#102689) 2023-10-24 21:58:37 +02:00
Erik Montnemery 6d1d3f4207 Use real devices in device_tracker device trigger tests (#102685) 2023-10-24 21:58:22 +02:00
Erik Montnemery 6edbee75f0 Use real devices in kodi device trigger tests (#102688) 2023-10-24 21:58:06 +02:00
Erik Montnemery cedade15ef Use real devices in humidifier device trigger tests (#102687) 2023-10-24 21:58:01 +02:00
Erik Montnemery 51f6dac97f Use real devices in fan device trigger tests (#102686) 2023-10-24 21:57:57 +02:00
Erik Montnemery 4536720540 Use real devices in device_automation device trigger tests (#102684) 2023-10-24 21:57:42 +02:00
Erik Montnemery ec3596e85d Use real devices in cover device trigger tests (#102681) 2023-10-24 21:57:33 +02:00
Erik Montnemery 13be486d61 Use real devices in climate device trigger tests (#102680) 2023-10-24 21:57:22 +02:00
Erik Montnemery 223abb6dca Use real devices in button device trigger tests (#102679) 2023-10-24 21:57:14 +02:00
Erik Montnemery acc5edb088 Use real devices in binary_sensor device trigger tests (#102678) 2023-10-24 21:56:53 +02:00
Tomáš Bedřich d25b4aae14 Add ZHA cover tilt (#102072)
* cover tilt reimplementation

* rework tilt test

* Fix ZHA cover tests

* Match ZHA cover tilt code-style with the rest

* Increase coverage for ZHA cover, optimize update

---------

Co-authored-by: josef109 <josefglaze@gmail.com>
2023-10-24 15:40:41 -04:00
Maciej Bieniek 4febb2e1d3 Bump nam to version 2.2.0 (#102673) 2023-10-24 21:14:17 +02:00
Erik Montnemery f733f20834 Use real devices in arcam_fmj device trigger tests (#102677) 2023-10-24 13:39:16 -05:00
Erik Montnemery 508cffd1b5 Bump py-improv-ble-client to 1.0.3 (#102661) 2023-10-24 18:05:55 +02:00
Bouwe Westerdijk 8c3ae1b30c Add hvac_modes property to Plugwise (#102636)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2023-10-24 17:53:55 +02:00
Joost Lekkerkerker 9600c7fac1 Add workout calendar to Withings (#102589) 2023-10-24 16:38:11 +02:00
J. Nick Koston 80b3fec675 Bump aioesphomeapi to 18.0.12 (#102626)
changelog: https://github.com/esphome/aioesphomeapi/compare/v18.0.11...v18.0.12
2023-10-24 08:35:59 -05:00
Erik Montnemery 4604c5a152 Allow connecting an Improv via BLE device to a public network (#102655) 2023-10-24 15:24:30 +02:00
Erik Montnemery 97cc05d0b4 Make it possible to restart core in safe mode (#102606) 2023-10-24 14:47:58 +02:00
Erik Montnemery 46322a0f59 Add improv_ble integration (#102129)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-24 14:19:19 +02:00
Rami Mosleh fea15148a1 Remove scan_interval from transmission (#98858)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-10-24 14:17:46 +02:00
Joost Lekkerkerker 8cfb8cb084 Add serial number to Blink (#102621) 2023-10-24 11:38:54 +02:00
Erik Montnemery e20d4abfe1 Test extra javascript functionality in frontend (#102643) 2023-10-24 11:31:16 +02:00
Erik Montnemery 421832e09c Remove unused test fixture from frontend tests (#102642) 2023-10-24 11:13:44 +02:00
Joost Lekkerkerker b42c47e800 Add last workout sensors to Withings (#102541)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-24 11:07:47 +02:00
Franck Nijhof 57a10a2e0d Set cart icon for shopping list integration (#102638) 2023-10-24 10:32:27 +02:00
Matthias Alphart b0d4e5cb65 Retire Niels Mündler from Fronius codeowners (#102639) 2023-10-24 09:20:28 +02:00
Erik Montnemery b953f2998c Rename the safe_mode integration to recovery_mode (#102581)
* Rename safe mode integration to recovery mode

* Update code
2023-10-24 09:11:14 +02:00
Jan Bouwhuis 6372bc3aaa Fix missed case alexa light attr can be None (#102612)
* Fix missed cased alexa light attr can be None

* Add test
2023-10-24 00:20:03 +02:00
Joost Lekkerkerker d5e7cccff9 Add serial number to Brother (#102523) 2023-10-24 00:15:58 +02:00
Abílio Costa 2935d7d919 Remove uneeded typing on Idasen Desk (#102615) 2023-10-23 23:57:58 +02:00
ollo69 5245c94342 Exclude AsusWRT tracker state attribute from recorder (#102602) 2023-10-23 23:16:27 +02:00
Allen Porter 5d430f53cd Add todo component (#100019) 2023-10-23 22:53:00 +02:00
J. Nick Koston fa1df7e334 Bump pyatv to 0.14.3 (#102196) 2023-10-23 15:48:19 -05:00
J. Nick Koston 4c8a919ca3 Bump aioesphomeapi to 18.0.11 (#102603) 2023-10-23 15:46:05 -05:00
Michael Hansen a78e3f7b0f Delay import of webrtc to avoid blocking start up if package is missing (#102594)
* Delay import of webrtc to avoid blocking start up if package is missing

* Update homeassistant/components/assist_pipeline/pipeline.py

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Update homeassistant/components/assist_pipeline/vad.py

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-10-23 13:34:32 -05:00
Erik Montnemery c481fdb7d0 Rename safe mode to recovery mode (#102580) 2023-10-23 20:33:08 +02:00
Erik Montnemery 7a009ed6cd Don't duplicate core services in hassio (#102593) 2023-10-23 20:26:56 +02:00
tronikos c555fe4462 Refactor ZHA IkeaFan (#101858)
IkeaFan refactor
2023-10-23 14:01:25 -04:00
Michael Hansen a52761171f No cooldown when wake words have the same id (#101846)
* No cooldown when wake words have the same id

* Use wake word entity id in cooldown decision
2023-10-23 13:12:34 -04:00
elmurato 54bcd70878 Increase timeouts in Minecraft Server (#101784) 2023-10-23 15:49:48 +02:00
Bouwe Westerdijk c7d2499a52 Bump plugwise to v0.33.1 (#102052) 2023-10-23 15:47:12 +02:00
mkmer 40ccae3d07 Add coordinator to Blink (#102536) 2023-10-23 15:34:28 +02:00
Franck Nijhof 5b39a08feb Update adguardhome to 0.6.2 (#102582)
Update adguard to 0.6.2
2023-10-23 14:47:43 +02:00
Guido Schmitz 04c0bca487 Remove name from device info in devolo Home Network (#102585) 2023-10-23 14:00:47 +02:00
Marc Mueller 9c0427a7ac Update pylint to 3.0.2 (#102576) 2023-10-23 13:08:47 +02:00
Joost Lekkerkerker 42c062de68 Only add Withings sleep sensors when we have data (#102578)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-23 12:59:13 +02:00
Erik Montnemery d5af6c595d Fix runaway regex in translations.develop (#102386)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-10-23 12:34:32 +02:00
Maikel Punie 8c9c915c45 Bump code-quality to silver for duotecno (#102284)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-10-23 12:14:10 +02:00
Joost Lekkerkerker 8a7de27946 Try negative WAQI station number before aborting (#102550) 2023-10-23 11:55:12 +02:00
TopdRob e27baedf32 Bump adax to 0.3.0 (#102556) 2023-10-23 11:21:33 +02:00
dependabot[bot] 3e23a4b4ee Bump github/codeql-action from 2.22.3 to 2.22.4 (#102566)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-23 11:11:06 +02:00
Joost Lekkerkerker 4c99d2607f Fix fibaro tests (#102575) 2023-10-23 11:04:21 +02:00
Maikel Punie 109819e9cd Only allow a single duotecno config entry (#102478) 2023-10-23 10:58:11 +02:00
Robert Resch 0c5b963847 Add lokalise multi reference check to hassfest (#101876) 2023-10-23 10:57:19 +02:00
Franck Nijhof e4af09d261 Update base image to 2023.10.1 (#102568) 2023-10-23 10:48:05 +02:00
Joost Lekkerkerker a6ade59133 Make Withings sleep sensor only show last night (#101993) 2023-10-23 10:39:17 +02:00
612 changed files with 22022 additions and 9277 deletions
+2 -1
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/**
@@ -96,8 +97,8 @@ components: &components
- homeassistant/components/persistent_notification/**
- homeassistant/components/person/**
- homeassistant/components/recorder/**
- homeassistant/components/recovery_mode/**
- homeassistant/components/repairs/**
- homeassistant/components/safe_mode/**
- homeassistant/components/script/**
- homeassistant/components/shopping_list/**
- homeassistant/components/ssdp/**
+2 -3
View File
@@ -286,9 +286,6 @@ omit =
homeassistant/components/edl21/__init__.py
homeassistant/components/edl21/sensor.py
homeassistant/components/egardia/*
homeassistant/components/eight_sleep/__init__.py
homeassistant/components/eight_sleep/binary_sensor.py
homeassistant/components/eight_sleep/sensor.py
homeassistant/components/electric_kiwi/__init__.py
homeassistant/components/electric_kiwi/api.py
homeassistant/components/electric_kiwi/oauth2.py
@@ -992,6 +989,7 @@ omit =
homeassistant/components/pushsafer/notify.py
homeassistant/components/pyload/sensor.py
homeassistant/components/qbittorrent/__init__.py
homeassistant/components/qbittorrent/coordinator.py
homeassistant/components/qbittorrent/sensor.py
homeassistant/components/qnap/__init__.py
homeassistant/components/qnap/coordinator.py
@@ -1267,6 +1265,7 @@ omit =
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/climate.py
homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/switch.py
+2 -2
View File
@@ -29,11 +29,11 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v2.22.3
uses: github/codeql-action/init@v2.22.4
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2.22.3
uses: github/codeql-action/analyze@v2.22.4
with:
category: "/language:python"
+1
View File
@@ -204,6 +204,7 @@ homeassistant.components.light.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*
homeassistant.components.local_ip.*
homeassistant.components.local_todo.*
homeassistant.components.lock.*
homeassistant.components.logbook.*
homeassistant.components.logger.*
+12 -6
View File
@@ -319,8 +319,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/efergy/ @tkdrob
/tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt
/homeassistant/components/eight_sleep/ @mezz64 @raman325
/tests/components/eight_sleep/ @mezz64 @raman325
/homeassistant/components/electrasmart/ @jafar-atili
/tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000
@@ -423,8 +421,8 @@ build.json @home-assistant/supervisor
/tests/components/fritzbox/ @mib1185 @flabbamann
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
/tests/components/fritzbox_callmonitor/ @cdce8p
/homeassistant/components/fronius/ @nielstron @farmio
/tests/components/fronius/ @nielstron @farmio
/homeassistant/components/fronius/ @farmio
/tests/components/fronius/ @farmio
/homeassistant/components/frontend/ @home-assistant/frontend
/tests/components/frontend/ @home-assistant/frontend
/homeassistant/components/frontier_silicon/ @wlcrs
@@ -479,6 +477,8 @@ build.json @home-assistant/supervisor
/tests/components/google_mail/ @tkdrob
/homeassistant/components/google_sheets/ @tkdrob
/tests/components/google_sheets/ @tkdrob
/homeassistant/components/google_tasks/ @allenporter
/tests/components/google_tasks/ @allenporter
/homeassistant/components/google_travel_time/ @eifinger
/tests/components/google_travel_time/ @eifinger
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
@@ -586,6 +586,8 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @zxdavb
/homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01
@@ -708,6 +710,8 @@ build.json @home-assistant/supervisor
/tests/components/local_calendar/ @allenporter
/homeassistant/components/local_ip/ @issacg
/tests/components/local_ip/ @issacg
/homeassistant/components/local_todo/ @allenporter
/tests/components/local_todo/ @allenporter
/homeassistant/components/lock/ @home-assistant/core
/tests/components/lock/ @home-assistant/core
/homeassistant/components/logbook/ @home-assistant/core
@@ -1035,6 +1039,8 @@ build.json @home-assistant/supervisor
/tests/components/recollect_waste/ @bachya
/homeassistant/components/recorder/ @home-assistant/core
/tests/components/recorder/ @home-assistant/core
/homeassistant/components/recovery_mode/ @home-assistant/core
/tests/components/recovery_mode/ @home-assistant/core
/homeassistant/components/rejseplanen/ @DarkFox
/homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core
@@ -1085,8 +1091,6 @@ build.json @home-assistant/supervisor
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
/homeassistant/components/sabnzbd/ @shaiu
/tests/components/sabnzbd/ @shaiu
/homeassistant/components/safe_mode/ @home-assistant/core
/tests/components/safe_mode/ @home-assistant/core
/homeassistant/components/saj/ @fredericvl
/homeassistant/components/samsungtv/ @chemelli74 @epenet
/tests/components/samsungtv/ @chemelli74 @epenet
@@ -1303,6 +1307,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/time_date/ @fabaff
/tests/components/time_date/ @fabaff
/homeassistant/components/tmb/ @alemuro
/homeassistant/components/todo/ @home-assistant/core
/tests/components/todo/ @home-assistant/core
/homeassistant/components/todoist/ @boralyl
/tests/components/todoist/ @boralyl
/homeassistant/components/tolo/ @MatthiasLohr
+5 -5
View File
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.10.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.10.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.10.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.10.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
+8 -3
View File
@@ -93,7 +93,9 @@ def get_arguments() -> argparse.Namespace:
help="Directory that contains the Home Assistant configuration",
)
parser.add_argument(
"--safe-mode", action="store_true", help="Start Home Assistant in safe mode"
"--recovery-mode",
action="store_true",
help="Start Home Assistant in recovery mode",
)
parser.add_argument(
"--debug", action="store_true", help="Start Home Assistant in debug mode"
@@ -183,7 +185,9 @@ def main() -> int:
ensure_config_path(config_dir)
# pylint: disable-next=import-outside-toplevel
from . import runner
from . import config, runner
safe_mode = config.safe_mode_enabled(config_dir)
runtime_conf = runner.RuntimeConfig(
config_dir=config_dir,
@@ -193,9 +197,10 @@ def main() -> int:
log_no_color=args.log_no_color,
skip_pip=args.skip_pip,
skip_pip_packages=args.skip_pip_packages,
safe_mode=args.safe_mode,
recovery_mode=args.recovery_mode,
debug=args.debug,
open_ui=args.open_ui,
safe_mode=safe_mode,
)
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
+2
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,
+15 -12
View File
@@ -120,6 +120,7 @@ async def async_setup_hass(
runtime_config.log_no_color,
)
hass.config.safe_mode = runtime_config.safe_mode
hass.config.skip_pip = runtime_config.skip_pip
hass.config.skip_pip_packages = runtime_config.skip_pip_packages
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
@@ -137,14 +138,14 @@ async def async_setup_hass(
config_dict = None
basic_setup_success = False
if not (safe_mode := runtime_config.safe_mode):
if not (recovery_mode := runtime_config.recovery_mode):
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try:
config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(
"Failed to parse configuration.yaml: %s. Activating safe mode",
"Failed to parse configuration.yaml: %s. Activating recovery mode",
err,
)
else:
@@ -156,24 +157,24 @@ async def async_setup_hass(
)
if config_dict is None:
safe_mode = True
recovery_mode = True
elif not basic_setup_success:
_LOGGER.warning("Unable to set up core integrations. Activating safe mode")
safe_mode = True
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
recovery_mode = True
elif (
"frontend" in hass.data.get(DATA_SETUP, {})
and "frontend" not in hass.config.components
):
_LOGGER.warning("Detected that frontend did not load. Activating safe mode")
_LOGGER.warning("Detected that frontend did not load. Activating recovery mode")
# Ask integrations to shut down. It's messy but we can't
# do a clean stop without knowing what is broken
with contextlib.suppress(asyncio.TimeoutError):
async with hass.timeout.async_timeout(10):
await hass.async_stop()
safe_mode = True
recovery_mode = True
old_config = hass.config
old_logging = hass.data.get(DATA_LOGGING)
@@ -187,16 +188,18 @@ async def async_setup_hass(
# Setup loader cache after the config dir has been set
loader.async_setup(hass)
if safe_mode:
_LOGGER.info("Starting in safe mode")
hass.config.safe_mode = True
if recovery_mode:
_LOGGER.info("Starting in recovery mode")
hass.config.recovery_mode = True
http_conf = (await http.async_get_last_config(hass)) or {}
await async_from_config_dict(
{"safe_mode": {}, "http": http_conf},
{"recovery_mode": {}, "http": http_conf},
hass,
)
elif hass.config.safe_mode:
_LOGGER.info("Starting in safe mode")
if runtime_config.open_ui:
hass.add_job(open_hass_ui, hass)
@@ -471,7 +474,7 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
# Add config entry domains
if not hass.config.safe_mode:
if not hass.config.recovery_mode:
domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded
+1
View File
@@ -11,6 +11,7 @@
"google_maps",
"google_pubsub",
"google_sheets",
"google_tasks",
"google_translate",
"google_travel_time",
"google_wifi",
+1 -1
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"]
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"quality_scale": "platinum",
"requirements": ["accuweather==1.0.0"]
"requirements": ["accuweather==2.1.0"]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.2.0", "Adax-local==0.1.5"]
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.6.1"]
"requirements": ["adguardhome==0.6.2"]
}
@@ -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"]
}
+12 -6
View File
@@ -630,12 +630,16 @@ class AlexaColorController(AlexaCapability):
if name != "color":
raise UnsupportedProperty(name)
hue, saturation = self.entity.attributes.get(light.ATTR_HS_COLOR, (0, 0))
hue_saturation: tuple[float, float] | None
if (hue_saturation := self.entity.attributes.get(light.ATTR_HS_COLOR)) is None:
hue_saturation = (0, 0)
if (brightness := self.entity.attributes.get(light.ATTR_BRIGHTNESS)) is None:
brightness = 0
return {
"hue": hue,
"saturation": saturation / 100.0,
"brightness": self.entity.attributes.get(light.ATTR_BRIGHTNESS, 0) / 255.0,
"hue": hue_saturation[0],
"saturation": hue_saturation[1] / 100.0,
"brightness": brightness / 255.0,
}
@@ -853,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(" ", "")
)
@@ -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"
]
}
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.13.4"],
"requirements": ["pyatv==0.14.3"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
@@ -371,11 +371,15 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
@property
def repeat(self) -> RepeatMode | None:
"""Return current repeat mode."""
if self._playing and self._is_feature_available(FeatureName.Repeat):
if (
self._playing
and self._is_feature_available(FeatureName.Repeat)
and (repeat := self._playing.repeat)
):
return {
RepeatState.Track: RepeatMode.ONE,
RepeatState.All: RepeatMode.ALL,
}.get(self._playing.repeat, RepeatMode.OFF)
}.get(repeat, RepeatMode.OFF)
return None
@property
+16 -1
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")
@@ -9,7 +9,7 @@ from homeassistant.components import stt
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DOMAIN
from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DATA_LAST_WAKE_UP, DOMAIN
from .error import PipelineNotFound
from .pipeline import (
AudioSettings,
@@ -58,6 +58,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Assist pipeline integration."""
hass.data[DATA_CONFIG] = config.get(DOMAIN, {})
# wake_word_id -> timestamp of last detection (monotonic_ns)
hass.data[DATA_LAST_WAKE_UP] = {}
await async_setup_pipeline_store(hass)
async_register_websocket_api(hass)
@@ -12,11 +12,13 @@ from pathlib import Path
from queue import Queue
from threading import Thread
import time
from typing import Any, Final, cast
from typing import TYPE_CHECKING, Any, Final, cast
import wave
import voluptuous as vol
from webrtc_noise_gain import AudioProcessor
if TYPE_CHECKING:
from webrtc_noise_gain import AudioProcessor
from homeassistant.components import (
conversation,
@@ -522,6 +524,12 @@ class PipelineRun:
# Initialize with audio settings
self.audio_processor_buffer = AudioBuffer(AUDIO_PROCESSOR_BYTES)
if self.audio_settings.needs_processor:
# Delay import of webrtc so HA start up is not crashing
# on older architectures (armhf).
#
# pylint: disable=import-outside-toplevel
from webrtc_noise_gain import AudioProcessor
self.audio_processor = AudioProcessor(
self.audio_settings.auto_gain_dbfs,
self.audio_settings.noise_suppression_level,
@@ -681,7 +689,8 @@ class PipelineRun:
wake_word_output: dict[str, Any] = {}
else:
# Avoid duplicate detections by checking cooldown
last_wake_up = self.hass.data.get(DATA_LAST_WAKE_UP)
wake_up_key = f"{self.wake_word_entity_id}.{result.wake_word_id}"
last_wake_up = self.hass.data[DATA_LAST_WAKE_UP].get(wake_up_key)
if last_wake_up is not None:
sec_since_last_wake_up = time.monotonic() - last_wake_up
if sec_since_last_wake_up < wake_word_settings.cooldown_seconds:
@@ -689,7 +698,7 @@ class PipelineRun:
raise WakeWordDetectionAborted
# Record last wake up time to block duplicate detections
self.hass.data[DATA_LAST_WAKE_UP] = time.monotonic()
self.hass.data[DATA_LAST_WAKE_UP][wake_up_key] = time.monotonic()
if result.queued_audio:
# Add audio that was pending at detection.
@@ -7,8 +7,6 @@ from dataclasses import dataclass
from enum import StrEnum
from typing import Final, cast
from webrtc_noise_gain import AudioProcessor
_SAMPLE_RATE: Final = 16000 # Hz
_SAMPLE_WIDTH: Final = 2 # bytes
@@ -51,6 +49,12 @@ class WebRtcVad(VoiceActivityDetector):
def __init__(self) -> None:
"""Initialize webrtcvad."""
# Delay import of webrtc so HA start up is not crashing
# on older architectures (armhf).
#
# pylint: disable=import-outside-toplevel
from webrtc_noise_gain import AudioProcessor
# Just VAD: no noise suppression or auto gain
self._audio_processor = AudioProcessor(0, 0)
@@ -10,6 +10,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DATA_ASUSWRT, DOMAIN
from .router import AsusWrtDevInfo, AsusWrtRouter
ATTR_LAST_TIME_REACHABLE = "last_time_reachable"
DEFAULT_DEVICE_NAME = "Unknown device"
@@ -52,6 +54,8 @@ def add_entities(
class AsusWrtDevice(ScannerEntity):
"""Representation of a AsusWrt device."""
_unrecorded_attributes = frozenset({ATTR_LAST_TIME_REACHABLE})
_attr_should_poll = False
def __init__(self, router: AsusWrtRouter, device: AsusWrtDevInfo) -> None:
@@ -97,7 +101,7 @@ class AsusWrtDevice(ScannerEntity):
self._attr_extra_state_attributes = {}
if self._device.last_activity:
self._attr_extra_state_attributes[
"last_time_reachable"
ATTR_LAST_TIME_REACHABLE
] = self._device.last_activity.isoformat(timespec="seconds")
self.async_write_ha_state()
+3 -5
View File
@@ -25,13 +25,14 @@ from homeassistant.exceptions import (
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import aiohttp_client, device_registry as dr, discovery_flow
from homeassistant.helpers import device_registry as dr, discovery_flow
from .activity import ActivityStream
from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin
from .util import async_create_august_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -46,10 +47,7 @@ YALEXS_BLE_DOMAIN = "yalexs_ble"
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up August from a config entry."""
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
session = aiohttp_client.async_create_clientsession(hass)
session = async_create_august_clientsession(hass)
august_gateway = AugustGateway(hass, session)
try:
@@ -13,7 +13,6 @@ from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
@@ -26,6 +25,7 @@ from .const import (
)
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
from .util import async_create_august_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -159,10 +159,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Set up the gateway."""
if self._august_gateway is not None:
return self._august_gateway
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
self._aiohttp_session = aiohttp_client.async_create_clientsession(self.hass)
self._aiohttp_session = async_create_august_clientsession(self.hass)
self._august_gateway = AugustGateway(self.hass, self._aiohttp_session)
return self._august_gateway
@@ -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"]
}
+24
View File
@@ -0,0 +1,24 @@
"""August util functions."""
import socket
import aiohttp
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client
@callback
def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSession:
"""Create an aiohttp session for the august integration."""
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
#
# The family is set to AF_INET because IPv6 keeps coming up as an issue
# see https://github.com/home-assistant/core/issues/97146
#
# When https://github.com/aio-libs/aiohttp/issues/4451 is implemented
# we can allow IPv6 again
#
return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET)
+27 -32
View File
@@ -31,6 +31,7 @@ from .const import (
SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN,
)
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -84,6 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
auth_data = deepcopy(dict(entry.data))
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)
try:
await blink.start()
@@ -94,18 +96,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Attempting a reauth flow")
raise ConfigEntryAuthFailed("Need 2FA for Blink")
hass.data[DOMAIN][entry.entry_id] = blink
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))
await blink.refresh(force=True)
async def blink_refresh(event_time=None):
"""Call blink to refresh info."""
await hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True)
await coordinator.api.refresh(force_cache=True)
async def async_save_video(call):
"""Call save video service handler."""
@@ -118,8 +120,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def send_pin(call):
"""Call blink to send new pin."""
pin = call.data[CONF_PIN]
await hass.data[DOMAIN][entry.entry_id].auth.send_auth_key(
hass.data[DOMAIN][entry.entry_id],
await coordinator.api.auth.send_auth_key(
hass.data[DOMAIN][entry.entry_id].api,
pin,
)
@@ -154,26 +156,21 @@ def _async_import_options_from_data_if_missing(
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Blink entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
return True
if not unload_ok:
return False
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO)
hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN)
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) != 0:
return True
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO)
hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN)
return True
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
blink: Blink = hass.data[DOMAIN][entry.entry_id]
blink: Blink = hass.data[DOMAIN][entry.entry_id].api
blink.refresh_rate = entry.options[CONF_SCAN_INTERVAL]
@@ -186,13 +183,12 @@ async def async_handle_save_video_service(
if not hass.config.is_allowed_path(video_path):
_LOGGER.error("Can't write %s, no access to path!", video_path)
return
try:
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if camera_name in all_cameras:
all_cameras = hass.data[DOMAIN][entry.entry_id].api.cameras
if camera_name in all_cameras:
try:
await all_cameras[camera_name].video_to_file(video_path)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
async def async_handle_save_recent_clips_service(
@@ -204,10 +200,9 @@ async def async_handle_save_recent_clips_service(
if not hass.config.is_allowed_path(clips_dir):
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
return
try:
all_cameras = hass.data[DOMAIN][entry.entry_id].cameras
if camera_name in all_cameras:
all_cameras = hass.data[DOMAIN][entry.entry_id].api.cameras
if camera_name in all_cameras:
try:
await all_cameras[camera_name].save_recent_clips(output_dir=clips_dir)
except OSError as err:
_LOGGER.error("Can't write recent clips to directory: %s", err)
except OSError as err:
_LOGGER.error("Can't write recent clips to directory: %s", err)
@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import logging
from blinkpy.blinkpy import Blink
from blinkpy.blinkpy import Blink, BlinkSyncModule
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
@@ -16,12 +16,14 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -32,76 +34,75 @@ async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Blink Alarm Control Panels."""
data = hass.data[DOMAIN][config.entry_id]
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
sync_modules = []
for sync_name, sync_module in data.sync.items():
sync_modules.append(BlinkSyncModuleHA(data, sync_name, sync_module))
async_add_entities(sync_modules, update_before_add=True)
for sync_name, sync_module in coordinator.api.sync.items():
sync_modules.append(BlinkSyncModuleHA(coordinator, sync_name, sync_module))
async_add_entities(sync_modules)
class BlinkSyncModuleHA(AlarmControlPanelEntity):
class BlinkSyncModuleHA(
CoordinatorEntity[BlinkUpdateCoordinator], AlarmControlPanelEntity
):
"""Representation of a Blink Alarm Control Panel."""
_attr_icon = ICON
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
_attr_name = None
_attr_has_entity_name = True
_attr_name = None
def __init__(self, data, name: str, sync) -> None:
def __init__(
self, coordinator: BlinkUpdateCoordinator, name: str, sync: BlinkSyncModule
) -> None:
"""Initialize the alarm control panel."""
self.data: Blink = data
super().__init__(coordinator)
self.api: Blink = coordinator.api
self._coordinator = coordinator
self.sync = sync
self._name: str = name
self._attr_unique_id: str = sync.serial
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, sync.serial)},
name=f"{DOMAIN} {name}",
manufacturer=DEFAULT_BRAND,
serial_number=sync.serial,
)
self._update_attr()
async def async_update(self) -> None:
"""Update the state of the device."""
if self.data.check_if_ok_to_update():
_LOGGER.debug(
"Initiating a blink.refresh() from BlinkSyncModule('%s') (%s)",
self._name,
self.data,
)
try:
await self.data.refresh(force=True)
self._attr_available = True
except asyncio.TimeoutError:
self._attr_available = False
@callback
def _handle_coordinator_update(self) -> None:
"""Handle coordinator update."""
self._update_attr()
super()._handle_coordinator_update()
_LOGGER.info("Updating State of Blink Alarm Control Panel '%s'", self._name)
self.sync.attributes["network_info"] = self.data.networks
@callback
def _update_attr(self) -> None:
"""Update attributes for alarm control panel."""
self.sync.attributes["network_info"] = self.api.networks
self.sync.attributes["associated_cameras"] = list(self.sync.cameras)
self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
self._attr_extra_state_attributes = self.sync.attributes
@property
def state(self) -> StateType:
"""Return state of alarm."""
return STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
self._attr_state = (
STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
)
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
try:
await self.sync.async_arm(False)
await self.sync.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
self.async_write_ha_state()
except asyncio.TimeoutError as er:
raise HomeAssistantError("Blink failed to disarm camera") from er
await self._coordinator.async_refresh()
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm command."""
try:
await self.sync.async_arm(True)
await self.sync.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
except asyncio.TimeoutError as er:
raise HomeAssistantError("Blink failed to arm camera away") from er
await self._coordinator.async_refresh()
self.async_write_ha_state()
+28 -14
View File
@@ -10,9 +10,10 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DEFAULT_BRAND,
@@ -21,6 +22,7 @@ from .const import (
TYPE_CAMERA_ARMED,
TYPE_MOTION_DETECTED,
)
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -45,39 +47,51 @@ async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the blink binary sensors."""
data = hass.data[DOMAIN][config.entry_id]
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkBinarySensor(data, camera, description)
for camera in data.cameras
BlinkBinarySensor(coordinator, camera, description)
for camera in coordinator.api.cameras
for description in BINARY_SENSORS_TYPES
]
async_add_entities(entities)
class BlinkBinarySensor(BinarySensorEntity):
class BlinkBinarySensor(CoordinatorEntity[BlinkUpdateCoordinator], BinarySensorEntity):
"""Representation of a Blink binary sensor."""
_attr_has_entity_name = True
def __init__(
self, data, camera, description: BinarySensorEntityDescription
self,
coordinator: BlinkUpdateCoordinator,
camera,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.data = data
super().__init__(coordinator)
self.entity_description = description
self._camera = data.cameras[camera]
self._attr_unique_id = f"{self._camera.serial}-{description.key}"
self._camera = coordinator.api.cameras[camera]
serial = self._camera.serial
self._attr_unique_id = f"{serial}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._camera.serial)},
identifiers={(DOMAIN, serial)},
serial_number=serial,
name=camera,
manufacturer=DEFAULT_BRAND,
model=self._camera.camera_type,
)
self._update_attrs()
@property
def is_on(self) -> bool | None:
"""Update sensor state."""
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update from data coordinator."""
self._update_attrs()
super()._handle_coordinator_update()
@callback
def _update_attrs(self) -> None:
"""Update attributes for binary sensor."""
is_on = self._camera.attributes[self.entity_description.key]
_LOGGER.debug(
"'%s' %s = %s",
@@ -87,4 +101,4 @@ class BlinkBinarySensor(BinarySensorEntity):
)
if self.entity_description.key == TYPE_BATTERY:
is_on = is_on != "ok"
return is_on
self._attr_is_on = is_on
+25 -12
View File
@@ -12,25 +12,30 @@ from requests.exceptions import ChunkedEncodingError
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
ATTR_VIDEO_CLIP = "video"
ATTR_IMAGE = "image"
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a Blink Camera."""
data = hass.data[DOMAIN][config.entry_id]
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkCamera(data, name, camera) for name, camera in data.cameras.items()
BlinkCamera(coordinator, name, camera)
for name, camera in coordinator.api.cameras.items()
]
async_add_entities(entities)
@@ -39,20 +44,22 @@ async def async_setup_entry(
platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera")
class BlinkCamera(Camera):
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
"""An implementation of a Blink Camera."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, data, name, camera) -> None:
def __init__(self, coordinator: BlinkUpdateCoordinator, name, camera) -> None:
"""Initialize a camera."""
super().__init__()
self.data = data
super().__init__(coordinator)
Camera.__init__(self)
self._coordinator = coordinator
self._camera = camera
self._attr_unique_id = f"{camera.serial}-camera"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, camera.serial)},
serial_number=camera.serial,
name=name,
manufacturer=DEFAULT_BRAND,
model=camera.camera_type,
@@ -68,17 +75,22 @@ class BlinkCamera(Camera):
"""Enable motion detection for the camera."""
try:
await self._camera.async_arm(True)
await self.data.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
except asyncio.TimeoutError as er:
raise HomeAssistantError("Blink failed to arm camera") from er
self._camera.motion_enabled = True
await self._coordinator.async_refresh()
async def async_disable_motion_detection(self) -> None:
"""Disable motion detection for the camera."""
try:
await self._camera.async_arm(False)
await self.data.refresh(force=True)
except asyncio.TimeoutError:
self._attr_available = False
except asyncio.TimeoutError as er:
raise HomeAssistantError("Blink failed to disarm camera") from er
self._camera.motion_enabled = False
await self._coordinator.async_refresh()
@property
def motion_detection_enabled(self) -> bool:
@@ -94,6 +106,7 @@ class BlinkCamera(Camera):
"""Trigger camera to take a snapshot."""
with contextlib.suppress(asyncio.TimeoutError):
await self._camera.snap_picture()
await self._coordinator.api.refresh()
self.async_write_ha_state()
def camera_image(
-1
View File
@@ -7,7 +7,6 @@ DEVICE_ID = "Home Assistant"
CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera"
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"
DEFAULT_BRAND = "Blink"
DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com"
DEFAULT_SCAN_INTERVAL = 300
@@ -0,0 +1,33 @@
"""Blink Coordinator."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from blinkpy.blinkpy import Blink
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""BlinkUpdateCoordinator - In charge of downloading the data for a site."""
def __init__(self, hass: HomeAssistant, api: Blink) -> None:
"""Initialize the data service."""
self.api = api
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Async update wrapper."""
return await self.api.refresh(force=True)
+1 -1
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"]
}
+32 -19
View File
@@ -1,8 +1,6 @@
"""Support for Blink system camera sensors."""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
import logging
from homeassistant.components.sensor import (
@@ -17,12 +15,13 @@ from homeassistant.const import (
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
from .coordinator import BlinkUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -49,44 +48,59 @@ async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Initialize a Blink sensor."""
data = hass.data[DOMAIN][config.entry_id]
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
entities = [
BlinkSensor(data, camera, description)
for camera in data.cameras
BlinkSensor(coordinator, camera, description)
for camera in coordinator.api.cameras
for description in SENSOR_TYPES
]
async_add_entities(entities)
class BlinkSensor(SensorEntity):
class BlinkSensor(CoordinatorEntity[BlinkUpdateCoordinator], SensorEntity):
"""A Blink camera sensor."""
_attr_has_entity_name = True
def __init__(self, data, camera, description: SensorEntityDescription) -> None:
def __init__(
self,
coordinator: BlinkUpdateCoordinator,
camera,
description: SensorEntityDescription,
) -> None:
"""Initialize sensors from Blink camera."""
super().__init__(coordinator)
self.entity_description = description
self.data = data
self._camera = data.cameras[camera]
self._attr_unique_id = f"{self._camera.serial}-{description.key}"
self._camera = coordinator.api.cameras[camera]
serial = self._camera.serial
self._attr_unique_id = f"{serial}-{description.key}"
self._sensor_key = (
"temperature_calibrated"
if description.key == "temperature"
else description.key
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._camera.serial)},
identifiers={(DOMAIN, serial)},
serial_number=serial,
name=f"{DOMAIN} {camera}",
manufacturer=DEFAULT_BRAND,
model=self._camera.camera_type,
)
self._update_attr()
@property
def native_value(self) -> StateType | date | datetime | Decimal:
"""Retrieve sensor data from the camera."""
@callback
def _handle_coordinator_update(self) -> None:
"""Handle coordinator update."""
self._update_attr()
super()._handle_coordinator_update()
@callback
def _update_attr(self) -> None:
"""Update attributes for sensor."""
try:
native_value = self._camera.attributes[self._sensor_key]
self._attr_native_value = self._camera.attributes[self._sensor_key]
_LOGGER.debug(
"'%s' %s = %s",
self._camera.attributes["name"],
@@ -94,8 +108,7 @@ class BlinkSensor(SensorEntity):
self._attr_native_value,
)
except KeyError:
native_value = None
self._attr_native_value = None
_LOGGER.error(
"%s not a valid camera attribute. Did the API change?", self._sensor_key
)
return native_value
@@ -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:
@@ -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
@@ -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"
]
}
@@ -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
+11 -3
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
@@ -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"]
}
@@ -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.",
@@ -383,6 +383,7 @@ async def async_setup_entry(
device_info = DeviceInfo(
configuration_url=f"http://{entry.data[CONF_HOST]}/",
identifiers={(DOMAIN, coordinator.data.serial)},
serial_number=coordinator.data.serial,
manufacturer="Brother",
model=coordinator.data.model,
name=coordinator.data.model,
+6 -1
View File
@@ -14,7 +14,11 @@ from homeassistant.components.bluetooth import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry, async_get
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceRegistry,
async_get,
)
from .const import (
BTHOME_BLE_EVENT,
@@ -55,6 +59,7 @@ def process_service_info(
sensor_device_info = update.devices[device_key.device_id]
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_BLUETOOTH, address)},
identifiers={(BLUETOOTH_DOMAIN, address)},
manufacturer=sensor_device_info.manufacturer,
model=sensor_device_info.model,
@@ -24,7 +24,7 @@
"location": {
"name": "Location"
},
"messages": {
"message": {
"name": "Message"
},
"start_time": {
+1 -1
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."]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.73.0"]
"requirements": ["hass-nabucasa==0.74.0"]
}
@@ -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
]
+3 -1
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",
+22 -20
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
@@ -1,11 +1,9 @@
"""Support for Comelit."""
import asyncio
from datetime import timedelta
from typing import Any
from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject
from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject, exceptions
from aiocomelit.const import BRIDGE
import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -70,14 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator):
async def _async_update_data(self) -> dict[str, Any]:
"""Update device data."""
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
logged = False
try:
logged = await self.api.login()
except (asyncio.exceptions.TimeoutError, aiohttp.ClientConnectorError) as err:
await self.api.login()
except exceptions.CannotConnect as err:
_LOGGER.warning("Connection error for %s", self._host)
await self.api.close()
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
finally:
if not logged:
raise ConfigEntryAuthFailed
except exceptions.CannotAuthenticate:
raise ConfigEntryAuthFailed
return await self.api.get_all_devices()
+3 -2
View File
@@ -51,7 +51,8 @@ class ComelitCoverEntity(
self._api = coordinator.api
self._device = device
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id because no serial number or mac is available
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device)
# Device doesn't provide a status so we assume UNKNOWN at first startup
@@ -108,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
+2 -1
View File
@@ -47,7 +47,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
self._api = coordinator.api
self._device = device
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id because no serial number or mac is available
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device)
@@ -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"]
}
+2 -1
View File
@@ -66,7 +66,8 @@ class ComelitSensorEntity(CoordinatorEntity[ComelitSerialBridge], SensorEntity):
self._api = coordinator.api
self._device = device
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id because no serial number or mac is available
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device)
+2 -1
View File
@@ -53,7 +53,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
self._api = coordinator.api
self._device = device
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id because no serial number or mac is available
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{device.type}-{device.index}"
self._attr_device_info = coordinator.platform_device_info(device)
if device.type == OTHER:
+42 -1
View File
@@ -38,7 +38,27 @@ from .deconz_device import DeconzDevice
from .gateway import DeconzGateway, get_gateway_from_config_entry
DECONZ_GROUP = "is_deconz_group"
EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: LightEffect.COLOR_LOOP, "None": LightEffect.NONE}
EFFECT_TO_DECONZ = {
EFFECT_COLORLOOP: LightEffect.COLOR_LOOP,
"None": LightEffect.NONE,
# Specific to Lidl christmas light
"carnival": LightEffect.CARNIVAL,
"collide": LightEffect.COLLIDE,
"fading": LightEffect.FADING,
"fireworks": LightEffect.FIREWORKS,
"flag": LightEffect.FLAG,
"glow": LightEffect.GLOW,
"rainbow": LightEffect.RAINBOW,
"snake": LightEffect.SNAKE,
"snow": LightEffect.SNOW,
"sparkles": LightEffect.SPARKLES,
"steady": LightEffect.STEADY,
"strobe": LightEffect.STROBE,
"twinkle": LightEffect.TWINKLE,
"updown": LightEffect.UPDOWN,
"vintage": LightEffect.VINTAGE,
"waves": LightEffect.WAVES,
}
FLASH_TO_DECONZ = {FLASH_SHORT: LightAlert.SHORT, FLASH_LONG: LightAlert.LONG}
DECONZ_TO_COLOR_MODE = {
@@ -47,6 +67,25 @@ DECONZ_TO_COLOR_MODE = {
LightColorMode.XY: ColorMode.XY,
}
TS0601_EFFECTS = [
"carnival",
"collide",
"fading",
"fireworks",
"flag",
"glow",
"rainbow",
"snake",
"snow",
"sparkles",
"steady",
"strobe",
"twinkle",
"updown",
"vintage",
"waves",
]
_LightDeviceT = TypeVar("_LightDeviceT", bound=Group | Light)
@@ -161,6 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
if device.effect is not None:
self._attr_supported_features |= LightEntityFeature.EFFECT
self._attr_effect_list = [EFFECT_COLORLOOP]
if device.model_id == "TS0601":
self._attr_effect_list += TS0601_EFFECTS
@property
def color_mode(self) -> str | None:
@@ -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]}'"
@@ -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:
@@ -52,7 +52,6 @@ class DevoloEntity(Entity):
identifiers={(DOMAIN, str(device.serial_number))},
manufacturer="devolo",
model=device.product,
name=entry.title,
serial_number=device.serial_number,
sw_version=device.firmware_version,
)
@@ -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":
-3
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"
+1 -1
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"]
}
+30 -12
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
@@ -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(
@@ -34,6 +34,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors: dict[str, str] = {}
if user_input is not None:
try:
@@ -5,5 +5,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/duotecno",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pyDuotecno==2023.10.1"]
}
@@ -12,7 +12,8 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"unknown": "[%key:common::config_flow::error::unknown%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"entity": {
@@ -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"]
}
@@ -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"]
}
+22 -207
View File
@@ -1,222 +1,37 @@
"""Support for Eight smart mattress covers and mattresses."""
"""The Eight Sleep integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from pyeight.eight import EightSleep
from pyeight.exceptions import RequestError
from pyeight.user import EightUser
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
ATTR_HW_VERSION,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SW_VERSION,
CONF_PASSWORD,
CONF_USERNAME,
Platform,
)
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo, async_get
from homeassistant.helpers.typing import UNDEFINED, ConfigType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN, NAME_MAP
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
HEAT_SCAN_INTERVAL = timedelta(seconds=60)
USER_SCAN_INTERVAL = timedelta(seconds=300)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
),
},
extra=vol.ALLOW_EXTRA,
)
DOMAIN = "eight_sleep"
@dataclass
class EightSleepConfigEntryData:
"""Data used for all entities for a given config entry."""
api: EightSleep
heat_coordinator: DataUpdateCoordinator
user_coordinator: DataUpdateCoordinator
def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) -> str:
"""Get the device's unique ID."""
unique_id = eight.device_id
assert unique_id
if user_obj:
unique_id = f"{unique_id}.{user_obj.user_id}.{user_obj.side}"
return unique_id
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Old set up method for the Eight Sleep component."""
if DOMAIN in config:
_LOGGER.warning(
"Your Eight Sleep configuration has been imported into the UI; "
"please remove it from configuration.yaml as support for it "
"will be removed in a future release"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Eight Sleep config entry."""
eight = EightSleep(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
hass.config.time_zone,
client_session=async_get_clientsession(hass),
)
# Authenticate, build sensors
try:
success = await eight.start()
except RequestError as err:
raise ConfigEntryNotReady from err
if not success:
# Authentication failed, cannot continue
return False
heat_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Eight Sleep from a config entry."""
ir.async_create_issue(
hass,
_LOGGER,
name=f"{DOMAIN}_heat",
update_interval=HEAT_SCAN_INTERVAL,
update_method=eight.update_device_data,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/eight_sleep"
},
)
user_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"{DOMAIN}_user",
update_interval=USER_SCAN_INTERVAL,
update_method=eight.update_user_data,
)
await heat_coordinator.async_config_entry_first_refresh()
await user_coordinator.async_config_entry_first_refresh()
if not eight.users:
# No users, cannot continue
return False
dev_reg = async_get(hass)
assert eight.device_data
device_data = {
ATTR_MANUFACTURER: "Eight Sleep",
ATTR_MODEL: eight.device_data.get("modelString", UNDEFINED),
ATTR_HW_VERSION: eight.device_data.get("sensorInfo", {}).get(
"hwRevision", UNDEFINED
),
ATTR_SW_VERSION: eight.device_data.get("firmwareVersion", UNDEFINED),
}
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, _get_device_unique_id(eight))},
name=f"{entry.data[CONF_USERNAME]}'s Eight Sleep",
**device_data,
)
for user in eight.users.values():
assert user.user_profile
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, _get_device_unique_id(eight, user))},
name=f"{user.user_profile['firstName']}'s Eight Sleep Side",
via_device=(DOMAIN, _get_device_unique_id(eight)),
**device_data,
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EightSleepConfigEntryData(
eight, heat_coordinator, user_coordinator
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
# stop the API before unloading everything
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
await config_entry_data.api.stop()
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
return unload_ok
class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]):
"""The base Eight Sleep entity class."""
def __init__(
self,
entry: ConfigEntry,
coordinator: DataUpdateCoordinator,
eight: EightSleep,
user_id: str | None,
sensor: str,
) -> None:
"""Initialize the data object."""
super().__init__(coordinator)
self._config_entry = entry
self._eight = eight
self._user_id = user_id
self._sensor = sensor
self._user_obj: EightUser | None = None
if user_id:
self._user_obj = self._eight.users[user_id]
mapped_name = NAME_MAP.get(sensor, sensor.replace("_", " ").title())
if self._user_obj is not None:
assert self._user_obj.user_profile
name = f"{self._user_obj.user_profile['firstName']}'s {mapped_name}"
self._attr_name = name
else:
self._attr_name = f"Eight Sleep {mapped_name}"
unique_id = f"{_get_device_unique_id(eight, self._user_obj)}.{sensor}"
self._attr_unique_id = unique_id
identifiers = {(DOMAIN, _get_device_unique_id(eight, self._user_obj))}
self._attr_device_info = DeviceInfo(identifiers=identifiers)
async def async_heat_set(self, target: int, duration: int) -> None:
"""Handle eight sleep service calls."""
if self._user_obj is None:
raise HomeAssistantError(
"This entity does not support the heat set service."
)
await self._user_obj.set_heating_level(target, duration)
config_entry_data: EightSleepConfigEntryData = self.hass.data[DOMAIN][
self._config_entry.entry_id
]
await config_entry_data.heat_coordinator.async_request_refresh()
return True
@@ -1,65 +0,0 @@
"""Support for Eight Sleep binary sensors."""
from __future__ import annotations
import logging
from pyeight.eight import EightSleep
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import EightSleepBaseEntity, EightSleepConfigEntryData
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
BINARY_SENSORS = ["bed_presence"]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the eight sleep binary sensor."""
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
eight = config_entry_data.api
heat_coordinator = config_entry_data.heat_coordinator
async_add_entities(
EightHeatSensor(entry, heat_coordinator, eight, user.user_id, binary_sensor)
for user in eight.users.values()
for binary_sensor in BINARY_SENSORS
)
class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity):
"""Representation of a Eight Sleep heat-based sensor."""
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
def __init__(
self,
entry: ConfigEntry,
coordinator: DataUpdateCoordinator,
eight: EightSleep,
user_id: str | None,
sensor: str,
) -> None:
"""Initialize the sensor."""
super().__init__(entry, coordinator, eight, user_id, sensor)
assert self._user_obj
_LOGGER.debug(
"Presence Sensor: %s, Side: %s, User: %s",
sensor,
self._user_obj.side,
user_id,
)
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
assert self._user_obj
return bool(self._user_obj.bed_presence)
@@ -1,90 +1,11 @@
"""Config flow for Eight Sleep integration."""
from __future__ import annotations
"""The Eight Sleep integration config flow."""
import logging
from typing import Any
from homeassistant.config_entries import ConfigFlow
from pyeight.eight import EightSleep
from pyeight.exceptions import RequestError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(type=TextSelectorType.EMAIL)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
)
from . import DOMAIN
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class EightSleepConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Eight Sleep."""
VERSION = 1
async def _validate_data(self, config: dict[str, str]) -> str | None:
"""Validate input data and return any error."""
await self.async_set_unique_id(config[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
eight = EightSleep(
config[CONF_USERNAME],
config[CONF_PASSWORD],
self.hass.config.time_zone,
client_session=async_get_clientsession(self.hass),
)
try:
await eight.fetch_token()
except RequestError as err:
return str(err)
return None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
if (err := await self._validate_data(user_input)) is not None:
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors={"base": "cannot_connect"},
description_placeholders={"error": err},
)
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
async def async_step_import(self, import_config: dict) -> FlowResult:
"""Handle import."""
if (err := await self._validate_data(import_config)) is not None:
_LOGGER.error("Unable to import configuration.yaml configuration: %s", err)
return self.async_abort(
reason="cannot_connect", description_placeholders={"error": err}
)
return self.async_create_entry(
title=import_config[CONF_USERNAME], data=import_config
)
@@ -1,16 +0,0 @@
"""Eight Sleep constants."""
DOMAIN = "eight_sleep"
HEAT_ENTITY = "heat"
USER_ENTITY = "user"
NAME_MAP = {
"current_sleep": "Sleep Session",
"current_sleep_fitness": "Sleep Fitness",
"last_sleep": "Previous Sleep Session",
}
SERVICE_HEAT_SET = "heat_set"
ATTR_TARGET = "target"
ATTR_DURATION = "duration"
@@ -1,10 +1,9 @@
{
"domain": "eight_sleep",
"name": "Eight Sleep",
"codeowners": ["@mezz64", "@raman325"],
"config_flow": true,
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
"integration_type": "system",
"iot_class": "cloud_polling",
"loggers": ["pyeight"],
"requirements": ["pyEight==0.3.2"]
"requirements": []
}
@@ -1,301 +0,0 @@
"""Support for Eight Sleep sensors."""
from __future__ import annotations
import logging
from typing import Any
from pyeight.eight import EightSleep
import voluptuous as vol
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import EightSleepBaseEntity, EightSleepConfigEntryData
from .const import ATTR_DURATION, ATTR_TARGET, DOMAIN, SERVICE_HEAT_SET
ATTR_ROOM_TEMP = "Room Temperature"
ATTR_AVG_ROOM_TEMP = "Average Room Temperature"
ATTR_BED_TEMP = "Bed Temperature"
ATTR_AVG_BED_TEMP = "Average Bed Temperature"
ATTR_RESP_RATE = "Respiratory Rate"
ATTR_AVG_RESP_RATE = "Average Respiratory Rate"
ATTR_HEART_RATE = "Heart Rate"
ATTR_AVG_HEART_RATE = "Average Heart Rate"
ATTR_SLEEP_DUR = "Time Slept"
ATTR_LIGHT_PERC = f"Light Sleep {PERCENTAGE}"
ATTR_DEEP_PERC = f"Deep Sleep {PERCENTAGE}"
ATTR_REM_PERC = f"REM Sleep {PERCENTAGE}"
ATTR_TNT = "Tosses & Turns"
ATTR_SLEEP_STAGE = "Sleep Stage"
ATTR_TARGET_HEAT = "Target Heating Level"
ATTR_ACTIVE_HEAT = "Heating Active"
ATTR_DURATION_HEAT = "Heating Time Remaining"
ATTR_PROCESSING = "Processing"
ATTR_SESSION_START = "Session Start"
ATTR_FIT_DATE = "Fitness Date"
ATTR_FIT_DURATION_SCORE = "Fitness Duration Score"
ATTR_FIT_ASLEEP_SCORE = "Fitness Asleep Score"
ATTR_FIT_OUT_SCORE = "Fitness Out-of-Bed Score"
ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score"
_LOGGER = logging.getLogger(__name__)
EIGHT_USER_SENSORS = [
"current_sleep",
"current_sleep_fitness",
"last_sleep",
"bed_temperature",
"sleep_stage",
]
EIGHT_HEAT_SENSORS = ["bed_state"]
EIGHT_ROOM_SENSORS = ["room_temperature"]
VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100))
VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800))
SERVICE_EIGHT_SCHEMA = {
ATTR_TARGET: VALID_TARGET_HEAT,
ATTR_DURATION: VALID_DURATION,
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the eight sleep sensors."""
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
eight = config_entry_data.api
heat_coordinator = config_entry_data.heat_coordinator
user_coordinator = config_entry_data.user_coordinator
all_sensors: list[SensorEntity] = []
for obj in eight.users.values():
all_sensors.extend(
EightUserSensor(entry, user_coordinator, eight, obj.user_id, sensor)
for sensor in EIGHT_USER_SENSORS
)
all_sensors.extend(
EightHeatSensor(entry, heat_coordinator, eight, obj.user_id, sensor)
for sensor in EIGHT_HEAT_SENSORS
)
all_sensors.extend(
EightRoomSensor(entry, user_coordinator, eight, sensor)
for sensor in EIGHT_ROOM_SENSORS
)
async_add_entities(all_sensors)
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_HEAT_SET,
SERVICE_EIGHT_SCHEMA,
"async_heat_set",
)
class EightHeatSensor(EightSleepBaseEntity, SensorEntity):
"""Representation of an eight sleep heat-based sensor."""
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(
self,
entry: ConfigEntry,
coordinator: DataUpdateCoordinator,
eight: EightSleep,
user_id: str,
sensor: str,
) -> None:
"""Initialize the sensor."""
super().__init__(entry, coordinator, eight, user_id, sensor)
assert self._user_obj
_LOGGER.debug(
"Heat Sensor: %s, Side: %s, User: %s",
self._sensor,
self._user_obj.side,
self._user_id,
)
@property
def native_value(self) -> int | None:
"""Return the state of the sensor."""
assert self._user_obj
return self._user_obj.heating_level
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attributes."""
assert self._user_obj
return {
ATTR_TARGET_HEAT: self._user_obj.target_heating_level,
ATTR_ACTIVE_HEAT: self._user_obj.now_heating,
ATTR_DURATION_HEAT: self._user_obj.heating_remaining,
}
def _get_breakdown_percent(
attr: dict[str, Any], key: str, denominator: int | float
) -> int | float:
"""Get a breakdown percent."""
try:
return round((attr["breakdown"][key] / denominator) * 100, 2)
except (ZeroDivisionError, KeyError):
return 0
def _get_rounded_value(attr: dict[str, Any], key: str) -> int | float | None:
"""Get rounded value for given key."""
if (val := attr.get(key)) is None:
return None
return round(val, 2)
class EightUserSensor(EightSleepBaseEntity, SensorEntity):
"""Representation of an eight sleep user-based sensor."""
def __init__(
self,
entry: ConfigEntry,
coordinator: DataUpdateCoordinator,
eight: EightSleep,
user_id: str,
sensor: str,
) -> None:
"""Initialize the sensor."""
super().__init__(entry, coordinator, eight, user_id, sensor)
assert self._user_obj
if self._sensor == "bed_temperature":
self._attr_icon = "mdi:thermometer"
self._attr_device_class = SensorDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"):
self._attr_native_unit_of_measurement = "Score"
if self._sensor != "sleep_stage":
self._attr_state_class = SensorStateClass.MEASUREMENT
_LOGGER.debug(
"User Sensor: %s, Side: %s, User: %s",
self._sensor,
self._user_obj.side,
self._user_id,
)
@property
def native_value(self) -> str | int | float | None:
"""Return the state of the sensor."""
if not self._user_obj:
return None
if "current" in self._sensor:
if "fitness" in self._sensor:
return self._user_obj.current_sleep_fitness_score
return self._user_obj.current_sleep_score
if "last" in self._sensor:
return self._user_obj.last_sleep_score
if self._sensor == "bed_temperature":
return self._user_obj.current_values["bed_temp"]
if self._sensor == "sleep_stage":
return self._user_obj.current_values["stage"]
return None
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device state attributes."""
attr = None
if "current" in self._sensor and self._user_obj:
if "fitness" in self._sensor:
attr = self._user_obj.current_fitness_values
else:
attr = self._user_obj.current_values
elif "last" in self._sensor and self._user_obj:
attr = self._user_obj.last_values
if attr is None:
# Skip attributes if sensor type doesn't support
return None
if "fitness" in self._sensor:
state_attr = {
ATTR_FIT_DATE: attr["date"],
ATTR_FIT_DURATION_SCORE: attr["duration"],
ATTR_FIT_ASLEEP_SCORE: attr["asleep"],
ATTR_FIT_OUT_SCORE: attr["out"],
ATTR_FIT_WAKEUP_SCORE: attr["wakeup"],
}
return state_attr
state_attr = {ATTR_SESSION_START: attr["date"]}
state_attr[ATTR_TNT] = attr["tnt"]
state_attr[ATTR_PROCESSING] = attr["processing"]
if attr.get("breakdown") is not None:
sleep_time = sum(attr["breakdown"].values()) - attr["breakdown"]["awake"]
state_attr[ATTR_SLEEP_DUR] = sleep_time
state_attr[ATTR_LIGHT_PERC] = _get_breakdown_percent(
attr, "light", sleep_time
)
state_attr[ATTR_DEEP_PERC] = _get_breakdown_percent(
attr, "deep", sleep_time
)
state_attr[ATTR_REM_PERC] = _get_breakdown_percent(attr, "rem", sleep_time)
room_temp = _get_rounded_value(attr, "room_temp")
bed_temp = _get_rounded_value(attr, "bed_temp")
if "current" in self._sensor:
state_attr[ATTR_RESP_RATE] = _get_rounded_value(attr, "resp_rate")
state_attr[ATTR_HEART_RATE] = _get_rounded_value(attr, "heart_rate")
state_attr[ATTR_SLEEP_STAGE] = attr["stage"]
state_attr[ATTR_ROOM_TEMP] = room_temp
state_attr[ATTR_BED_TEMP] = bed_temp
elif "last" in self._sensor:
state_attr[ATTR_AVG_RESP_RATE] = _get_rounded_value(attr, "resp_rate")
state_attr[ATTR_AVG_HEART_RATE] = _get_rounded_value(attr, "heart_rate")
state_attr[ATTR_AVG_ROOM_TEMP] = room_temp
state_attr[ATTR_AVG_BED_TEMP] = bed_temp
return state_attr
class EightRoomSensor(EightSleepBaseEntity, SensorEntity):
"""Representation of an eight sleep room sensor."""
_attr_icon = "mdi:thermometer"
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
def __init__(
self,
entry,
coordinator: DataUpdateCoordinator,
eight: EightSleep,
sensor: str,
) -> None:
"""Initialize the sensor."""
super().__init__(entry, coordinator, eight, None, sensor)
@property
def native_value(self) -> int | float | None:
"""Return the state of the sensor."""
return self._eight.room_temperature
@@ -1,20 +0,0 @@
heat_set:
target:
entity:
integration: eight_sleep
domain: sensor
fields:
duration:
required: true
selector:
number:
min: 0
max: 28800
unit_of_measurement: seconds
target:
required: true
selector:
number:
min: -100
max: 100
unit_of_measurement: "°"
@@ -1,35 +1,8 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "Cannot connect to Eight Sleep cloud: {error}"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:component::eight_sleep::config::error::cannot_connect%]"
}
},
"services": {
"heat_set": {
"name": "Heat set",
"description": "Sets heating/cooling level for eight sleep.",
"fields": {
"duration": {
"name": "Duration",
"description": "Duration to heat/cool at the target level in seconds."
},
"target": {
"name": "Target",
"description": "Target cooling/heating level from -100 to 100."
}
}
"issues": {
"integration_removed": {
"title": "The Eight Sleep integration has been removed",
"description": "The Eight Sleep integration has been removed from Home Assistant.\n\nThe Eight Sleep API has changed and now requires a unique secret which is inaccessible outside of their apps.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Eight Sleep integration entries]({entries})."
}
}
}
@@ -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
@@ -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."
@@ -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)
+19 -1
View File
@@ -29,6 +29,7 @@ from aioesphomeapi import (
SensorInfo,
SensorState,
SwitchInfo,
TextInfo,
TextSensorInfo,
UserService,
build_unique_id,
@@ -68,6 +69,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
SelectInfo: Platform.SELECT,
SensorInfo: Platform.SENSOR,
SwitchInfo: Platform.SWITCH,
TextInfo: Platform.TEXT,
TextSensorInfo: Platform.SENSOR,
}
@@ -105,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)
@@ -425,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()
+10 -11
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()
@@ -16,8 +16,8 @@
"loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [
"async-interrupt==1.1.1",
"aioesphomeapi==18.0.10",
"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."]
+63
View File
@@ -0,0 +1,63 @@
"""Support for esphome texts."""
from __future__ import annotations
from aioesphomeapi import EntityInfo, TextInfo, TextMode as EsphomeTextMode, TextState
from homeassistant.components.text import TextEntity, TextMode
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry
from .enum_mapper import EsphomeEnumMapper
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up esphome texts based on a config entry."""
await platform_async_setup_entry(
hass,
entry,
async_add_entities,
info_type=TextInfo,
entity_type=EsphomeText,
state_type=TextState,
)
TEXT_MODES: EsphomeEnumMapper[EsphomeTextMode, TextMode] = EsphomeEnumMapper(
{
EsphomeTextMode.TEXT: TextMode.TEXT,
EsphomeTextMode.PASSWORD: TextMode.PASSWORD,
}
)
class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity):
"""A text implementation for esphome."""
@callback
def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Set attrs from static info."""
super()._on_static_info_update(static_info)
static_info = self._static_info
self._attr_native_min = static_info.min_length
self._attr_native_max = static_info.max_length
self._attr_pattern = static_info.pattern
self._attr_mode = TEXT_MODES.from_esphome(static_info.mode) or TextMode.TEXT
@property
@esphome_state_property
def native_value(self) -> str | None:
"""Return the state of the entity."""
state = self._state
if state.missing_state:
return None
return state.state
async def async_set_value(self, value: str) -> None:
"""Update the current value."""
await self._client.text_command(self._key, value)
+13 -1
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:
+12 -2
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(
+1 -1
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
@@ -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
+16 -1
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."""
+2 -2
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."""
+60 -27
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"
+3 -2
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."
+2 -2
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:
+49 -1
View File
@@ -2,6 +2,7 @@
from pyflume import FlumeAuth, FlumeDeviceList
from requests import Session
from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -10,8 +11,14 @@ from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.selector import ConfigEntrySelector
from .const import (
BASE_TOKEN_FILENAME,
@@ -19,8 +26,18 @@ from .const import (
FLUME_AUTH,
FLUME_DEVICES,
FLUME_HTTP_SESSION,
FLUME_NOTIFICATIONS_COORDINATOR,
PLATFORMS,
)
from .coordinator import FlumeNotificationDataUpdateCoordinator
SERVICE_LIST_NOTIFICATIONS = "list_notifications"
CONF_CONFIG_ENTRY = "config_entry"
LIST_NOTIFICATIONS_SERVICE_SCHEMA = vol.All(
{
vol.Required(CONF_CONFIG_ENTRY): ConfigEntrySelector(),
},
)
def _setup_entry(hass: HomeAssistant, entry: ConfigEntry):
@@ -59,14 +76,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
flume_auth, flume_devices, http_session = await hass.async_add_executor_job(
_setup_entry, hass, entry
)
notification_coordinator = FlumeNotificationDataUpdateCoordinator(
hass=hass, auth=flume_auth
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
FLUME_DEVICES: flume_devices,
FLUME_AUTH: flume_auth,
FLUME_HTTP_SESSION: http_session,
FLUME_NOTIFICATIONS_COORDINATOR: notification_coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_setup_service(hass)
return True
@@ -81,3 +103,29 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_setup_service(hass: HomeAssistant) -> None:
"""Add the services for the flume integration."""
async def list_notifications(call: ServiceCall) -> ServiceResponse:
"""Return the user notifications."""
entry_id: str = call.data[CONF_CONFIG_ENTRY]
entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id)
if not entry:
raise ValueError(f"Invalid config entry: {entry_id}")
if not (flume_domain_data := hass.data[DOMAIN].get(entry_id)):
raise ValueError(f"Config entry not loaded: {entry_id}")
return {
"notifications": flume_domain_data[
FLUME_NOTIFICATIONS_COORDINATOR
].notifications
}
hass.services.async_register(
DOMAIN,
SERVICE_LIST_NOTIFICATIONS,
list_notifications,
schema=LIST_NOTIFICATIONS_SERVICE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
@@ -15,8 +15,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
DOMAIN,
FLUME_AUTH,
FLUME_DEVICES,
FLUME_NOTIFICATIONS_COORDINATOR,
FLUME_TYPE_BRIDGE,
FLUME_TYPE_SENSOR,
KEY_DEVICE_ID,
@@ -84,7 +84,6 @@ async def async_setup_entry(
) -> None:
"""Set up a Flume binary sensor.."""
flume_domain_data = hass.data[DOMAIN][config_entry.entry_id]
flume_auth = flume_domain_data[FLUME_AUTH]
flume_devices = flume_domain_data[FLUME_DEVICES]
flume_entity_list: list[
@@ -94,9 +93,7 @@ async def async_setup_entry(
connection_coordinator = FlumeDeviceConnectionUpdateCoordinator(
hass=hass, flume_devices=flume_devices
)
notification_coordinator = FlumeNotificationDataUpdateCoordinator(
hass=hass, auth=flume_auth
)
notification_coordinator = flume_domain_data[FLUME_NOTIFICATIONS_COORDINATOR]
flume_devices = get_valid_flume_devices(flume_devices)
for device in flume_devices:
device_id = device[KEY_DEVICE_ID]
+1 -1
View File
@@ -29,7 +29,7 @@ FLUME_TYPE_SENSOR = 2
FLUME_AUTH = "flume_auth"
FLUME_HTTP_SESSION = "http_session"
FLUME_DEVICES = "devices"
FLUME_NOTIFICATIONS_COORDINATOR = "notifications_coordinator"
CONF_TOKEN_FILE = "token_filename"
BASE_TOKEN_FILENAME = "FLUME_TOKEN_FILE"
@@ -0,0 +1,7 @@
list_notifications:
fields:
config_entry:
required: true
selector:
config_entry:
integration: flume
@@ -61,5 +61,17 @@
"name": "30 days"
}
}
},
"services": {
"list_notifications": {
"name": "List notifications",
"description": "Return user notifications.",
"fields": {
"config_entry": {
"name": "Flume",
"description": "The flume config entry for which to return notifications."
}
}
}
}
}

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