Compare commits

...

128 Commits

Author SHA1 Message Date
Franck Nijhof
78222bd51c 2023.6.3 (#95119) 2023-06-23 19:08:57 +02:00
Franck Nijhof
9f6dab0643 Remove incompatible config schema for Fully Kiosk 2023-06-23 17:56:25 +02:00
Franck Nijhof
4cf9beccd8 Bumped version to 2023.6.3 2023-06-23 16:46:52 +02:00
Matthias Alphart
8f9425f09f Fix KNX device trigger passing info data (#95076) 2023-06-23 16:46:33 +02:00
Marcel van der Veldt
0fa954040e Fix removal of orphaned Matter devices (#95044) 2023-06-23 16:46:30 +02:00
Hmmbob
e26b8e11d3 Fix goodwe midnight error (#95041) 2023-06-23 16:45:56 +02:00
Richard Kroegel
ced6968e85 Bump bimmer_connected to 0.13.7 (#95017) 2023-06-23 16:45:16 +02:00
Raman Gupta
44e7243e25 Fix zwave_js device diagnostics dump (#94999)
* Fix zwave_js device diagnostics dump

* Update tests/components/zwave_js/test_diagnostics.py

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

* Update tests/components/zwave_js/test_diagnostics.py

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

* Update tests/components/zwave_js/test_diagnostics.py

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

* Update tests/components/zwave_js/test_diagnostics.py

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

* Update tests/components/zwave_js/test_diagnostics.py

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

* Update tests/components/zwave_js/test_diagnostics.py

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

* improve test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-23 16:43:34 +02:00
Marcel van der Veldt
bbbc9f646f Bump Matter Server to 3.5.1: some small fixes and stability improvements (#94985) 2023-06-23 16:43:31 +02:00
epenet
cda784c969 Add error handling to hassio issues (#94951) 2023-06-23 16:43:28 +02:00
Quentame
34ef89b16b Fix Meteo France blocked config entry when weather alert API fails (#94911)
* Fix: do not block config entry when weather alert API fails

* PR review
2023-06-23 16:43:24 +02:00
Allen Porter
f8cfaa6147 Bump ical to 4.5.4 (#94894) 2023-06-23 16:43:21 +02:00
Álvaro Fernández Rojas
5da5522481 Update aioairzone to v0.6.4 (#94873) 2023-06-23 16:43:18 +02:00
Ernst Klamer
cee8004641 Bump bthome to 2.12.0 (#94822) 2023-06-23 16:43:15 +02:00
J. Nick Koston
e1751647f4 Bump HAP-python to 4.7.0 (#94812) 2023-06-23 16:43:11 +02:00
Austin Mroczek
f33d671a5d Fix Totalconnect BinarySensorDeviceClass logic (#94772)
* handle temperature

* test for temperature

* test for unknown
2023-06-23 16:43:07 +02:00
Jan Čermák
254b1fd314 Fix warning from rapt_ble caused by payload version 2 (#94718) 2023-06-23 16:43:04 +02:00
Raman Gupta
89c6494056 Fix zwave_js trigger event reattach logic (#94702) 2023-06-23 16:43:01 +02:00
Dirk Sarodnick
b52cfd3324 Fix bluetooth tracker asyncio usage (#94695)
* fix for asyncio usage

fixes the error "Passing coroutines is forbidden, use tasks explicitly", caused by passing an async function into asyncio.wait directly instead of creating a task for it.

* removes unnecessary default param

* corrects formatting for black
2023-06-23 16:42:55 +02:00
Joost Lekkerkerker
6329f6bc07 Add strings for YouTube reauthentication (#94655) 2023-06-23 16:38:29 +02:00
Joost Lekkerkerker
57dd62e7d6 Make YouTube select lower quality thumbnails (#94652)
* Make YouTube select lower quality thumbnails

* Make YouTube select lower quality thumbnails

* Make YouTube select lower quality thumbnails

* Make YouTube select lower quality thumbnails

* Add tests

* Add tests

* Add tests

* Add tests

* Add tests
2023-06-23 16:38:21 +02:00
Dominik
203820d836 Fix glances raid plugin data (#94597)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2023-06-23 16:38:14 +02:00
Dominik
e1c486fc4a Bump minimum typing_extensions to 4.6.3 (#94587) 2023-06-23 16:37:27 +02:00
Alistair Tudor
78bbec0a6e Fix unit for Habitica text sensors (#94550) 2023-06-23 16:35:33 +02:00
Tom Harris
ffe35c73b6 Handle Insteon events correctly (#94549)
Make events generalized
2023-06-23 16:35:30 +02:00
Joost Lekkerkerker
d2385f97a7 Handle LastFM unavailable (#94456) 2023-06-23 16:35:27 +02:00
Kim Frellsen
bd0b8dc0bc Fortios device tracker updates (#92331)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-06-23 16:35:24 +02:00
Mike Heath
3f936993a9 Register Fully Kiosk services regardless of setup result (#88647)
* Register services at integration level

If HA is unable to connect to Fully Kiosk, the services don't get
registered. This can cause repair to create notifications saying
that the 'fully_kiosk.load_url' service is unknown.

Fixes #85444

* Validate config entry is loaded

* Refactor service invocation

Raises `HomeAssistantError` when the user provides an device id that is
not in the device registry or a device that is not a Fully Kiosk
device. If the device's config entry is not loaded, a warning is
logged.

* Update homeassistant/components/fully_kiosk/services.py

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

* Assert HomeAssistantError when integration unloaded

* Remove unused import

* Set CONFIG_SCHEMA

* Update homeassistant/components/fully_kiosk/__init__.py

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

* Add test for non fkb devices targets in service calls

* Apply suggestions from code review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-23 16:35:19 +02:00
Paulus Schoutsen
e5c5790768 2023.6.2 (#94621) 2023-06-15 00:19:30 -04:00
Paulus Schoutsen
905bdd0dd5 Bumped version to 2023.6.2 2023-06-14 21:18:40 -04:00
Erik Montnemery
9cbcfca2cd Improve multipan debug logging (#94580) 2023-06-14 21:18:35 -04:00
Chris Talkington
e6b8e4fd48 Fix failed recovery in ipp (#94573) 2023-06-14 21:18:34 -04:00
Chris Talkington
8f437c5833 Fix failed recovery in roku (#94572) 2023-06-14 21:18:33 -04:00
Ian Foster
d28d909114 Fix keyboard_remote for python 3.11 (#94570)
* started work to update keyboard_remote to work with python 3.11

* updated function names

* all checks pass

* fixed asyncio for python 3.11

* cleanup

* Update homeassistant/components/keyboard_remote/__init__.py

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

* Update __init__.py

added:
from __future__ import annotations

* Fix typing

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-14 21:18:32 -04:00
Maciej Bieniek
f67577ebe0 Catch InvalidAuthError in shutdown() method for Shelly gen2 devices (#94563)
* Catch InvalidAuthError in shutdown() method

* Add test

* Revert unwanted change in tests
2023-06-14 21:18:31 -04:00
Franck Nijhof
70d33129d4 Update Home Assistant base image to 2023.06.0 (#94556) 2023-06-14 21:18:29 -04:00
Aaron Bach
a63ce8100e Bump regenmaschine to 2023.06.0 (#94554) 2023-06-14 21:18:28 -04:00
J. Nick Koston
d557c6e43e Bump yalexs-ble to 2.1.18 (#94547) 2023-06-14 21:18:27 -04:00
Raman Gupta
fd0404bb4a Fix entity and device selector TypedDict's (#94510) 2023-06-14 21:18:26 -04:00
Chris Phillips
576cf52573 Bump russound_rio to 1.0.0 (#94500) 2023-06-14 21:17:51 -04:00
mover85
e83f0bb7a5 Revert "Bump pydaikin 2.9.1 (#93635)" (#94469)
Revert to pydaikin 2.9.0
2023-06-14 21:17:08 -04:00
Raman Gupta
fa8e952324 Set default value for endpoint in zwave device automations (#94445)
* Set default value for endpoint in zwave device automations

* add test case
2023-06-14 21:17:07 -04:00
G Johansson
25a4679266 Fix reload service in Command Line (#94436)
* Fix reload in Command Line

* Add read new yaml
2023-06-14 21:17:06 -04:00
G Johansson
f5aa4f5866 Fix manual update for Command Line (#94433)
Manual update command line
2023-06-14 21:17:04 -04:00
Yuxin Wang
0083649e43 Add unit inference for Amps and VA in APCUPSD integration (#94431)
* Add unit inference for Amps and VA

* Rename `init_integration` to `async_init_integration` for better consistency with HA naming style
2023-06-14 21:17:03 -04:00
Sander
2505de35c9 Fix: Xiaomi Miio Fan, delay off countdown unit conversion (#94428) 2023-06-14 21:17:02 -04:00
Christopher Bailey
238eebb0b6 Bump unifiprotect to 4.10.3 (#94416)
* Bump unifiprotect to 4.10.3

* Reqs
2023-06-14 21:17:01 -04:00
Matthias Alphart
4cb30e69ac Update knx-frontend to 2023.6.9.195839 (#94404) 2023-06-14 21:16:19 -04:00
Joost Lekkerkerker
ac00977e57 Abort youtube configuration if user has no channel (#94402)
* Abort configuration if user has no channel

* Clean up

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-06-14 21:15:21 -04:00
jasonkuster
b2db849798 Fix ZHA binding api to actually return responses (#94388) 2023-06-14 21:15:20 -04:00
Raman Gupta
2c7a176580 Re-add event listeners after Z-Wave server disconnection (#94383)
* Re-add event listeners after Z-Wave server disconnection

* switch order

* Add tests
2023-06-14 21:15:19 -04:00
Matthias Alphart
4dbc408696 Update xknxproject to 3.1.1 (#94375) 2023-06-14 21:14:42 -04:00
Sebastian Muszynski
582fd11a70 Fix deprecated asyncio.wait use with coroutines (#94371) 2023-06-14 21:13:25 -04:00
Jan Bouwhuis
96cb5ff8b0 Fix dep noaa-coops for noaa_tides (#94370)
Bump noaa-coops to 0.1.9
2023-06-14 21:13:23 -04:00
Jafar Atili
6029e23ab7 fix: electrasmart - cast temperature to int in set_temperature (#94368)
fix: cast temperature to int
2023-06-14 21:13:22 -04:00
Jonathan Keljo
3434d74993 Upgrade sisyphus-control to 3.1.3 (#94310) 2023-06-14 21:13:21 -04:00
Nathan Spencer
e091793b6c Bump pylitterbot to 2023.4.2 (#94301) 2023-06-14 21:13:20 -04:00
Glenn Waters
9c8444da0e Bump elkm1-lib to 2.2.5 (#94296)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-06-14 21:13:19 -04:00
Tom Harris
427f0f4bee Fix issue with Insteon linked devices maintaining current state (#94286)
* Bump pyinsteon

* Update tests
2023-06-14 21:13:18 -04:00
FFT
95528f875e Change pyoppleio to pyoppleio-legacy (#88050)
* Change pyoppleio to pyoppleio-310 (#75268)

* [m] change opple component's dependency to a new working one
2023-06-14 21:13:17 -04:00
Paulus Schoutsen
a5f86bff45 2023.6.1 (#94288) 2023-06-08 14:57:37 -04:00
Paulus Schoutsen
d991970754 Bumped version to 2023.6.1 2023-06-08 13:39:33 -04:00
Tom Harris
d745b44180 Fix Insteon startup for users with X10 devices (#94277) 2023-06-08 13:39:10 -04:00
Tom Harris
602fcd6b1b Restructure Insteon start-up (#92818)
* Restructure startup

* Code review

* Further typing

* Fix circular import
2023-06-08 13:39:09 -04:00
Franck Nijhof
b39b0a960e Fix repair issue about no yaml for config entries (#94271) 2023-06-08 13:35:08 -04:00
Paulus Schoutsen
40bb796f03 Fix default value when logger used (#94269) 2023-06-08 13:28:54 -04:00
Christopher Bailey
2801ba6cad Bump unifiprotect to 4.10.2 (#94263) 2023-06-08 13:28:52 -04:00
Paul Bottein
5da0ef36ea Update frontend to 20230608.0 (#94256) 2023-06-08 13:28:51 -04:00
Joost Lekkerkerker
d861292900 Retrieve friends in an async manner in Lastfm (#94255) 2023-06-08 13:28:50 -04:00
Jc2k
a3fda43c64 Bump aiohomekit to 2.6.5 (fixes python 3.11 regression) (#94245) 2023-06-08 13:28:49 -04:00
Joost Lekkerkerker
8705a26a1a Catch exception when user has no lastfm friends (#94235) 2023-06-08 13:28:48 -04:00
jan iversen
2b1c45c28c Solve wrong return code from modbus. (#94234) 2023-06-08 13:28:47 -04:00
Jan Bouwhuis
0cf3825183 Fix imap crash on email without subject (#94230) 2023-06-08 13:28:46 -04:00
Kostas Chatzikokolakis
413e1c97d7 Bump pulsectl to 23.5.2 (#94227) 2023-06-08 13:28:45 -04:00
Joost Lekkerkerker
3b27a3aabf Bump python-opensky to 0.0.9 (#94224) 2023-06-08 13:28:44 -04:00
Joost Lekkerkerker
4509e13ceb Bump python-opensky (#93916) 2023-06-08 13:28:43 -04:00
Álvaro Fernández Rojas
33bf8c600b Update aioairzone-cloud to v0.1.8 (#94223) 2023-06-08 13:27:46 -04:00
Jan-Philipp Benecke
b508875f17 Set httpx log level to warning (#94217)
Set log level of httpx to warning
2023-06-08 13:27:45 -04:00
Allen Porter
ac963a2b6e Require pydantic 1.10.8 or higher (#94208)
* Requied pydantic 1.10.9 or higher

* Simplify constraint to 2.0

* Drop constraint by one patch release to 1.10.8 or higher

* Add package constraints to gen requirements script
2023-06-08 13:27:43 -04:00
James Connor
13029cf26f Fix ambiclimate for Python 3.11 (#94203)
Fix ambiclimate python 3.11 break
2023-06-08 13:27:42 -04:00
Paulus Schoutsen
f39a6b96ff Rename Local Media to My Media (#94201)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-06-08 13:27:41 -04:00
Mick Vleeshouwer
c6a17d6832 Bump pyoverkiz to 1.8.0 (#94176) 2023-06-08 13:27:40 -04:00
Joost Lekkerkerker
74c0552a12 Fix Abode unit of measurement (#94168)
Change unit of measurement to HA const
2023-06-08 13:27:38 -04:00
Justin Vanderhooft
f24b514c9a Bump melnor-bluetooth to fix a timezone issue (#94159) 2023-06-08 13:27:37 -04:00
Erik Montnemery
e1c47fdb61 Fix OTBR reset (#94157) 2023-06-08 13:27:36 -04:00
j4n-e4t
93baf24394 Add error handling to input_select integration (#93940) 2023-06-08 12:27:23 -04:00
Franck Nijhof
a4e236d0b9 2023.6.0 (#94158) 2023-06-07 15:39:47 +02:00
Franck Nijhof
421fa5b035 Bumped version to 2023.6.0 2023-06-07 13:49:03 +02:00
Erik Montnemery
3d3fecbd23 Disable google assistant local control of climate entities (#94153) 2023-06-07 13:48:20 +02:00
Erik Montnemery
468be632fd Add debug logs to cloud migration (#94151) 2023-06-07 13:48:17 +02:00
Bram Kragten
74ccdcda68 Update frontend to 20230607.0 (#94150) 2023-06-07 13:48:14 +02:00
Erik Montnemery
5cc61acfb2 Fix migration of Google Assistant cloud settings (#94148) 2023-06-07 13:48:11 +02:00
Christopher Bailey
02d55a8e49 Bump unifiprotect to 4.10.1 (#94141) 2023-06-07 13:48:06 +02:00
Paulus Schoutsen
f4e3ef6b51 Bumped version to 2023.6.0b6 2023-06-06 22:00:28 -04:00
Paulus Schoutsen
7740539df0 Bump waqiasync to 1.1.0 (#94136) 2023-06-06 22:00:20 -04:00
Christopher Bailey
b077bf9b86 Fix multiple smart detects firing at once for UniFi Protect (#94133)
* Fix multiple smart detects firing at once

* Tweak

* Clean up logging. Linting

* Linting
2023-06-06 22:00:19 -04:00
Joakim Plate
23f2898836 Correct zha device classes for voc and pm25 (#94130)
Correct zha device classes
2023-06-06 22:00:18 -04:00
Shay Levy
e6638ca356 Remove goalfeed integration (#94129) 2023-06-06 21:59:32 -04:00
Jean-François Roy
93d52d8835 Bump aiobafi6 to 0.8.2 (#94125) 2023-06-06 21:58:09 -04:00
puddly
26e08abb9a Revert "Increase Zigbee command retries (#93877)" (#94123) 2023-06-06 21:58:08 -04:00
J. Nick Koston
6a573b507e Remove mark_read service from persistent_notification (#94122)
* Remove mark_read from persistent_notification

Nothing on the frontend uses this, and the service is not documented

There is not much point in keeping this as the notifications
are no longer stored in the state machine

* adjust

* adjust
2023-06-06 21:58:07 -04:00
Bram Kragten
2b39550e55 Update frontend to 20230606.0 (#94119) 2023-06-06 21:58:06 -04:00
J. Nick Koston
0e50baf007 Verify persistant notifications can be dismissed by the id they are created with (#94112) 2023-06-06 21:58:05 -04:00
Luke
286de1f051 Bump python-roborock to 23.4 (#94111)
* bump to 23.0

* bump to 23.4
2023-06-06 21:58:04 -04:00
Luke
3e23996247 Bump Roborock to 0.21.0 (#94035)
bump to 21.0
2023-06-06 21:58:03 -04:00
Álvaro Fernández Rojas
7a658117bb Update aioairzone to v0.6.3 and fix issue with latest firmware update (#94100) 2023-06-06 21:56:36 -04:00
Luke
49388eab3a Add diagnostics to Roborock (#94099)
* Add diagnostics

* Update homeassistant/components/roborock/models.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* adds snapshot

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2023-06-06 21:56:35 -04:00
Justin Vanderhooft
e6fcc6b73c fix: Bump melnor-bluetooth to fix deadlock (#94098) 2023-06-06 21:56:33 -04:00
Robert Svensson
e00012289d Bump aiounifi to v48 - Fix fail to initialise due to board_rev not exist (#94093) 2023-06-06 21:56:32 -04:00
Luke
f373f1abd5 Add missing translation keys for Roborock mop intensity (#94088) 2023-06-06 21:56:31 -04:00
puddly
2c43672a8a Include port info in the ZHA websocket settings response (#93934) 2023-06-06 21:56:30 -04:00
Paulus Schoutsen
7a6327d7e2 Bumped version to 2023.6.0b5 2023-06-05 16:13:07 -04:00
G Johansson
ee8f63b9c9 Fix reload service in Command Line (#94085)
Fix multi platform reload service in command line
2023-06-05 16:12:59 -04:00
Bram Kragten
28e0f5e104 Update frontend to 20230605.0 (#94083)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-06-05 16:12:58 -04:00
Michael Hansen
eb036af410 Bump intents to 2023.6.5 (#94077) 2023-06-05 16:12:57 -04:00
Luke
4bb6fec1d6 Don't add Roborock switches if it is not supported (#94069)
* don't add switches if it is not supported

* don't create entity unless if it is valid

* Raise on other exceptions

* rework valid_enties
2023-06-05 16:12:56 -04:00
J. Nick Koston
dbd5511e5e Bump zeroconf to 0.64.0 (#94052) 2023-06-05 16:12:55 -04:00
Raman Gupta
580065e946 Fix zwave_js.update entity restore logic (#94043) 2023-06-05 16:12:54 -04:00
Pascal Reeb
4a31cb0ad8 Update pynuki to 1.6.2 (#94041)
chore(component/nuki): update pynuki to 1.6.2
2023-06-05 16:12:53 -04:00
G Johansson
5a63079c80 Remove update_before_add from binary_sensor in Command Line (#94040)
Remove update_before_add
2023-06-05 16:12:52 -04:00
tronikos
902bd521d2 Android TV Remote: Abort zeroconf if mac address is missing (#94026)
Abort zeroconf if mac address is missing
2023-06-05 16:12:51 -04:00
Ernst Klamer
aff4d537a7 Bump xiaomi-ble to 0.17.2 (#94011)
Bump xiaomi-ble

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-06-05 16:12:49 -04:00
Joost Lekkerkerker
4f00cc9faa Show the sensor state using the coordinatordata instead of initial data (#94008)
* Show the sensor state using the coordinatordata instead of initial data

* Add test

* Remove part
2023-06-05 16:12:48 -04:00
Joost Lekkerkerker
2a99fea1de Add video id to youtube sensor state attributes (#93668)
* Add video id to state attributes

* Make extra state attributes not optional

* Revert "Make extra state attributes not optional"

This reverts commit d2f9e936c809dd50a5e4bbdaa181c9c9ddd3d217.
2023-06-05 16:12:48 -04:00
tronikos
9aeba6221b Fix error in tibber while fetching latest statistics (#93998) 2023-06-05 15:55:59 -04:00
Paulus Schoutsen
bb2a89f065 Bumped version to 2023.6.0b4 2023-06-02 23:35:41 -04:00
Robert Hillis
f92298c6fc Catch Google Sheets api error (#93979) 2023-06-02 23:35:36 -04:00
G Johansson
6ff55a6505 Add scan interval to Command Line (#93752)
* Add scan interval

* Handle previous not complete

* Fix faulty text

* Add tests

* lingering

* Cool down

* Fix tests
2023-06-02 23:35:35 -04:00
205 changed files with 3638 additions and 982 deletions

View File

@@ -420,7 +420,6 @@ omit =
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
homeassistant/components/glances/sensor.py
homeassistant/components/goalfeed/*
homeassistant/components/goodwe/__init__.py
homeassistant/components/goodwe/button.py
homeassistant/components/goodwe/coordinator.py

View File

@@ -1,11 +1,11 @@
image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -391,6 +391,7 @@ def async_enable_logging(
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger(None).exception(
"Uncaught exception", exc_info=args # type: ignore[arg-type]

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -71,7 +72,7 @@ class AbodeSensor(AbodeDevice, SensorEntity):
elif description.key == CONST.HUMI_STATUS_KEY:
self._attr_native_unit_of_measurement = device.humidity_unit
elif description.key == CONST.LUX_STATUS_KEY:
self._attr_native_unit_of_measurement = device.lux_unit
self._attr_native_unit_of_measurement = LIGHT_LUX
@property
def native_value(self) -> float | None:

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.1"]
"requirements": ["aioairzone==0.6.4"]
}

View File

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

View File

@@ -98,7 +98,7 @@ async def async_setup_entry(
tasks = []
for heater in data_connection.get_devices():
tasks.append(heater.update_device_info())
tasks.append(asyncio.create_task(heater.update_device_info()))
await asyncio.wait(tasks)
devs = []

View File

@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.host = discovery_info.host
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
self.mac = discovery_info.properties.get("bt")
assert self.mac
if not self.mac:
return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.host, CONF_NAME: self.name}

View File

@@ -430,7 +430,9 @@ INFERRED_UNITS = {
" Percent": PERCENTAGE,
" Volts": UnitOfElectricPotential.VOLT,
" Ampere": UnitOfElectricCurrent.AMPERE,
" Amps": UnitOfElectricCurrent.AMPERE,
" Volt-Ampere": UnitOfApparentPower.VOLT_AMPERE,
" VA": UnitOfApparentPower.VOLT_AMPERE,
" Watts": UnitOfPower.WATT,
" Hz": UnitOfFrequency.HERTZ,
" C": UnitOfTemperature.CELSIUS,

View File

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

View File

@@ -5,7 +5,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"iot_class": "local_push",
"requirements": ["aiobafi6==0.8.0"],
"requirements": ["aiobafi6==0.8.2"],
"zeroconf": [
{
"type": "_api._tcp.local.",

View File

@@ -173,7 +173,11 @@ async def async_setup_scanner(
rssi = await hass.async_add_executor_job(client.request_rssi)
client.close()
tasks.append(see_device(hass, async_see, mac, friendly_name, rssi))
tasks.append(
asyncio.create_task(
see_device(hass, async_see, mac, friendly_name, rssi)
)
)
if tasks:
await asyncio.wait(tasks)

View File

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

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==2.11.3"]
"requirements": ["bthome-ble==2.12.0"]
}

View File

@@ -47,6 +47,15 @@ from .coordinator import (
from .device import device_key_to_bluetooth_entity_key
SENSOR_DESCRIPTIONS = {
# Acceleration (m/s²)
(
BTHomeSensorDeviceClass.ACCELERATION,
Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.ACCELERATION}_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}",
native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
# Battery (percent)
(BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}",
@@ -131,6 +140,15 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.TOTAL,
),
# Gyroscope (°/s)
(
BTHomeSensorDeviceClass.GYROSCOPE,
Units.GYROSCOPE_DEGREES_PER_SECOND,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.GYROSCOPE}_{Units.GYROSCOPE_DEGREES_PER_SECOND}",
native_unit_of_measurement=Units.GYROSCOPE_DEGREES_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT,
),
# Humidity in (percent)
(BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
@@ -242,6 +260,15 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
# Timestamp (datetime object)
(
BTHomeSensorDeviceClass.TIMESTAMP,
None,
): SensorEntityDescription(
key=f"{BTHomeSensorDeviceClass.TIMESTAMP}",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
),
# UV index (-)
(
BTHomeSensorDeviceClass.UV_INDEX,

View File

@@ -221,6 +221,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
if self._prefs.alexa_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.alexa_settings_version < 3
@@ -233,6 +238,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
):
self._migrate_alexa_entity_settings_v1()
_LOGGER.info(
"Finished migration of Alexa settings from v%s to v%s",
self._prefs.alexa_settings_version,
ALEXA_SETTINGS_VERSION,
)
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)

View File

@@ -108,7 +108,12 @@ def _supported_legacy(hass: HomeAssistant, entity_id: str) -> bool:
if domain in SUPPORTED_DOMAINS:
return True
device_class = get_device_class(hass, entity_id)
try:
device_class = get_device_class(hass, entity_id)
except HomeAssistantError:
# The entity no longer exists
return False
if (
domain == "binary_sensor"
and device_class in SUPPORTED_BINARY_SENSOR_DEVICE_CLASSES
@@ -208,6 +213,11 @@ class CloudGoogleConfig(AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
_LOGGER.info(
"Start migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
if self._prefs.google_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.google_settings_version < 3
@@ -220,6 +230,11 @@ class CloudGoogleConfig(AbstractConfig):
):
self._migrate_google_entity_settings_v1()
_LOGGER.info(
"Finished migration of Google Assistant settings from v%s to v%s",
self._prefs.google_settings_version,
GOOGLE_SETTINGS_VERSION,
)
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)

View File

@@ -11,16 +11,24 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
SCAN_INTERVAL as BINARY_SENSOR_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SCAN_INTERVAL as COVER_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
SCAN_INTERVAL as SENSOR_DEFAULT_SCAN_INTERVAL,
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SCAN_INTERVAL as SWITCH_DEFAULT_SCAN_INTERVAL,
)
from homeassistant.const import (
CONF_COMMAND,
CONF_COMMAND_CLOSE,
@@ -34,15 +42,19 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
@@ -74,6 +86,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=BINARY_SENSOR_DEFAULT_SCAN_INTERVAL
): vol.All(cv.time_period, cv.positive_timedelta),
}
)
COVER_SCHEMA = vol.Schema(
@@ -86,6 +101,9 @@ COVER_SCHEMA = vol.Schema(
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=COVER_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
NOTIFY_SCHEMA = vol.Schema(
@@ -106,6 +124,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_SCAN_INTERVAL, default=SENSOR_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
SWITCH_SCHEMA = vol.Schema(
@@ -118,6 +139,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SWITCH_DEFAULT_SCAN_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta
),
}
)
COMBINED_SCHEMA = vol.Schema(
@@ -142,22 +166,49 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Command Line from yaml config."""
command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, [])
async def _reload_config(call: Event | ServiceCall) -> None:
"""Reload Command Line."""
reload_config = await async_integration_yaml_config(hass, "command_line")
reset_platforms = async_get_platforms(hass, "command_line")
for reset_platform in reset_platforms:
_LOGGER.debug("Reload resetting platform: %s", reset_platform.domain)
await reset_platform.async_reset()
if not reload_config:
return
await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config)
async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
await async_load_platforms(hass, config.get(DOMAIN, []), config)
return True
async def async_load_platforms(
hass: HomeAssistant,
command_line_config: list[dict[str, dict[str, Any]]],
config: ConfigType,
) -> None:
"""Load platforms from yaml."""
if not command_line_config:
return True
return
_LOGGER.debug("Full config loaded: %s", command_line_config)
load_coroutines: list[Coroutine[Any, Any, None]] = []
platforms: list[Platform] = []
reload_configs: list[tuple] = []
for platform_config in command_line_config:
for platform, _config in platform_config.items():
platforms.append(PLATFORM_MAPPING[platform])
if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms:
platforms.append(mapped_platform)
_LOGGER.debug(
"Loading config %s for platform %s",
platform_config,
PLATFORM_MAPPING[platform],
)
reload_configs.append((PLATFORM_MAPPING[platform], _config))
load_coroutines.append(
discovery.async_load_platform(
hass,
@@ -168,10 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
await async_setup_reload_service(hass, DOMAIN, platforms)
if load_coroutines:
_LOGGER.debug("Loading platforms: %s", platforms)
await asyncio.gather(*load_coroutines)
return True

View File

@@ -1,6 +1,7 @@
"""Support for custom shell commands to retrieve values."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import voluptuous as vol
@@ -18,17 +19,20 @@ from homeassistant.const import (
CONF_NAME,
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .sensor import CommandSensorData
DEFAULT_NAME = "Binary Command Sensor"
@@ -84,6 +88,9 @@ async def async_setup_platform(
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
command_timeout: int = binary_sensor_config[CONF_COMMAND_TIMEOUT]
unique_id: str | None = binary_sensor_config.get(CONF_UNIQUE_ID)
scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL
)
if value_template is not None:
value_template.hass = hass
data = CommandSensorData(hass, command, command_timeout)
@@ -98,15 +105,17 @@ async def async_setup_platform(
payload_off,
value_template,
unique_id,
scan_interval,
)
],
True,
)
class CommandBinarySensor(BinarySensorEntity):
"""Representation of a command line binary sensor."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +125,7 @@ class CommandBinarySensor(BinarySensorEntity):
payload_off: str,
value_template: Template | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
self.data = data
@@ -126,8 +136,39 @@ class CommandBinarySensor(BinarySensorEntity):
self._payload_off = payload_off
self._value_template = value_template
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Binary Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Binary Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -141,3 +182,12 @@ class CommandBinarySensor(BinarySensorEntity):
self._attr_is_on = True
elif value == self._payload_off:
self._attr_is_on = False
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())

View File

@@ -1,7 +1,11 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
import logging
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
CONF_COMMAND_TIMEOUT = "command_timeout"
DEFAULT_TIMEOUT = 15
DOMAIN = "command_line"

View File

@@ -1,7 +1,8 @@
"""Support for command line covers."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -19,21 +20,23 @@ from homeassistant.const import (
CONF_COVERS,
CONF_FRIENDLY_NAME,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
COVER_SCHEMA = vol.Schema(
{
@@ -97,11 +100,12 @@ async def async_setup_platform(
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_UNIQUE_ID),
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not covers:
_LOGGER.error("No covers added")
LOGGER.error("No covers added")
return
async_add_entities(covers)
@@ -110,6 +114,8 @@ async def async_setup_platform(
class CommandCover(CoverEntity):
"""Representation a command line cover."""
_attr_should_poll = False
def __init__(
self,
name: str,
@@ -120,6 +126,7 @@ class CommandCover(CoverEntity):
value_template: Template | None,
timeout: int,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the cover."""
self._attr_name = name
@@ -131,17 +138,32 @@ class CommandCover(CoverEntity):
self._value_template = value_template
self._timeout = timeout
self._attr_unique_id = unique_id
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
def _move_cover(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
returncode = call_shell_with_timeout(command, self._timeout)
success = returncode == 0
if not success:
_LOGGER.error(
LOGGER.error(
"Command failed (with return code %s): %s", returncode, command
)
@@ -165,12 +187,27 @@ class CommandCover(CoverEntity):
def _query_state(self) -> str | None:
"""Query for the state."""
if self._command_state:
_LOGGER.info("Running state value command: %s", self._command_state)
LOGGER.info("Running state value command: %s", self._command_state)
return check_output_or_log(self._command_state, self._timeout)
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Cover %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -181,15 +218,26 @@ class CommandCover(CoverEntity):
self._state = None
if payload:
self._state = int(payload)
await self.async_update_ha_state(True)
def open_cover(self, **kwargs: Any) -> None:
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._move_cover(self._command_open)
await self.hass.async_add_executor_job(self._move_cover, self._command_open)
await self._update_entity_state(None)
def close_cover(self, **kwargs: Any) -> None:
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._move_cover(self._command_close)
await self.hass.async_add_executor_job(self._move_cover, self._command_close)
await self._update_entity_state(None)
def stop_cover(self, **kwargs: Any) -> None:
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._move_cover(self._command_stop)
await self.hass.async_add_executor_job(self._move_cover, self._command_stop)
await self._update_entity_state(None)

View File

@@ -1,10 +1,10 @@
"""Allows to configure custom shell commands to turn a value for a sensor."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from datetime import timedelta
import json
import logging
import voluptuous as vol
@@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_COMMAND,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
@@ -28,15 +29,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import check_output_or_log
_LOGGER = logging.getLogger(__name__)
CONF_JSON_ATTRIBUTES = "json_attributes"
DEFAULT_NAME = "Command Sensor"
@@ -88,6 +89,7 @@ async def async_setup_platform(
if value_template is not None:
value_template.hass = hass
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
data = CommandSensorData(hass, command, command_timeout)
async_add_entities(
@@ -99,15 +101,17 @@ async def async_setup_platform(
value_template,
json_attributes,
unique_id,
scan_interval,
)
],
True,
]
)
class CommandSensor(SensorEntity):
"""Representation of a sensor that is using shell commands."""
_attr_should_poll = False
def __init__(
self,
data: CommandSensorData,
@@ -116,6 +120,7 @@ class CommandSensor(SensorEntity):
value_template: Template | None,
json_attributes: list[str] | None,
unique_id: str | None,
scan_interval: timedelta,
) -> None:
"""Initialize the sensor."""
self._attr_name = name
@@ -126,8 +131,39 @@ class CommandSensor(SensorEntity):
self._value_template = value_template
self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_unique_id = unique_id
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_update(self) -> None:
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
await self._update_entity_state(None)
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Sensor - {self.name}",
cancel_on_shutdown=True,
),
)
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Sensor %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Get the latest data and updates the state."""
await self.hass.async_add_executor_job(self.data.update)
value = self.data.value
@@ -144,11 +180,11 @@ class CommandSensor(SensorEntity):
if k in json_dict
}
else:
_LOGGER.warning("JSON result was not a dictionary")
LOGGER.warning("JSON result was not a dictionary")
except ValueError:
_LOGGER.warning("Unable to parse output as JSON: %s", value)
LOGGER.warning("Unable to parse output as JSON: %s", value)
else:
_LOGGER.warning("Empty reply found when expecting JSON data")
LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None:
self._attr_native_value = None
return
@@ -163,6 +199,15 @@ class CommandSensor(SensorEntity):
else:
self._attr_native_value = value
self.async_write_ha_state()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
class CommandSensorData:
"""The class for handling the data retrieval."""
@@ -191,7 +236,7 @@ class CommandSensorData:
args_to_render = {"arguments": args}
rendered_args = args_compiled.render(args_to_render)
except TemplateError as ex:
_LOGGER.exception("Error rendering command template: %s", ex)
LOGGER.exception("Error rendering command template: %s", ex)
return
else:
rendered_args = None
@@ -203,5 +248,5 @@ class CommandSensorData:
# Template used. Construct the string used in the shell
command = f"{prog} {rendered_args}"
_LOGGER.debug("Running command: %s", command)
LOGGER.debug("Running command: %s", command)
self.value = check_output_or_log(command, self.timeout)

View File

@@ -1,7 +1,8 @@
"""Support for custom shell commands to turn a switch on/off."""
from __future__ import annotations
import logging
import asyncio
from datetime import timedelta
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -20,6 +21,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SWITCHES,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
@@ -27,16 +29,17 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.template import Template
from homeassistant.helpers.template_entity import ManualTriggerEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN
from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER
from .utils import call_shell_with_timeout, check_output_or_log
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SWITCH_SCHEMA = vol.Schema(
{
@@ -112,11 +115,12 @@ async def async_setup_platform(
device_config.get(CONF_COMMAND_STATE),
value_template,
device_config[CONF_COMMAND_TIMEOUT],
device_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
)
)
if not switches:
_LOGGER.error("No switches added")
LOGGER.error("No switches added")
return
async_add_entities(switches)
@@ -125,6 +129,8 @@ async def async_setup_platform(
class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Representation a switch that can be toggled using shell commands."""
_attr_should_poll = False
def __init__(
self,
config: ConfigType,
@@ -134,6 +140,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_state: str | None,
value_template: Template | None,
timeout: int,
scan_interval: timedelta,
) -> None:
"""Initialize the switch."""
super().__init__(self.hass, config)
@@ -144,11 +151,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
self._command_state = command_state
self._value_template = value_template
self._timeout = timeout
self._attr_should_poll = bool(command_state)
self._scan_interval = scan_interval
self._process_updates: asyncio.Lock | None = None
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
if self._command_state:
self.async_on_remove(
async_track_time_interval(
self.hass,
self._update_entity_state,
self._scan_interval,
name=f"Command Line Cover - {self.name}",
cancel_on_shutdown=True,
),
)
async def _switch(self, command: str) -> bool:
"""Execute the actual commands."""
_LOGGER.info("Running command: %s", command)
LOGGER.info("Running command: %s", command)
success = (
await self.hass.async_add_executor_job(
@@ -158,18 +180,18 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
)
if not success:
_LOGGER.error("Command failed: %s", command)
LOGGER.error("Command failed: %s", command)
return success
def _query_state_value(self, command: str) -> str | None:
"""Execute state command for return value."""
_LOGGER.info("Running state value command: %s", command)
LOGGER.info("Running state value command: %s", command)
return check_output_or_log(command, self._timeout)
def _query_state_code(self, command: str) -> bool:
"""Execute state command for return code."""
_LOGGER.info("Running state code command: %s", command)
LOGGER.info("Running state code command: %s", command)
return (
call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0
)
@@ -188,7 +210,22 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if TYPE_CHECKING:
return None
async def async_update(self) -> None:
async def _update_entity_state(self, now) -> None:
"""Update the state of the entity."""
if self._process_updates is None:
self._process_updates = asyncio.Lock()
if self._process_updates.locked():
LOGGER.warning(
"Updating Command Line Switch %s took longer than the scheduled update interval %s",
self.name,
self._scan_interval,
)
return
async with self._process_updates:
await self._async_update()
async def _async_update(self) -> None:
"""Update device state."""
if self._command_state:
payload = str(await self.hass.async_add_executor_job(self._query_state))
@@ -201,15 +238,25 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
if payload or value:
self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload)
await self.async_update_ha_state(True)
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self._update_entity_state(dt_util.now())
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if await self._switch(self._command_on) and not self._command_state:
self._attr_is_on = True
self.async_schedule_update_ha_state()
await self._update_entity_state(None)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if await self._switch(self._command_off) and not self._command_state:
self._attr_is_on = False
self.async_schedule_update_ha_state()
await self._update_entity_state(None)

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.5.30"]
"requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.5"]
}

View File

@@ -7,6 +7,6 @@
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"quality_scale": "platinum",
"requirements": ["pydaikin==2.9.1"],
"requirements": ["pydaikin==2.9.0"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@@ -42,7 +42,7 @@ async def async_setup_entry(
[
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, zone in enumerate(zones)
if zone[0] != ("-", "0")
if zone != ("-", "0")
]
)
if daikin_api.device.support_advanced_modes:

View File

@@ -250,7 +250,7 @@ class ElectraClimateEntity(ClimateEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ValueError("No target temperature provided")
self._electra_ac_device.set_temperature(temperature)
self._electra_ac_device.set_temperature(int(temperature))
await self._async_operate_electra_ac()
def _update_device_attrs(self) -> None:

View File

@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"loggers": ["elkm1_lib"],
"requirements": ["elkm1-lib==2.2.2"]
"requirements": ["elkm1-lib==2.2.5"]
}

View File

@@ -192,7 +192,7 @@ class Flexit(ClimateEntity):
result = float(
await self._async_read_int16_from_register(register_type, register)
)
if result == -1:
if not result:
return -1
return result / 10.0
@@ -200,6 +200,6 @@ class Flexit(ClimateEntity):
result = await self._hub.async_pymodbus_call(
self._slave, register, value, CALL_TYPE_WRITE_REGISTER
)
if result == -1:
if not result:
return False
return True

View File

@@ -43,7 +43,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner
fgt = FortiOSAPI()
try:
fgt.tokenlogin(host, token, verify_ssl)
fgt.tokenlogin(host, token, verify_ssl, None, 12, "root")
except ConnectionError as ex:
_LOGGER.error("ConnectionError to FortiOS API: %s", ex)
return None
@@ -77,7 +77,12 @@ class FortiOSDeviceScanner(DeviceScanner):
def update(self):
"""Update clients from the device."""
clients_json = self._fgt.monitor("user/device/query", "")
clients_json = self._fgt.monitor(
"user/device/query",
"",
parameters={"filter": "format=master_mac|hostname|is_online"},
)
self._clients_json = clients_json
self._clients = []
@@ -85,8 +90,12 @@ class FortiOSDeviceScanner(DeviceScanner):
if clients_json:
try:
for client in clients_json["results"]:
if client["is_online"]:
self._clients.append(client["mac"].upper())
if (
"is_online" in client
and "master_mac" in client
and client["is_online"]
):
self._clients.append(client["master_mac"].upper())
except KeyError as kex:
_LOGGER.error("Key not found in clients: %s", kex)
@@ -106,17 +115,10 @@ class FortiOSDeviceScanner(DeviceScanner):
return None
for client in data["results"]:
if client["mac"] == device:
try:
if "master_mac" in client and client["master_mac"] == device:
if "hostname" in client:
name = client["hostname"]
_LOGGER.debug("Getting device name=%s", name)
return name
except KeyError as kex:
_LOGGER.debug(
"No hostname found for %s in client data: %s",
device,
kex,
)
return device.replace(":", "_")
else:
name = client["master_mac"].replace(":", "_")
return name
return None

View File

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

View File

@@ -2,6 +2,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .coordinator import FullyKioskDataUpdateCoordinator
@@ -17,6 +18,14 @@ PLATFORMS = [
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Fully Kiosk Browser."""
await async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Fully Kiosk Browser from a config entry."""
@@ -28,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
coordinator.async_update_listeners()
await async_setup_services(hass)
return True

View File

@@ -1,14 +1,12 @@
"""Services for the Fully Kiosk Browser integration."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from fullykiosk import FullyKiosk
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.device_registry as dr
@@ -16,59 +14,53 @@ from .const import (
ATTR_APPLICATION,
ATTR_URL,
DOMAIN,
LOGGER,
SERVICE_LOAD_URL,
SERVICE_START_APPLICATION,
)
from .coordinator import FullyKioskDataUpdateCoordinator
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Fully Kiosk Browser integration."""
async def execute_service(
call: ServiceCall,
fully_method: Callable,
*args: list[str],
**kwargs: dict[str, Any],
) -> None:
"""Execute a Fully service call.
:param call: {ServiceCall} HA service call.
:param fully_method: {Callable} A method of the FullyKiosk class.
:param args: Arguments for fully_method.
:param kwargs: Key-word arguments for fully_method.
:return: None
"""
LOGGER.debug(
"Calling Fully service %s with args: %s, %s", ServiceCall, args, kwargs
)
async def collect_coordinators(
device_ids: list[str],
) -> list[FullyKioskDataUpdateCoordinator]:
config_entries = list[ConfigEntry]()
registry = dr.async_get(hass)
for target in call.data[ATTR_DEVICE_ID]:
for target in device_ids:
device = registry.async_get(target)
if device:
for key in device.config_entries:
entry = hass.config_entries.async_get_entry(key)
if not entry:
continue
if entry.domain != DOMAIN:
continue
coordinator = hass.data[DOMAIN][key]
# fully_method(coordinator.fully, *args, **kwargs) would make
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(
*args, **kwargs
device_entries = list[ConfigEntry]()
for entry_id in device.config_entries:
entry = hass.config_entries.async_get_entry(entry_id)
if entry and entry.domain == DOMAIN:
device_entries.append(entry)
if not device_entries:
raise HomeAssistantError(
f"Device '{target}' is not a {DOMAIN} device"
)
break
config_entries.extend(device_entries)
else:
raise HomeAssistantError(
f"Device '{target}' not found in device registry"
)
coordinators = list[FullyKioskDataUpdateCoordinator]()
for config_entry in config_entries:
if config_entry.state != ConfigEntryState.LOADED:
raise HomeAssistantError(f"{config_entry.title} is not loaded")
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
return coordinators
async def async_load_url(call: ServiceCall) -> None:
"""Load a URL on the Fully Kiosk Browser."""
await execute_service(call, FullyKiosk.loadUrl, call.data[ATTR_URL])
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.fully.loadUrl(call.data[ATTR_URL])
async def async_start_app(call: ServiceCall) -> None:
"""Start an app on the device."""
await execute_service(
call, FullyKiosk.startApplication, call.data[ATTR_APPLICATION]
)
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
# Register all the above services
service_mapping = [

View File

@@ -223,13 +223,6 @@ SENSOR_TYPES = {
icon="mdi:docker",
state_class=SensorStateClass.MEASUREMENT,
),
("raid", "used"): GlancesSensorEntityDescription(
key="used",
type="raid",
name_suffix="Raid used",
icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT,
),
("raid", "available"): GlancesSensorEntityDescription(
key="available",
type="raid",
@@ -237,6 +230,13 @@ SENSOR_TYPES = {
icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT,
),
("raid", "used"): GlancesSensorEntityDescription(
key="used",
type="raid",
name_suffix="Raid used",
icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -269,36 +269,36 @@ async def async_setup_entry(
if sensor_type in ["fs", "sensors", "raid"]:
for sensor_label, params in sensors.items():
for param in params:
sensor_description = SENSOR_TYPES[(sensor_type, param)]
if sensor_description := SENSOR_TYPES.get((sensor_type, param)):
_migrate_old_unique_ids(
hass,
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
f"{sensor_label}-{sensor_description.key}",
)
entities.append(
GlancesSensor(
coordinator,
name,
sensor_label,
sensor_description,
)
)
else:
for sensor in sensors:
if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)):
_migrate_old_unique_ids(
hass,
f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}",
f"{sensor_label}-{sensor_description.key}",
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
f"-{sensor_description.key}",
)
entities.append(
GlancesSensor(
coordinator,
name,
sensor_label,
"",
sensor_description,
)
)
else:
for sensor in sensors:
sensor_description = SENSOR_TYPES[(sensor_type, sensor)]
_migrate_old_unique_ids(
hass,
f"{coordinator.host}-{name} {sensor_description.name_suffix}",
f"-{sensor_description.key}",
)
entities.append(
GlancesSensor(
coordinator,
name,
"",
sensor_description,
)
)
async_add_entities(entities)

View File

@@ -1,65 +0,0 @@
"""Component for the Goalfeed service."""
import json
import pysher
import requests
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
# Version downgraded due to regression in library
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
DOMAIN = "goalfeed"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
GOALFEED_HOST = "feed.goalfeed.ca"
GOALFEED_AUTH_ENDPOINT = "https://goalfeed.ca/feed/auth"
GOALFEED_APP_ID = "bfd4ed98c1ff22c04074"
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Goalfeed component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
def goal_handler(data):
"""Handle goal events."""
goal = json.loads(json.loads(data))
hass.bus.fire("goal", event_data=goal)
def connect_handler(data):
"""Handle connection."""
post_data = {
"username": username,
"password": password,
"connection_info": data,
}
resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json()
channel = pusher.subscribe("private-goals", resp["auth"])
channel.bind("goal", goal_handler)
pusher = pysher.Pusher(
GOALFEED_APP_ID, secure=False, port=8080, custom_host=GOALFEED_HOST
)
pusher.connection.bind("pusher:connection_established", connect_handler)
pusher.connect()
return True

View File

@@ -1,9 +0,0 @@
{
"domain": "goalfeed",
"name": "Goalfeed",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/goalfeed",
"iot_class": "cloud_push",
"loggers": ["pysher"],
"requirements": ["pysher==1.0.7"]
}

View File

@@ -243,7 +243,7 @@ class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
"""
if not self.coordinator.last_update_success:
self.coordinator.reset_sensor(self._sensor.id)
self.coordinator.reset_sensor(self._sensor.id_)
self.async_write_ha_state()
_LOGGER.debug("Goodwe reset %s to 0", self.name)
next_midnight = dt_util.start_of_local_day(

View File

@@ -186,7 +186,7 @@ STORE_GOOGLE_LOCAL_WEBHOOK_ID = "local_webhook_id"
SOURCE_CLOUD = "cloud"
SOURCE_LOCAL = "local"
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK, TYPE_THERMOSTAT}
FAN_SPEEDS = {
"5/5": ["High", "Max", "Fast", "5"],

View File

@@ -7,13 +7,18 @@ import aiohttp
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from gspread import Client
from gspread.exceptions import APIError
from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
@@ -93,6 +98,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
except RefreshError as ex:
entry.async_start_reauth(hass)
raise ex
except APIError as ex:
raise HomeAssistantError("Failed to write data") from ex
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
row_data = {"created": str(datetime.now())} | call.data[DATA]
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])

View File

@@ -24,7 +24,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
SENSORS_TYPES = {
"name": SensorType("Name", None, "", ["profile", "name"]),
"name": SensorType("Name", None, None, ["profile", "name"]),
"hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]),
"maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
@@ -35,7 +35,7 @@ SENSORS_TYPES = {
"Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]
),
"gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]),
"class": SensorType("Class", "mdi:sword", "", ["stats", "class"]),
"class": SensorType("Class", "mdi:sword", None, ["stats", "class"]),
}
TASKS_TYPES = {

View File

@@ -305,7 +305,11 @@ class SupervisorIssues:
async def update(self) -> None:
"""Update issues from Supervisor resolution center."""
data = await self._client.get_resolution_info()
try:
data = await self._client.get_resolution_info()
except HassioAPIError as err:
_LOGGER.error("Failed to update supervisor issues: %r", err)
return
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])

View File

@@ -96,19 +96,29 @@ class MultiprotocolAddonManager(AddonManager):
) -> None:
"""Register a multipan platform."""
self._platforms[integration_domain] = platform
if self._channel is not None or not await platform.async_using_multipan(hass):
channel = await platform.async_get_channel(hass)
using_multipan = await platform.async_using_multipan(hass)
_LOGGER.info(
"Registering new multipan platform '%s', using multipan: %s, channel: %s",
integration_domain,
using_multipan,
channel,
)
if self._channel is not None or not using_multipan:
return
new_channel = await platform.async_get_channel(hass)
if new_channel is None:
if channel is None:
return
_LOGGER.info(
"Setting multipan channel to %s (source: '%s')",
new_channel,
channel,
integration_domain,
)
self.async_set_channel(new_channel)
self.async_set_channel(channel)
async def async_change_channel(
self, channel: int, delay: float

View File

@@ -626,10 +626,10 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
@pyhap_callback # type: ignore[misc]
def pair(
self, client_uuid: UUID, client_public: str, client_permissions: int
self, client_username_bytes: bytes, client_public: str, client_permissions: int
) -> bool:
"""Override super function to dismiss setup message if paired."""
success = super().pair(client_uuid, client_public, client_permissions)
success = super().pair(client_username_bytes, client_public, client_permissions)
if success:
async_dismiss_setup_message(self.hass, self._entry_id)
return cast(bool, success)

View File

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

View File

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

View File

@@ -120,7 +120,7 @@ class ImapMessage:
@property
def subject(self) -> str:
"""Decode the message subject."""
decoded_header = decode_header(self.email_message["Subject"])
decoded_header = decode_header(self.email_message["Subject"] or "")
subject_header = make_header(decoded_header)
return str(subject_header)

View File

@@ -302,12 +302,9 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
async def async_select_option(self, option: str) -> None:
"""Select new option."""
if option not in self.options:
_LOGGER.warning(
"Invalid option: %s (possible options: %s)",
option,
", ".join(self.options),
raise HomeAssistantError(
f"Invalid option: {option} (possible options: {', '.join(self.options)})"
)
return
self._attr_current_option = option
self.async_write_ha_state()

View File

@@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SENSOR_TYPES = {
OPEN_CLOSE_SENSOR: BinarySensorDeviceClass.OPENING,
@@ -62,7 +62,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.BINARY_SENSOR}"
async_dispatcher_connect(hass, signal, async_add_insteon_binary_sensor_entities)
async_add_insteon_binary_sensor_entities()
async_add_insteon_devices(
hass,
Platform.BINARY_SENSOR,
InsteonBinarySensorEntity,
async_add_entities,
)
class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity):

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
FAN_ONLY = "fan_only"
@@ -71,7 +71,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.CLIMATE}"
async_dispatcher_connect(hass, signal, async_add_insteon_climate_entities)
async_add_insteon_climate_entities()
async_add_insteon_devices(
hass,
Platform.CLIMATE,
InsteonClimateEntity,
async_add_entities,
)
class InsteonClimateEntity(InsteonEntity, ClimateEntity):

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -34,7 +34,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.COVER}"
async_dispatcher_connect(hass, signal, async_add_insteon_cover_entities)
async_add_insteon_cover_entities()
async_add_insteon_devices(
hass,
Platform.COVER,
InsteonCoverEntity,
async_add_entities,
)
class InsteonCoverEntity(InsteonEntity, CoverEntity):

View File

@@ -17,7 +17,7 @@ from homeassistant.util.percentage import (
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
SPEED_RANGE = (1, 255) # off is not included
@@ -38,7 +38,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.FAN}"
async_dispatcher_connect(hass, signal, async_add_insteon_fan_entities)
async_add_insteon_fan_entities()
async_add_insteon_devices(
hass,
Platform.FAN,
InsteonFanEntity,
async_add_entities,
)
class InsteonFanEntity(InsteonEntity, FanEntity):

View File

@@ -1,4 +1,7 @@
"""Utility methods for the Insteon platform."""
from collections.abc import Iterable
from pyinsteon.device_types.device_base import Device
from pyinsteon.device_types.ipdb import (
AccessControl_Morningstar,
ClimateControl_Thermostat,
@@ -44,7 +47,7 @@ from pyinsteon.device_types.ipdb import (
from homeassistant.const import Platform
DEVICE_PLATFORM = {
DEVICE_PLATFORM: dict[Device, dict[Platform, Iterable[int]]] = {
AccessControl_Morningstar: {Platform.LOCK: [1]},
DimmableLightingControl: {Platform.LIGHT: [1]},
DimmableLightingControl_Dial: {Platform.LIGHT: [1]},
@@ -101,11 +104,11 @@ DEVICE_PLATFORM = {
}
def get_device_platforms(device):
def get_device_platforms(device) -> dict[Platform, Iterable[int]]:
"""Return the HA platforms for a device type."""
return DEVICE_PLATFORM.get(type(device), {}).keys()
return DEVICE_PLATFORM.get(type(device), {})
def get_platform_groups(device, domain) -> dict:
"""Return the platforms that a device belongs in."""
return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined]
def get_device_platform_groups(device: Device, platform: Platform) -> Iterable[int]:
"""Return the list of device groups for a platform."""
return get_device_platforms(device).get(platform, [])

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
MAX_BRIGHTNESS = 255
@@ -37,7 +37,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LIGHT}"
async_dispatcher_connect(hass, signal, async_add_insteon_light_entities)
async_add_insteon_light_entities()
async_add_insteon_devices(
hass,
Platform.LIGHT,
InsteonDimmerEntity,
async_add_entities,
)
class InsteonDimmerEntity(InsteonEntity, LightEntity):

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -30,7 +30,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.LOCK}"
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
async_add_insteon_lock_entities()
async_add_insteon_devices(
hass,
Platform.LOCK,
InsteonLockEntity,
async_add_entities,
)
class InsteonLockEntity(InsteonEntity, LockEntity):

View File

@@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["pyinsteon", "pypubsub"],
"requirements": [
"pyinsteon==1.4.2",
"pyinsteon==1.4.3",
"insteon-frontend-home-assistant==0.3.5"
],
"usb": [

View File

@@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
from .utils import async_add_insteon_devices, async_add_insteon_entities
async def async_setup_entry(
@@ -33,7 +33,12 @@ async def async_setup_entry(
signal = f"{SIGNAL_ADD_ENTITIES}_{Platform.SWITCH}"
async_dispatcher_connect(hass, signal, async_add_insteon_switch_entities)
async_add_insteon_switch_entities()
async_add_insteon_devices(
hass,
Platform.SWITCH,
InsteonSwitchEntity,
async_add_entities,
)
class InsteonSwitchEntity(InsteonEntity, SwitchEntity):

View File

@@ -1,7 +1,10 @@
"""Utilities used by insteon component."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import logging
from typing import TYPE_CHECKING, Any
from pyinsteon import devices
from pyinsteon.address import Address
@@ -30,6 +33,7 @@ from homeassistant.const import (
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
@@ -38,6 +42,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
CONF_CAT,
@@ -78,7 +83,7 @@ from .const import (
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .ipdb import get_device_platforms, get_platform_groups
from .ipdb import get_device_platform_groups, get_device_platforms
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
@@ -89,6 +94,9 @@ from .schemas import (
X10_HOUSECODE_SCHEMA,
)
if TYPE_CHECKING:
from .insteon_entity import InsteonEntity
_LOGGER = logging.getLogger(__name__)
@@ -107,8 +115,8 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
"""Register Insteon device events."""
@callback
def async_fire_group_on_off_event(
name: str, address: Address, group: int, button: str
def async_fire_insteon_event(
name: str, address: Address, group: int, button: str | None = None
):
# Firing an event when a button is pressed.
if button and button[-2] == "_":
@@ -132,12 +140,15 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
_LOGGER.debug("Firing event %s with %s", event, schema)
hass.bus.async_fire(event, schema)
if str(device.address).startswith("X10"):
return
for name_or_group, event in device.events.items():
if isinstance(name_or_group, int):
for _, event in device.events[name_or_group].items():
_register_event(event, async_fire_group_on_off_event)
_register_event(event, async_fire_insteon_event)
else:
_register_event(event, async_fire_group_on_off_event)
_register_event(event, async_fire_insteon_event)
def register_new_device_callback(hass):
@@ -158,8 +169,10 @@ def register_new_device_callback(hass):
await device.async_status()
platforms = get_device_platforms(device)
for platform in platforms:
groups = get_device_platform_groups(device, platform)
signal = f"{SIGNAL_ADD_ENTITIES}_{platform}"
dispatcher_send(hass, signal, {"address": device.address})
dispatcher_send(hass, signal, {"address": device.address, "groups": groups})
add_insteon_events(hass, device)
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
@@ -383,20 +396,38 @@ def print_aldb_to_log(aldb):
@callback
def async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
):
"""Add Insteon devices to a platform."""
new_entities = []
device_list = [discovery_info.get("address")] if discovery_info else devices
for address in device_list:
device = devices[address]
groups = get_platform_groups(device, platform)
for group in groups:
new_entities.append(entity_type(device, group))
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
discovery_info: dict[str, Any],
) -> None:
"""Add an Insteon group to a platform."""
address = discovery_info["address"]
device = devices[address]
new_entities = [
entity_type(device=device, group=group) for group in discovery_info["groups"]
]
async_add_entities(new_entities)
@callback
def async_add_insteon_devices(
hass: HomeAssistant,
platform: Platform,
entity_type: type[InsteonEntity],
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add all entities to a platform."""
for address in devices:
device = devices[address]
groups = get_device_platform_groups(device, platform)
discovery_info = {"address": address, "groups": groups}
async_add_insteon_entities(
hass, platform, entity_type, async_add_entities, discovery_info
)
def get_usb_ports() -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = list_ports.comports()

View File

@@ -19,21 +19,18 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IPP from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
# Create IPP instance for this entry
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = IPPDataUpdateCoordinator(
hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
base_path=entry.data[CONF_BASE_PATH],
tls=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -41,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -1,11 +1,14 @@
"""Receive signals from a keyboard and use it as a remote control."""
# pylint: disable=import-error
from __future__ import annotations
import asyncio
from contextlib import suppress
import logging
import os
from typing import Any
import aionotify
from asyncinotify import Inotify, Mask
from evdev import InputDevice, categorize, ecodes, list_devices
import voluptuous as vol
@@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the keyboard_remote."""
config = config[DOMAIN]
domain_config: list[dict[str, Any]] = config[DOMAIN]
remote = KeyboardRemote(hass, config)
remote = KeyboardRemote(hass, domain_config)
remote.setup()
return True
@@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class KeyboardRemote:
"""Manage device connection/disconnection using inotify to asynchronously monitor."""
def __init__(self, hass, config):
def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None:
"""Create handlers and setup dictionaries to keep track of them."""
self.hass = hass
self.handlers_by_name = {}
self.handlers_by_descriptor = {}
self.active_handlers_by_descriptor = {}
self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {}
self.inotify = None
self.watcher = None
self.monitor_task = None
@@ -110,16 +114,12 @@ class KeyboardRemote:
connected, and start monitoring for device connection/disconnection.
"""
# start watching
self.watcher = aionotify.Watcher()
self.watcher.watch(
alias="devinput",
path=DEVINPUT,
flags=aionotify.Flags.CREATE
| aionotify.Flags.ATTRIB
| aionotify.Flags.DELETE,
_LOGGER.debug("Start monitoring")
self.inotify = Inotify()
self.watcher = self.inotify.add_watch(
DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE
)
await self.watcher.setup(self.hass.loop)
# add initial devices (do this AFTER starting watcher in order to
# avoid race conditions leading to missing device connections)
@@ -134,7 +134,9 @@ class KeyboardRemote:
continue
self.active_handlers_by_descriptor[descriptor] = handler
initial_start_monitoring.add(handler.async_start_monitoring(dev))
initial_start_monitoring.add(
asyncio.create_task(handler.async_device_start_monitoring(dev))
)
if initial_start_monitoring:
await asyncio.wait(initial_start_monitoring)
@@ -146,6 +148,10 @@ class KeyboardRemote:
_LOGGER.debug("Cleanup on shutdown")
if self.inotify and self.watcher:
self.inotify.rm_watch(self.watcher)
self.watcher = None
if self.monitor_task is not None:
if not self.monitor_task.done():
self.monitor_task.cancel()
@@ -153,11 +159,16 @@ class KeyboardRemote:
handler_stop_monitoring = set()
for handler in self.active_handlers_by_descriptor.values():
handler_stop_monitoring.add(handler.async_stop_monitoring())
handler_stop_monitoring.add(
asyncio.create_task(handler.async_device_stop_monitoring())
)
if handler_stop_monitoring:
await asyncio.wait(handler_stop_monitoring)
if self.inotify:
self.inotify.close()
self.inotify = None
def get_device_handler(self, descriptor):
"""Find the correct device handler given a descriptor (path)."""
@@ -187,20 +198,21 @@ class KeyboardRemote:
async def async_monitor_devices(self):
"""Monitor asynchronously for device connection/disconnection or permissions changes."""
_LOGGER.debug("Start monitoring loop")
try:
while True:
event = await self.watcher.get_event()
async for event in self.inotify:
descriptor = f"{DEVINPUT}/{event.name}"
_LOGGER.debug("got events for %s: %s", descriptor, event.mask)
descriptor_active = descriptor in self.active_handlers_by_descriptor
if (event.flags & aionotify.Flags.DELETE) and descriptor_active:
if (event.mask & Mask.DELETE) and descriptor_active:
handler = self.active_handlers_by_descriptor[descriptor]
del self.active_handlers_by_descriptor[descriptor]
await handler.async_stop_monitoring()
await handler.async_device_stop_monitoring()
elif (
(event.flags & aionotify.Flags.CREATE)
or (event.flags & aionotify.Flags.ATTRIB)
(event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB)
) and not descriptor_active:
dev, handler = await self.hass.async_add_executor_job(
self.get_device_handler, descriptor
@@ -208,31 +220,32 @@ class KeyboardRemote:
if handler is None:
continue
self.active_handlers_by_descriptor[descriptor] = handler
await handler.async_start_monitoring(dev)
await handler.async_device_start_monitoring(dev)
except asyncio.CancelledError:
_LOGGER.debug("Monitoring canceled")
return
class DeviceHandler:
"""Manage input events using evdev with asyncio."""
def __init__(self, hass, dev_block):
def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None:
"""Fill configuration data."""
self.hass = hass
key_types = dev_block.get(TYPE)
key_types = dev_block[TYPE]
self.key_values = set()
for key_type in key_types:
self.key_values.add(KEY_VALUE[key_type])
self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD)
self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY)
self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT)
self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD]
self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY]
self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT]
self.monitor_task = None
self.dev = None
async def async_keyrepeat(self, path, name, code, delay, repeat):
async def async_device_keyrepeat(self, path, name, code, delay, repeat):
"""Emulate keyboard delay/repeat behaviour by sending key events on a timer."""
await asyncio.sleep(delay)
@@ -248,8 +261,9 @@ class KeyboardRemote:
)
await asyncio.sleep(repeat)
async def async_start_monitoring(self, dev):
async def async_device_start_monitoring(self, dev):
"""Start event monitoring task and issue event."""
_LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name)
if self.monitor_task is None:
self.dev = dev
self.monitor_task = self.hass.async_create_task(
@@ -261,7 +275,7 @@ class KeyboardRemote:
)
_LOGGER.debug("Keyboard (re-)connected, %s", dev.name)
async def async_stop_monitoring(self):
async def async_device_stop_monitoring(self):
"""Stop event monitoring task and issue event."""
if self.monitor_task is not None:
with suppress(OSError):
@@ -295,6 +309,7 @@ class KeyboardRemote:
_LOGGER.debug("Start device monitoring")
await self.hass.async_add_executor_job(dev.grab)
async for event in dev.async_read_loop():
# pylint: disable=no-member
if event.type is ecodes.EV_KEY:
if event.value in self.key_values:
_LOGGER.debug(categorize(event))
@@ -313,7 +328,7 @@ class KeyboardRemote:
and self.emulate_key_hold
):
repeat_tasks[event.code] = self.hass.async_create_task(
self.async_keyrepeat(
self.async_device_keyrepeat(
dev.path,
dev.name,
event.code,

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
"iot_class": "local_push",
"loggers": ["aionotify", "evdev"],
"requirements": ["evdev==1.4.0", "aionotify==0.2.0"]
"requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"]
}

View File

@@ -84,6 +84,7 @@ async def async_attach_trigger(
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
trigger_data = trigger_info["trigger_data"]
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
job = HassJob(action, f"KNX device trigger {trigger_info}")
knx: KNXModule = hass.data[DOMAIN]
@@ -95,7 +96,7 @@ async def async_attach_trigger(
return
hass.async_run_hass_job(
job,
{"trigger": telegram},
{"trigger": {**trigger_data, **telegram}},
)
return knx.telegrams.async_listen_telegram(

View File

@@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.10.0",
"xknxproject==3.1.0",
"knx_frontend==2023.5.31.141540"
"xknxproject==3.1.1",
"knx-frontend==2023.6.9.195839"
]
}

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Final
from knx_frontend import get_build_id, locate_dir
from knx_frontend import entrypoint_js, is_dev_build, locate_dir
import voluptuous as vol
from xknx.telegram import TelegramDirection
from xknxproject.exceptions import XknxProjectException
@@ -31,9 +31,10 @@ async def register_panel(hass: HomeAssistant) -> None:
if DOMAIN not in hass.data.get("frontend_panels", {}):
path = locate_dir()
build_id = get_build_id()
hass.http.register_static_path(
URL_BASE, path, cache_headers=(build_id != "dev")
URL_BASE,
path,
cache_headers=not is_dev_build,
)
await panel_custom.async_register_panel(
hass=hass,
@@ -41,12 +42,13 @@ async def register_panel(hass: HomeAssistant) -> None:
webcomponent_name="knx-frontend",
sidebar_title=DOMAIN.upper(),
sidebar_icon="mdi:bus-electric",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
module_url=f"{URL_BASE}/{entrypoint_js()}",
embed_iframe=True,
require_admin=True,
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/info",
@@ -129,6 +131,7 @@ async def ws_project_file_remove(
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/group_monitor_info",
@@ -155,6 +158,7 @@ def ws_group_monitor_info(
)
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "knx/subscribe_telegrams",

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from pylast import LastFMNetwork, User, WSError
from pylast import LastFMNetwork, PyLastError, User, WSError
import voluptuous as vol
from homeassistant.config_entries import (
@@ -128,11 +128,14 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
main_user, _ = get_lastfm_user(
self.data[CONF_API_KEY], self.data[CONF_MAIN_USER]
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
return self.async_show_form(
step_id="friends",
@@ -197,11 +200,14 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry):
self.options[CONF_API_KEY],
self.options[CONF_MAIN_USER],
)
friends_response = await self.hass.async_add_executor_job(
main_user.get_friends
)
friends = [
SelectOptionDict(value=friend.name, label=friend.get_name(True))
for friend in main_user.get_friends()
for friend in friends_response
]
except WSError:
except PyLastError:
friends = []
else:
friends = []

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import hashlib
from pylast import LastFMNetwork, Track, User, WSError
from pylast import LastFMNetwork, PyLastError, Track, User
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
@@ -104,26 +104,30 @@ class LastFmSensor(SensorEntity):
def update(self) -> None:
"""Update device state."""
self._attr_native_value = STATE_NOT_SCROBBLING
try:
self._user.get_playcount()
except WSError as exc:
play_count = self._user.get_playcount()
self._attr_entity_picture = self._user.get_image()
now_playing = self._user.get_now_playing()
top_tracks = self._user.get_top_tracks(limit=1)
last_tracks = self._user.get_recent_tracks(limit=1)
except PyLastError as exc:
self._attr_available = False
LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc)
return
self._attr_entity_picture = self._user.get_image()
if now_playing := self._user.get_now_playing():
self._attr_available = True
if now_playing:
self._attr_native_value = format_track(now_playing)
else:
self._attr_native_value = STATE_NOT_SCROBBLING
top_played = None
if top_tracks := self._user.get_top_tracks(limit=1):
top_played = format_track(top_tracks[0].item)
last_played = None
if last_tracks := self._user.get_recent_tracks(limit=1):
last_played = format_track(last_tracks[0].track)
play_count = self._user.get_playcount()
self._attr_extra_state_attributes = {
ATTR_LAST_PLAYED: last_played,
ATTR_PLAY_COUNT: play_count,
ATTR_TOP_PLAYED: top_played,
ATTR_LAST_PLAYED: None,
ATTR_TOP_PLAYED: None,
}
if len(last_tracks) > 0:
self._attr_extra_state_attributes[ATTR_LAST_PLAYED] = format_track(
last_tracks[0].track
)
if len(top_tracks) > 0:
self._attr_extra_state_attributes[ATTR_TOP_PLAYED] = format_track(
top_tracks[0].item
)

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.4.0"]
"requirements": ["pylitterbot==2023.4.2"]
}

View File

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

View File

@@ -13,7 +13,6 @@ from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import (
ATTR_LEVEL,
DEFAULT_LOGSEVERITY,
DOMAIN,
LOGGER_DEFAULT,
LOGGER_FILTERS,
@@ -39,9 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(
LOGGER_DEFAULT, default=DEFAULT_LOGSEVERITY
): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL,
vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}),
vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}),
}

View File

@@ -119,7 +119,7 @@ class LoggerSettings:
self._yaml_config = yaml_config
self._default_level = logging.INFO
if DOMAIN in yaml_config:
if DOMAIN in yaml_config and LOGGER_DEFAULT in yaml_config[DOMAIN]:
self._default_level = yaml_config[DOMAIN][LOGGER_DEFAULT]
self._store: Store[dict[str, dict[str, dict[str, Any]]]] = Store(
hass, STORAGE_VERSION, STORAGE_KEY

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
import asyncio
from contextlib import suppress
import async_timeout
from matter_server.client import MatterClient
from matter_server.client.exceptions import CannotConnect, InvalidServerVersion
from matter_server.common.errors import MatterError, NodeCommissionFailed
from matter_server.common.errors import MatterError, NodeCommissionFailed, NodeNotExists
import voluptuous as vol
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
@@ -207,7 +208,9 @@ async def async_remove_config_entry_device(
)
matter = get_matter(hass)
await matter.matter_client.remove_node(node.node_id)
with suppress(NodeNotExists):
# ignore if the server has already removed the node.
await matter.matter_client.remove_node(node.node_id)
return True

View File

@@ -92,7 +92,7 @@ class MatterAdapter:
get_clean_name(basic_info.nodeLabel)
or get_clean_name(basic_info.productLabel)
or get_clean_name(basic_info.productName)
or device_type.__class__.__name__
or device_type.__name__
if device_type
else None
)
@@ -117,7 +117,7 @@ class MatterAdapter:
identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}"))
model = (
get_clean_name(basic_info.productName) or device_type.__class__.__name__
get_clean_name(basic_info.productName) or device_type.__name__
if device_type
else None
)

View File

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

View File

@@ -38,7 +38,7 @@ def async_setup(hass: HomeAssistant) -> None:
class LocalSource(MediaSource):
"""Provide local directories as media sources."""
name: str = "Local Media"
name: str = "My media"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize local source."""

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/melnor",
"iot_class": "local_polling",
"requirements": ["melnor-bluetooth==0.0.22"]
"requirements": ["melnor-bluetooth==0.0.25"]
}

View File

@@ -133,10 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator_alert.async_refresh()
if not coordinator_alert.last_update_success:
raise ConfigEntryNotReady
hass.data[DOMAIN][department] = True
if coordinator_alert.last_update_success:
hass.data[DOMAIN][department] = True
else:
_LOGGER.warning(
(
@@ -158,11 +156,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = {
UNDO_UPDATE_LISTENER: undo_listener,
COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain,
COORDINATOR_ALERT: coordinator_alert,
UNDO_UPDATE_LISTENER: undo_listener,
}
if coordinator_alert and coordinator_alert.last_update_success:
hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/noaa_tides",
"iot_class": "cloud_polling",
"loggers": ["noaa_coops"],
"requirements": ["noaa-coops==0.1.8"]
"requirements": ["noaa-coops==0.1.9"]
}

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nuki",
"iot_class": "local_polling",
"loggers": ["pynuki"],
"requirements": ["pynuki==1.6.1"]
"requirements": ["pynuki==1.6.2"]
}

View File

@@ -4,5 +4,5 @@
"codeowners": ["@joostlek"],
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling",
"requirements": ["python-opensky==0.0.7"]
"requirements": ["python-opensky==0.0.9"]
}

View File

@@ -78,7 +78,7 @@ def setup_platform(
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
radius = config.get(CONF_RADIUS, 0)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius)
bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000)
session = async_get_clientsession(hass)
opensky = OpenSky(session=session)
add_entities(

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/opple",
"iot_class": "local_polling",
"loggers": ["pyoppleio"],
"requirements": ["pyoppleio==1.0.5"]
"requirements": ["pyoppleio-legacy==1.0.8"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0"]
"requirements": ["python-otbr-api==2.2.0"]
}

View File

@@ -95,6 +95,11 @@ class OTBRData:
"""Create an active operational dataset."""
return await self.api.create_active_dataset(dataset)
@_handle_otbr_error
async def delete_active_dataset(self) -> None:
"""Delete the active operational dataset."""
return await self.api.delete_active_dataset()
@_handle_otbr_error
async def set_active_dataset_tlvs(self, dataset: bytes) -> None:
"""Set current active operational dataset in TLVS format."""

View File

@@ -81,6 +81,12 @@ async def websocket_create_network(
connection.send_error(msg["id"], "set_enabled_failed", str(exc))
return
try:
await data.delete_active_dataset()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "delete_active_dataset_failed", str(exc))
return
try:
await data.create_active_dataset(
python_otbr_api.ActiveDataSet(

View File

@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.7.9"],
"requirements": ["pyoverkiz==1.8.0"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",

View File

@@ -29,8 +29,6 @@ ATTR_NOTIFICATION_ID: Final = "notification_id"
ATTR_TITLE: Final = "title"
ATTR_STATUS: Final = "status"
STATUS_UNREAD = "unread"
STATUS_READ = "read"
# Remove EVENT_PERSISTENT_NOTIFICATIONS_UPDATED in Home Assistant 2023.9
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated"
@@ -43,7 +41,6 @@ class Notification(TypedDict):
message: str
notification_id: str
title: str | None
status: str
class UpdateType(StrEnum):
@@ -98,7 +95,6 @@ def async_create(
notifications[notification_id] = {
ATTR_MESSAGE: message,
ATTR_NOTIFICATION_ID: notification_id,
ATTR_STATUS: STATUS_UNREAD,
ATTR_TITLE: title,
ATTR_CREATED_AT: dt_util.utcnow(),
}
@@ -135,7 +131,6 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the persistent notification component."""
notifications = _async_get_or_create_notifications(hass)
@callback
def create_service(call: ServiceCall) -> None:
@@ -152,29 +147,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Handle the dismiss notification service call."""
async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID])
@callback
def mark_read_service(call: ServiceCall) -> None:
"""Handle the mark_read notification service call."""
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
if notification_id not in notifications:
_LOGGER.error(
(
"Marking persistent_notification read failed: "
"Notification ID %s not found"
),
notification_id,
)
return
notification = notifications[notification_id]
notification[ATTR_STATUS] = STATUS_READ
async_dispatcher_send(
hass,
SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED,
UpdateType.UPDATED,
{notification_id: notification},
)
hass.services.async_register(
DOMAIN,
"create",
@@ -192,10 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION
)
hass.services.async_register(
DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION
)
websocket_api.async_register_command(hass, websocket_get_notifications)
websocket_api.async_register_command(hass, websocket_subscribe_notifications)

View File

@@ -33,15 +33,3 @@ dismiss:
example: 1234
selector:
text:
mark_read:
name: Mark read
description: Mark a notification read.
fields:
notification_id:
name: Notification ID
description: Target ID of the notification, which should be mark read.
required: true
example: 1234
selector:
text:

View File

@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback",
"iot_class": "local_polling",
"requirements": ["pulsectl==20.2.4"]
"requirements": ["pulsectl==23.5.2"]
}

View File

@@ -10,7 +10,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["regenmaschine"],
"requirements": ["regenmaschine==2023.05.1"],
"requirements": ["regenmaschine==2023.06.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -16,5 +16,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/rapt_ble",
"iot_class": "local_push",
"requirements": ["rapt-ble==0.1.1"]
"requirements": ["rapt-ble==0.1.2"]
}

View File

@@ -0,0 +1,38 @@
"""Support for the Airzone diagnostics."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RoborockDataUpdateCoordinator
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
return {
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
"coordinators": {
f"**REDACTED-{i}**": {
"roborock_device_info": async_redact_data(
coordinator.roborock_device_info.as_dict(), TO_REDACT_COORD
),
"api": coordinator.api.diagnostic_data,
}
for i, coordinator in enumerate(coordinators.values())
},
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/roborock",
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": ["python-roborock==0.17.0"]
"requirements": ["python-roborock==0.23.4"]
}

View File

@@ -1,5 +1,6 @@
"""Roborock Models."""
from dataclasses import dataclass
from typing import Any
from roborock.containers import HomeDataDevice, HomeDataProduct, NetworkInfo
from roborock.roborock_typing import DeviceProp
@@ -13,3 +14,12 @@ class RoborockHassDeviceInfo:
network_info: NetworkInfo
product: HomeDataProduct
props: DeviceProp
def as_dict(self) -> dict[str, dict[str, Any]]:
"""Turn RoborockHassDeviceInfo into a dictionary."""
return {
"device": self.device.as_dict(),
"network_info": self.network_info.as_dict(),
"product": self.product.as_dict(),
"props": self.props.as_dict(),
}

View File

@@ -90,8 +90,11 @@
"name": "Mop intensity",
"state": {
"off": "Off",
"low": "Low",
"mild": "Mild",
"medium": "Medium",
"moderate": "Moderate",
"high": "High",
"intense": "Intense",
"custom": "Custom"
}

View File

@@ -1,9 +1,11 @@
"""Support for Roborock switch."""
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
@@ -30,6 +32,8 @@ class RoborockSwitchDescriptionMixin:
evaluate_value: Callable[[dict], bool]
# Sets the status of the switch
set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]]
# Check support of this feature
check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]]
@dataclass
@@ -45,6 +49,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_CHILD_LOCK_STATUS
),
evaluate_value=lambda data: data["lock_status"] == 1,
key="child_lock",
translation_key="child_lock",
@@ -56,6 +63,9 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0}
),
get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS),
check_support=lambda data: data.api.send_command(
RoborockCommand.GET_FLOW_LED_STATUS
),
evaluate_value=lambda data: data["status"] == 1,
key="status_indicator",
translation_key="status_indicator",
@@ -75,16 +85,38 @@ async def async_setup_entry(
coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][
config_entry.entry_id
]
async_add_entities(
(
RoborockSwitchEntity(
f"{description.key}_{slugify(device_id)}",
coordinator,
description,
)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
possible_entities: list[
tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription]
] = [
(device_id, coordinator, description)
for device_id, coordinator in coordinators.items()
for description in SWITCH_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
results = await asyncio.gather(
*(
description.check_support(coordinator)
for _, coordinator, description in possible_entities
),
return_exceptions=True,
)
valid_entities: list[RoborockSwitchEntity] = []
for posible_entity, result in zip(possible_entities, results):
if isinstance(result, Exception):
if not isinstance(result, RoborockException):
raise result
_LOGGER.debug("Not adding entity because of %s", result)
else:
valid_entities.append(
RoborockSwitchEntity(
f"{posible_entity[2].key}_{slugify(posible_entity[0])}",
posible_entity[1],
posible_entity[2],
result,
)
)
async_add_entities(
valid_entities,
True,
)
@@ -99,10 +131,12 @@ class RoborockSwitchEntity(RoborockEntity, SwitchEntity):
unique_id: str,
coordinator: RoborockDataUpdateCoordinator,
entity_description: RoborockSwitchDescription,
initial_value: bool,
) -> None:
"""Create a switch entity."""
self.entity_description = entity_description
super().__init__(unique_id, coordinator.device_info, coordinator.api)
self._attr_is_on = initial_value
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""

View File

@@ -22,13 +22,11 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Roku from a config entry."""
hass.data.setdefault(DOMAIN, {})
if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)):
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
hass.data[DOMAIN][entry.entry_id] = coordinator
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -36,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push",
"loggers": ["russound_rio"],
"requirements": ["russound_rio==0.1.8"]
"requirements": ["russound-rio==1.0.0"]
}

View File

@@ -535,7 +535,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
async def shutdown(self) -> None:
"""Shutdown the coordinator."""
if self.device.connected:
await async_stop_scanner(self.device)
try:
await async_stop_scanner(self.device)
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
await self.device.shutdown()
await self._async_disconnected()

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
"iot_class": "local_push",
"loggers": ["sisyphus_control"],
"requirements": ["sisyphus-control==3.1.2"]
"requirements": ["sisyphus-control==3.1.3"]
}

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.1.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"],
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -606,7 +606,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
)
last_stats = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, statistic_id, True, {}
get_last_statistics, self.hass, 1, statistic_id, True, set()
)
if not last_stats:

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