Compare commits

..

172 Commits

Author SHA1 Message Date
Paulus Schoutsen
f170aba0cc Merge pull request #65955 from home-assistant/rc 2022-02-06 15:30:01 -08:00
M. Frister
66e076b57f Bump soco to 0.26.2 (#65919) 2022-02-06 14:35:14 -08:00
Paulus Schoutsen
1338b347b5 Remove duplicate methods 2022-02-06 14:33:07 -08:00
Paulus Schoutsen
9b471ab653 Bumped version to 2022.2.3 2022-02-06 14:23:08 -08:00
J. Nick Koston
e90a6bbe1c Add diagnostics support to HomeKit (#65942)
* Add diagnostics support to HomeKit

* remove debug
2022-02-06 14:23:03 -08:00
Michael
aa9965675d Improve device shutdown and unload of Synology DSM integration (#65936)
* ignore errors during unload/logout

* automatic host update is an info, nut debug
2022-02-06 14:23:02 -08:00
Joakim Sørensen
ad3b2f02b4 disabled_by can be None when updating devices (#65934) 2022-02-06 14:23:02 -08:00
Matthias Alphart
0dbe9b7cf4 Update xknx to 0.19.2 - fix TCP tunnelling (#65920) 2022-02-06 14:23:01 -08:00
J. Nick Koston
b9d346baed Fix loss of ability to control white channel in HomeKit on RGB&W lights (#65864)
* Fix loss of ability to control white channel in HomeKit on RGB&W lights

- Fix white channel missing from RGB/W lights

- Fix temp missing from RGB/CW lights

- Fixes #65529

* cover the missing case

* bright fix

* force brightness notify on color mode change as well
2022-02-06 14:23:00 -08:00
Jeef
7791711603 feat: bumped version (#65863) 2022-02-06 14:22:59 -08:00
jjlawren
fdfffcb73e Fix Spotify, Tidal, Apple Music playback on Sonos groups (#65838) 2022-02-06 14:22:59 -08:00
J. Nick Koston
8e6bd840a4 Fix flash at turn on with newer 0x04 Magic Home models (#65836) 2022-02-06 14:22:58 -08:00
Allen Porter
619a52a387 Fix legacy nest diagnostics to return empty rather than fail (#65824)
Fix legacy nest diangostics to return gracefully, rather than a TypError
by checking explicitiy for SDM in the config entry. Update diagnostics
to use the common nest test fixture, and extend with support for the
legacy nest config. Use the sdm test fixture in the existing legacy
tests so they all share the same config files.
2022-02-06 14:22:57 -08:00
Shay Levy
a4d59aa599 Bump aioshelly to 1.0.9 (#65803) 2022-02-06 14:22:57 -08:00
Ferdinand
4ba494f5cd Fix the restart when the saj device is down (#65796) 2022-02-06 14:22:56 -08:00
Franck Nijhof
7a7f9deb89 Update Pillow to 9.0.1 (#65779) 2022-02-06 14:19:47 -08:00
J. Nick Koston
5786f68bb7 Prevent multiple dhcp flows from being started for the same device/domain (#65753) 2022-02-06 14:19:46 -08:00
Aaron Bach
bccfe6646e Add redacted subscription data to SimpliSafe diagnostics (#65751) 2022-02-06 14:19:45 -08:00
ollo69
fc7ea6e1b3 Improve androidtv mac address handling and test coverage (#65749)
* Better mac addr handling and improve test coverage

* Apply suggested changes

* Apply more suggested changes
2022-02-06 14:19:44 -08:00
Aaron Bach
058420bb2f Bump simplisafe-python to 2022.02.0 (#65748) 2022-02-06 14:19:44 -08:00
Maciej Bieniek
9695235920 Fix wind speed unit (#65723) 2022-02-06 14:19:43 -08:00
J. Nick Koston
57526bd21f Add coverage for color_rgbww_to_rgb, fix divzero case (#65721) 2022-02-06 14:19:42 -08:00
Sean Vig
eff9690c8a Fix Amcrest service calls (#65717)
Fixes #65522
Fixes #65647
2022-02-06 14:19:42 -08:00
Aidan Timson
d754ea1645 Fix OVO Energy NoneType error occurring for some users (#65714) 2022-02-06 14:19:41 -08:00
Michael
5f6214ede7 check wan access type (#65389) 2022-02-06 14:19:40 -08:00
Paulus Schoutsen
0f02ae981d Merge pull request #65713 from home-assistant/rc 2022-02-04 12:46:28 -08:00
Paulus Schoutsen
51abdf9c63 Bumped version to 2022.2.2 2022-02-04 12:02:06 -08:00
Franck Nijhof
1a2e9aaaed Depend on diagnostics in the frontend (#65710) 2022-02-04 12:01:35 -08:00
Paulus Schoutsen
56d1fc6dad Fix tuya diagnostics mutating cached state objects (#65708) 2022-02-04 12:01:34 -08:00
Paulus Schoutsen
5a44f8eadd Fix passing a string to device registry disabled_by (#65701) 2022-02-04 12:01:33 -08:00
Paulus Schoutsen
609661a862 Move scene and button restore to internal hook (#65696) 2022-02-04 12:01:33 -08:00
Joakim Sørensen
27dbf98dae Allow selecting own repositories (#65695) 2022-02-04 12:01:19 -08:00
jkuettner
6cf2665200 Fix "vevent" KeyError in caldav component again (#65685)
* Fix "vevent" KeyError in caldav component again

* code formatting
2022-02-04 12:00:02 -08:00
Paulus Schoutsen
5aa02b884e Call out 3rd party containers more clearly (#65684) 2022-02-04 12:00:01 -08:00
J. Nick Koston
84b2ec2244 Fix warm/cold reversal in rgbww_to_color_temperature (#65677) 2022-02-04 12:00:01 -08:00
epenet
35f2536d46 Bump renault-api to 0.1.8 (#65670)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-02-04 12:00:00 -08:00
Erik Montnemery
e6e95a1131 Only remove duplicated statistics on error (#65653) 2022-02-04 11:59:59 -08:00
Erik Montnemery
ea1245f308 Improve recorder migration for PostgreSQL when columns already exist (#65680) 2022-02-04 11:59:39 -08:00
Erik Montnemery
9cd6bb7335 Don't use shared session during recorder migration (#65672) 2022-02-04 11:59:26 -08:00
Erik Montnemery
4e3cd1471a Remove limit of amount of duplicated statistics (#65641) 2022-02-04 11:58:02 -08:00
alexanv1
67a9932c5c Fix Z-Wave lights (#65638)
* Fix Z-Wave lights

* Update tests
2022-02-04 11:58:01 -08:00
J. Nick Koston
0efa276fca Bump flux_led to 0.28.20 (#65621) 2022-02-04 11:58:01 -08:00
Paulus Schoutsen
c6d5a0842b Bump homematicip to 1.0.2 (#65620) 2022-02-04 11:58:00 -08:00
Jeff Irion
b004c5deb6 Bump androidtv to 0.0.63 (fix MAC issues) (#65615) 2022-02-04 11:57:59 -08:00
Jeff Irion
06b6b176db Bump androidtv to 0.0.62 (#65440) 2022-02-04 11:57:58 -08:00
Raman Gupta
9eeaec4f79 Raise when zwave_js device automation fails validation (#65610) 2022-02-04 11:55:51 -08:00
Duco Sebel
3babc43fa5 Add migration to migrate 'homewizard_energy' to 'homewizard' (#65594) 2022-02-04 11:55:50 -08:00
Thomas Schamm
a670317b80 Bumped boschshcpy 0.2.28 to 0.2.29 (#65328) 2022-02-04 11:55:49 -08:00
Paulus Schoutsen
f44f1f0c4a Merge pull request #65606 from home-assistant/rc 2022-02-03 16:57:24 -08:00
Shay Levy
b450a41d7b Fix Shelly Plus i4 KeyError (#65604) 2022-02-03 16:19:57 -08:00
J. Nick Koston
88c3ab1113 Fix lutron_caseta button events including area name in device name (#65601) 2022-02-03 16:05:20 -08:00
Paulus Schoutsen
f1c8fc241a Merge pull request #65598 from home-assistant/rc 2022-02-03 16:04:08 -08:00
Paulus Schoutsen
e5b9d5baa3 Bumped version to 2022.2.1 2022-02-03 15:06:46 -08:00
jjlawren
3c43089cc2 Log traceback in debug for Sonos unsubscribe errors (#65596) 2022-02-03 15:06:38 -08:00
Franck Nijhof
c8827e00b3 Update pvo to 0.2.1 (#65584) 2022-02-03 15:06:37 -08:00
Michael
3a1a12b13e Extend diagnostics data in Fritz!Tools (#65573) 2022-02-03 15:06:37 -08:00
Bram Kragten
2928935838 Update frontend to 20220203.0 (#65572) 2022-02-03 15:06:36 -08:00
Franck Nijhof
b9a37e2c3e Guard against empty Tuya data types (#65571) 2022-02-03 15:06:35 -08:00
G Johansson
92f4f99d41 Add back resolvers config flow dnsip (#65570) 2022-02-03 15:06:34 -08:00
Franck Nijhof
e32a54eecc Add missing Tuya vacuum states (#65567) 2022-02-03 15:06:34 -08:00
Michael
2bb65ecf38 Fix data update when guest client disappears in Fritz!Tools (#65564)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2022-02-03 15:06:33 -08:00
J. Nick Koston
afbc55b181 Do not update unifiprotect host from discovery if its not an ip (#65548) 2022-02-03 15:06:32 -08:00
Jan Bouwhuis
931c27f452 Return current state if template throws (#65534) 2022-02-03 15:06:31 -08:00
Franck Nijhof
689133976a Fix missing windspeed in Tuya climate (#65511) 2022-02-03 15:06:31 -08:00
Eduard van Valkenburg
faa8ac692e Fix SIA availability (#65509) 2022-02-03 15:06:30 -08:00
Aaron Bach
ec0b0e41a1 Bump pytile to 2022.02.0 (#65482) 2022-02-03 15:06:29 -08:00
Aaron Bach
6550d04313 Allow Flu Near You to re-attempt startup on error (#65481) 2022-02-03 15:06:28 -08:00
jjlawren
8d33964e4d Fix vanished checks on old Sonos firmware (#65477) 2022-02-03 15:06:27 -08:00
Aaron Bach
d195e8a1b4 Catch correct error during OpenUV startup (#65459) 2022-02-03 15:06:26 -08:00
Aaron Bach
a8b29c4be9 Fix unknown alarm websocket event error for restored SimpliSafe connections (#65457) 2022-02-03 15:06:26 -08:00
mk-maddin
f7ec373aab Fix script / automation repeat with count 0 fails (#65448)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-02-03 15:06:24 -08:00
Colin Robbins
1ae2bfcc89 Fix Shodan sensor (#65443) 2022-02-03 15:06:24 -08:00
Maciej Bieniek
1155d229f3 Get wind speed unit from AccuWeather data (#65425) 2022-02-03 15:06:23 -08:00
Franck Nijhof
2f638a6b5e Merge pull request #65442 from home-assistant/rc 2022-02-02 19:44:34 +01:00
Franck Nijhof
580573fcb3 Bumped version to 2022.2.0 2022-02-02 18:12:26 +01:00
Bram Kragten
8851af7dba Update frontend to 20220202.0 (#65432) 2022-02-02 18:11:28 +01:00
Erik Montnemery
ec2e450442 Stringify MQTT payload in mqtt/debug/info WS response (#65429) 2022-02-02 18:11:24 +01:00
Maikel Punie
dacf5957d2 Bump velbus-aio to 2022.2.1 (#65422) 2022-02-02 18:11:20 +01:00
Erik Montnemery
5190282b4d Don't warn on time.sleep injected by the debugger (#65420) 2022-02-02 18:11:16 +01:00
Josh Shoemaker
51c6cac74d Bump aladdin_connect to 0.4 to fix integration for some users due to API changes (#65407) 2022-02-02 18:11:11 +01:00
J. Nick Koston
1809489421 Ensure unifiprotect discovery can be ignored (#65406) 2022-02-02 18:11:07 +01:00
J. Nick Koston
690764ec84 Bump lutron_caseta to 0.13.1 to fix setup when no button devices are present (#65400) 2022-02-02 18:11:03 +01:00
jjlawren
2b0e828736 Fix Sonos diagnostics with offline device (#65393)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-02-02 18:10:59 +01:00
Pierre Ståhl
91023cf132 Sort Apple TV app list by name (#65386) 2022-02-02 18:10:54 +01:00
Pierre Ståhl
fcd14e2830 Fix disconnect bug in Apple TV integration (#65385) 2022-02-02 18:10:49 +01:00
jjlawren
40a174cc70 Detect battery-operated Sonos devices going offline (#65382) 2022-02-02 18:10:44 +01:00
J. Nick Koston
95d4be375c Handle brightness being None for senseme (#65372) 2022-02-02 18:10:40 +01:00
Jan Bouwhuis
37f9c833c0 Fix MQTT expire_after effects after reloading (#65359)
* Cleanup sensor expire triggers after reload

* fix test binary_sensor

* Also trigger cleanup parent classes

* Restore an expiring state after a reload

* correct discovery_update

* restore expiring state with remaining time

* Update homeassistant/components/mqtt/binary_sensor.py

description

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Log remaining time

* Move check

* check and tests reload

* remove self.async_write_ha_state()

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-02-02 18:10:36 +01:00
Erik Montnemery
b902c59504 Report unmet dependencies for failing config flows (#65061)
* Report unmet dependencies for failing config flows

* Apply suggestions from code review

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

* Update homeassistant/setup.py

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

* Modify error message

* Add test

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-02 18:10:30 +01:00
Paulus Schoutsen
ba237fd383 Bumped version to 2022.2.0b6 2022-02-01 10:00:39 -08:00
Paulus Schoutsen
b687f68d53 Bump frontend to 20220201.0 (#65380) 2022-02-01 10:00:36 -08:00
Robert Svensson
f3c39d8dca Redact host address in UniFi diagnostics (#65379) 2022-02-01 09:59:25 -08:00
Michael
19fff6489b Fix wan_access switch for disconnected devices in Fritz!Tools (#65378) 2022-02-01 09:59:24 -08:00
Bram Kragten
4f8752b351 Allow removing keys from automation (#65374) 2022-02-01 09:59:23 -08:00
G Johansson
03bd3f5001 Fix options for dnsip (#65369) 2022-02-01 09:59:22 -08:00
Erik Montnemery
055382c84c Improve CastProtocol (#65357)
* Improve CastProtocol

* Tweak
2022-02-01 09:59:21 -08:00
Paulus Schoutsen
68651be2cc Simplify unifi cleanup logic (#65345) 2022-02-01 09:59:21 -08:00
schreyack
09c2c129b9 Fix honeywell hold mode (#65327)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-02-01 09:59:20 -08:00
ZuluWhiskey
7fe1b85495 Fix MotionEye config flow (#64360)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-01 09:59:19 -08:00
Paulus Schoutsen
5082582769 Bumped version to 2022.2.0b5 2022-01-31 17:12:40 -08:00
Paulus Schoutsen
b7c7571a39 I zone, you zone, we zoning (#65344) 2022-01-31 17:12:26 -08:00
Simone Chemelli
63a90b7226 Add diagnostics for SamsungTV (#65342) 2022-01-31 17:12:25 -08:00
J. Nick Koston
5735762af2 Bump zeroconf to 0.38.3 (#65341) 2022-01-31 17:12:25 -08:00
Paulus Schoutsen
90127d04fa Bump aiohue to 4.0.1 (#65340) 2022-01-31 17:12:24 -08:00
Paulus Schoutsen
114da0bd4f Bump version tag on async_timeout warning (#65339) 2022-01-31 17:12:23 -08:00
J. Nick Koston
5c3d4cb9a5 Prevent unifiprotect from being rediscovered on UDM-PROs (#65335) 2022-01-31 17:12:23 -08:00
Simone Chemelli
3f8d2f3102 Add diagnostics support to Fritz (#65334)
* Add diagnostics support to Fritz

* Temporary remove tests

* coveragerc
2022-01-31 17:12:22 -08:00
J. Nick Koston
eea9e26ef5 Fix guardian being rediscovered via dhcp (#65332) 2022-01-31 17:12:21 -08:00
Michael
649b4ce329 Improve debugging and error handling in Fritz!Tools (#65324) 2022-01-31 17:12:21 -08:00
Simone Chemelli
1facd0edd4 Fritz tests cleanup (#65054) 2022-01-31 17:12:20 -08:00
Paulus Schoutsen
1fbd624a24 Alexa to handle brightness and catch exceptions (#65322) 2022-01-31 17:10:25 -08:00
Pascal Winters
0a000babc9 Bump pyps4-2ndscreen to 1.3.1 (#65320) 2022-01-31 17:10:25 -08:00
Franck Nijhof
74632d26fa Ensure PVOutput connection error is logged (#65319) 2022-01-31 17:10:24 -08:00
Franck Nijhof
87b20c6abe Update tailscale to 0.2.0 (#65318) 2022-01-31 17:10:23 -08:00
Franck Nijhof
ea511357b6 Add diagnostics support to WLED (#65317) 2022-01-31 17:10:23 -08:00
Jeff Irion
00b2c85e98 Bump androidtv to 0.0.61 (#65315) 2022-01-31 17:10:22 -08:00
J. Nick Koston
961cf15e6e Improve reliability of august setup with recent api changes (#65314) 2022-01-31 17:10:21 -08:00
Franck Nijhof
7117395489 Fix missing expiration data in Whois information (#65313) 2022-01-31 17:10:21 -08:00
Franck Nijhof
5dc92bb2ce Update wled to 0.13.0 (#65312) 2022-01-31 17:10:19 -08:00
Franck Nijhof
0519b29501 Update adguard to 0.5.1 (#65305) 2022-01-31 17:10:19 -08:00
Duco Sebel
4f8e19ed4a Add HomeWizard diagnostics (#65297)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-01-31 17:10:18 -08:00
Duco Sebel
fd7f66fbdc Fix HomeWizard unclosed clientsession error when closing Home Assistant (#65296) 2022-01-31 17:10:17 -08:00
Mick Vleeshouwer
9294319048 Bump pyoverkiz to 1.3.2 (#65293) 2022-01-31 17:09:29 -08:00
starkillerOG
c1019394ed Bump pynetgear to 0.9.1 (#65290) 2022-01-31 17:06:23 -08:00
Erik Montnemery
0885d48186 Correct cast media browse filter for audio groups (#65288) 2022-01-31 17:06:22 -08:00
Tobias Sauerwein
13ad1cc56c Bump pyatmo to v.6.2.4 (#65285)
* Bump pyatmo to v6.2.3

Signed-off-by: cgtobi <cgtobi@gmail.com>

* Bump pyatmo to v6.2.4

Signed-off-by: cgtobi <cgtobi@gmail.com>
2022-01-31 17:06:21 -08:00
J. Nick Koston
c5d68f8669 Increase august timeout and make failure to sync at startup non-fatal (#65281) 2022-01-31 17:06:21 -08:00
fOmey
2757976a5a Tuya fan percentage fix (#65225) 2022-01-31 17:06:20 -08:00
Erik Montnemery
73750d8a25 Add cast platform for extending Google Cast media_player (#65149)
* Add cast platform for extending Google Cast media_player

* Update tests

* Refactor according to review comments

* Add test for playing using a cast platform

* Apply suggestions from code review

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

* Pass cast type instead of a filter function when browsing

* Raise on invalid cast platform

* Test media browsing

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>
2022-01-31 17:06:19 -08:00
jjlawren
2eef05eb84 Send notification to alert of Sonos networking issues (#65084)
* Send notification to alert of Sonos networking issues

* Add links to documentation
2022-01-31 17:06:19 -08:00
Brett Adams
3446c95cd3 Add diagnostics to Advantage Air (#65006)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-01-31 17:06:18 -08:00
Teemu R
cdcbb87d97 Bump python-kasa to 0.4.1 for tplink integration (#64123)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-01-31 17:06:17 -08:00
Paulus Schoutsen
ef143b5eb2 Bumped version to 2022.2.0b4 2022-01-30 20:28:42 -08:00
Matthias Alphart
5d7aefa0b4 Update xknx to 0.19.1 (#65275) 2022-01-30 20:28:34 -08:00
Brynley McDonald
6b6bd381fd Fix flick_electric auth failures (#65274) 2022-01-30 20:28:34 -08:00
Shay Levy
252f5f6b35 Bump aiowebostv to 0.1.2 (#65267) 2022-01-30 20:28:33 -08:00
J. Nick Koston
8bdee9cb1c Simplify whois value_fn (#65265) 2022-01-30 20:28:32 -08:00
J. Nick Koston
7e350b8347 Handle missing attrs in whois results (#65254)
* Handle missing attrs in whois results

- Some attrs are not set depending on where the
  domain is registered

- Fixes #65164

* Set to unknown instead of do not create

* no multi-line lambda
2022-01-30 20:28:32 -08:00
J. Nick Koston
ac8a1248f9 Fix debugpy blocking the event loop at startup (#65252) 2022-01-30 20:28:31 -08:00
J. Nick Koston
ffe262abce Fix flux_led not generating unique ids when discovery fails (#65250) 2022-01-30 20:28:30 -08:00
J. Nick Koston
5174e68b16 Fix powerwall login retry when hitting rate limit (#65245) 2022-01-30 20:28:30 -08:00
Shay Levy
6e4c281e15 Fix webostv live TV source missing when configuring sources (#65243) 2022-01-30 20:28:29 -08:00
Joakim Sørensen
8e71e2e8ee Use .json.txt for diagnostics download filetype (#65236) 2022-01-30 20:28:28 -08:00
J. Nick Koston
26905115c8 Increase the timeout for flux_led directed discovery (#65222) 2022-01-30 20:28:28 -08:00
J. Nick Koston
eca3514f9e Fix senseme fan lights (#65217) 2022-01-30 20:28:27 -08:00
jjlawren
305ffc4ab6 Add activity statistics to Sonos diagnostics (#65214) 2022-01-30 20:28:26 -08:00
Robert Svensson
508fd0cb2a Add logic to avoid creating the same scene multiple times (#65207) 2022-01-30 20:28:25 -08:00
Shay Levy
5368fb6d54 Fix webostv configure sources when selected source is missing (#65195)
* Fix webostv configure sources when selected source is missing

* Add comment for filtering duplicates
2022-01-30 20:28:25 -08:00
Michael
d6527953c3 Fix "internet access" switch for Fritz connected device without known IP address (#65190)
* fix get wan access

* small improvement
- default wan_access to None
- test if dev_info.ip_address is not empty
2022-01-30 20:28:24 -08:00
Robert Svensson
14c969ef6d Better manage of nested lists (#65176) 2022-01-30 20:28:23 -08:00
Aaron Bach
f6f25fa4ff Add diagnostics to SimpliSafe (#65171)
* Add diagnostics to SimpliSafe

* Bump

* Cleanup
2022-01-30 20:28:23 -08:00
Aaron Bach
dcf6e61d4f Ensure diagnostics redaction can handle lists of lists (#65170)
* Ensure diagnostics redaction can handle lists of lists

* Code review

* Update homeassistant/components/diagnostics/util.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Code review

* Typing

* Revert "Typing"

This reverts commit 8a57f772caa5180b609175591d81dfc473769f70.

* New typing attempt

* Revert "New typing attempt"

This reverts commit e26e4aae69f62325fdd6af4d80c8fd1f74846e54.

* Fix typing

* Fix typing again

* Add tests

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-01-30 20:28:22 -08:00
Aaron Bach
2041d4c118 Clean up SimpliSafe config flow tests (#65167)
* Clean up SimpliSafe config flow tests

* Cleanup
2022-01-30 20:28:21 -08:00
starkillerOG
b40bcecac0 Aqara restore door sensor state on start (#65128)
* restore door sensor state on start

* fix import

* fix issues

* also fix Natgas, WaterLeak and Smoke sensors

* remove unnesesary async_schedule_update_ha_state
2022-01-30 20:28:21 -08:00
Erik Montnemery
2ed20df906 Minor refactoring of cast media_player (#65125) 2022-01-30 20:28:20 -08:00
Marvin Wichmann
1a6964448c Fix KNX Expose for strings longer than 14 bytes (#63026)
* Fix KNX Expose for too long strings

* Fix tests

* Catch exception and avoid error during config entry setup for exposures

* Properly catch exceptions in knx expose

* Fix pylint

* Fix CI

* Add test for conversion error
2022-01-30 20:28:19 -08:00
Marvin Wichmann
3dde12f887 Add tests for KNX diagnostic and expose (#64938)
* Add test for KNX diagnostic

* Add test for KNX expose

* Apply review suggestions
2022-01-30 20:27:37 -08:00
Paulus Schoutsen
cd6c182c07 Bumped version to 2022.2.0b3 2022-01-28 21:53:21 -08:00
J. Nick Koston
f8e0c41e91 Fix uncaught exception during isy994 dhcp discovery with ignored entry (#65165) 2022-01-28 21:53:12 -08:00
J. Nick Koston
5f56107116 Add additional blink OUIs to DHCP discovery (#65162) 2022-01-28 21:53:11 -08:00
J. Nick Koston
fb3c99a891 Add additional roomba OUIs to DHCP discovery (#65161) 2022-01-28 21:53:11 -08:00
J. Nick Koston
ca505b79b5 Add dhcp discovery to oncue (#65160) 2022-01-28 21:53:10 -08:00
J. Nick Koston
c74a8bf65a Add OUI for KL430 tplink light strip to discovery (#65159) 2022-01-28 21:53:09 -08:00
Franck Nijhof
406801ef73 Fix setting speed of Tuya fan (#65155) 2022-01-28 21:53:09 -08:00
Marc Mueller
2bfedcbdc5 Move remaining keys to setup.cfg (#65154)
* Move metadata keys

* Move options

* Delete setup.py

* Remove unused constants
* Remove deprecated test_suite key

* Improve metadata

* Only include homeassistant*, not script*
* Add long_desc_content_type
* Remove license file (auto-included by setuptools + wheels)

* Add setup.py

Pip 21.2 doesn't support editable installs without it.
2022-01-28 21:53:08 -08:00
Simone Chemelli
84f817eb25 Fix status for Fritz device tracker (#65152) 2022-01-28 21:53:07 -08:00
Simone Chemelli
4ead2f2f7e Fix excepton for SamsungTV getting device info (#65151) 2022-01-28 21:53:07 -08:00
Marc Mueller
421f9716a7 Use isolated build environments (#65145) 2022-01-28 21:53:06 -08:00
Allen Porter
25e6d8858c Update nest diagnostics (#65141) 2022-01-28 21:53:05 -08:00
Marc Mueller
3829a81d15 Move project_urls to setup.cfg (#65129) 2022-01-28 21:53:05 -08:00
Marc Mueller
9318843867 Move version metadata key to setup.cfg (#65091)
* Move version to setup.cfg
* Move python_requires to setup.cfg
* Add script to validate project metadata
* Add dedicated pre-commit hook
2022-01-28 21:53:04 -08:00
Marc Mueller
4eb787b619 Move install_requires to setup.cfg (#65095) 2022-01-28 21:52:33 -08:00
269 changed files with 6726 additions and 2067 deletions

View File

@@ -27,6 +27,7 @@ omit =
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/advantage_air/diagnostics.py
homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/*
homeassistant/components/agent_dvr/alarm_control_panel.py
@@ -375,6 +376,7 @@ omit =
homeassistant/components/fritz/common.py
homeassistant/components/fritz/const.py
homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritz/diagnostics.py
homeassistant/components/fritz/sensor.py
homeassistant/components/fritz/services.py
homeassistant/components/fritz/switch.py
@@ -462,6 +464,7 @@ omit =
homeassistant/components/homematic/*
homeassistant/components/home_plus_control/api.py
homeassistant/components/home_plus_control/switch.py
homeassistant/components/homewizard/diagnostics.py
homeassistant/components/homeworks/*
homeassistant/components/honeywell/__init__.py
homeassistant/components/honeywell/climate.py
@@ -560,12 +563,7 @@ omit =
homeassistant/components/knx/__init__.py
homeassistant/components/knx/climate.py
homeassistant/components/knx/cover.py
homeassistant/components/knx/diagnostics.py
homeassistant/components/knx/expose.py
homeassistant/components/knx/knx_entity.py
homeassistant/components/knx/light.py
homeassistant/components/knx/notify.py
homeassistant/components/knx/schema.py
homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/const.py
@@ -949,6 +947,7 @@ omit =
homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py
homeassistant/components/samsungtv/bridge.py
homeassistant/components/samsungtv/diagnostics.py
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py

View File

@@ -76,8 +76,10 @@ jobs:
- name: Build package
shell: bash
run: |
pip install twine wheel
python setup.py sdist bdist_wheel
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload package
shell: bash

View File

@@ -107,7 +107,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/manifest\.json|setup\.py|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
- id: hassfest
name: hassfest
entry: script/run-in-env.sh python3 -m script.hassfest
@@ -115,3 +115,10 @@ repos:
language: script
types: [text]
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|\.strict-typing|homeassistant/.+/services\.yaml|script/hassfest/.+\.py)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
pass_filenames: false
language: script
types: [text]
files: ^(script/hassfest/.+\.py|homeassistant/const\.py$|setup\.cfg)$

View File

@@ -1,4 +1,3 @@
include README.rst
include LICENSE.md
graft homeassistant
recursive-exclude * *.py[co]

View File

@@ -17,7 +17,7 @@ def timeout(
loop = asyncio.get_running_loop()
else:
report(
"called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2",
"called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.3",
error_if_core=False,
)
if delay is not None:
@@ -30,7 +30,7 @@ def timeout(
def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None:
"""Backwards compatible current_task."""
report(
"called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead",
"called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.3; use asyncio.current_task instead",
error_if_core=False,
)
return asyncio.current_task()

View File

@@ -17,7 +17,12 @@ from homeassistant.components.weather import (
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.const import (
CONF_NAME,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
@@ -62,6 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Initialize."""
super().__init__(coordinator)
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][
"Unit"
]
if wind_speed_unit == "mi/h":
self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR
else:
self._attr_wind_speed_unit = wind_speed_unit
self._attr_name = name
self._attr_unique_id = coordinator.location_key
self._attr_temperature_unit = (

View File

@@ -80,8 +80,8 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
adguard = AdGuardHome(
user_input[CONF_HOST],
port=user_input[CONF_PORT],
username=username, # type:ignore[arg-type]
password=password, # type:ignore[arg-type]
username=username,
password=password,
tls=user_input[CONF_SSL],
verify_ssl=user_input[CONF_VERIFY_SSL],
session=session,

View File

@@ -3,7 +3,7 @@
"name": "AdGuard Home",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard",
"requirements": ["adguardhome==0.5.0"],
"requirements": ["adguardhome==0.5.1"],
"codeowners": ["@frenck"],
"iot_class": "local_polling"
}

View File

@@ -0,0 +1,25 @@
"""Provides diagnostics for Advantage Air."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data
# Return only the relevant children
return {
"aircons": data["aircons"],
"system": async_redact_data(data["system"], TO_REDACT),
}

View File

@@ -2,7 +2,7 @@
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["aladdin_connect==0.3"],
"requirements": ["aladdin_connect==0.4"],
"codeowners": [],
"iot_class": "cloud_polling"
}

View File

@@ -1,6 +1,8 @@
"""Alexa related errors."""
from __future__ import annotations
from typing import Literal
from homeassistant.exceptions import HomeAssistantError
from .const import API_TEMP_UNITS
@@ -58,6 +60,30 @@ class AlexaInvalidValueError(AlexaError):
error_type = "INVALID_VALUE"
class AlexaInteralError(AlexaError):
"""Class to represent internal errors."""
namespace = "Alexa"
error_type = "INTERNAL_ERROR"
class AlexaNotSupportedInCurrentMode(AlexaError):
"""The device is not in the correct mode to support this command."""
namespace = "Alexa"
error_type = "NOT_SUPPORTED_IN_CURRENT_MODE"
def __init__(
self,
endpoint_id: str,
current_mode: Literal["COLOR", "ASLEEP", "NOT_PROVISIONED", "OTHER"],
) -> None:
"""Initialize invalid endpoint error."""
msg = f"Not supported while in {current_mode} mode"
AlexaError.__init__(self, msg, {"currentDeviceMode": current_mode})
self.endpoint_id = endpoint_id
class AlexaUnsupportedThermostatModeError(AlexaError):
"""Class to represent UnsupportedThermostatMode errors."""

View File

@@ -212,20 +212,14 @@ async def async_api_adjust_brightness(hass, config, directive, context):
entity = directive.entity
brightness_delta = int(directive.payload["brightnessDelta"])
# read current state
try:
current = math.floor(
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100
)
except ZeroDivisionError:
current = 0
# set brightness
brightness = max(0, brightness_delta + current)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness},
{
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_STEP_PCT: brightness_delta,
},
blocking=False,
context=context,
)

View File

@@ -48,8 +48,18 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
response = directive.error()
except AlexaError as err:
response = directive.error(
error_type=err.error_type, error_message=err.error_message
error_type=err.error_type,
error_message=err.error_message,
payload=err.payload,
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Uncaught exception processing Alexa %s/%s request (%s)",
directive.namespace,
directive.name,
directive.entity_id or "-",
)
response = directive.error(error_message="Unknown error")
request_info = {"namespace": directive.namespace, "name": directive.name}

View File

@@ -515,8 +515,8 @@ class AmcrestCam(Camera):
max_tries = 3
for tries in range(max_tries, 0, -1):
try:
await getattr(self, f"_set_{func}")(value)
new_value = await getattr(self, f"_get_{func}")()
await getattr(self, f"_async_set_{func}")(value)
new_value = await getattr(self, f"_async_get_{func}")()
if new_value != value:
raise AmcrestCommandFailed
except (AmcrestError, AmcrestCommandFailed) as error:

View File

@@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
@@ -33,16 +34,30 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_FIRETV,
DOMAIN,
PROP_ETHMAC,
PROP_SERIALNO,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
PLATFORMS = [Platform.MEDIA_PLAYER]
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_LOGGER = logging.getLogger(__name__)
def get_androidtv_mac(dev_props):
"""Return formatted mac from device properties."""
for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC):
if if_mac := dev_props.get(prop_mac):
mac = format_mac(if_mac)
if mac not in _INVALID_MACS:
return mac
return None
def _setup_androidtv(hass, config):
"""Generate an ADB key (if needed) and load it."""
adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey"))

View File

@@ -11,9 +11,8 @@ from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from . import async_connect_androidtv
from . import async_connect_androidtv, get_androidtv_mac
from .const import (
CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT,
@@ -124,9 +123,15 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return RESULT_CONN_ERROR, None
dev_prop = aftv.device_properties
unique_id = format_mac(
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
_LOGGER.info(
"Android TV at %s: %s = %r, %s = %r",
user_input[CONF_HOST],
PROP_ETHMAC,
dev_prop.get(PROP_ETHMAC),
PROP_WIFIMAC,
dev_prop.get(PROP_WIFIMAC),
)
unique_id = get_androidtv_mac(dev_prop)
await aftv.adb_close()
return None, unique_id

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
"adb-shell[async]==0.4.0",
"androidtv[async]==0.0.60",
"androidtv[async]==0.0.63",
"pure-python-adb[async]==0.3.0.dev0"
],
"codeowners": ["@JeffLIrion", "@ollo69"],

View File

@@ -51,12 +51,13 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import get_androidtv_mac
from .const import (
ANDROID_DEV,
ANDROID_DEV_OPT,
@@ -80,8 +81,6 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_CLASSES,
DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
@@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_SW_VERSION):
self._attr_device_info[ATTR_SW_VERSION] = sw_version
if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")):
if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._app_id_to_name = {}

View File

@@ -179,7 +179,6 @@ class AppleTVManager:
def _handle_disconnect(self):
"""Handle that the device disconnected and restart connect loop."""
if self.atv:
self.atv.listener = None
self.atv.close()
self.atv = None
self._dispatch_send(SIGNAL_DISCONNECTED)
@@ -196,8 +195,6 @@ class AppleTVManager:
self._is_on = False
try:
if self.atv:
self.atv.push_updater.listener = None
self.atv.push_updater.stop()
self.atv.close()
self.atv = None
if self._task:

View File

@@ -162,15 +162,15 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
except exceptions.ProtocolError:
_LOGGER.exception("Failed to update app list")
else:
self._app_list = {app.name: app.identifier for app in apps}
self._app_list = {
app.name: app.identifier
for app in sorted(apps, key=lambda app: app.name.lower())
}
self.async_write_ha_state()
@callback
def async_device_disconnected(self):
"""Handle when connection was lost to device."""
self.atv.push_updater.stop()
self.atv.push_updater.listener = None
self.atv.power.listener = None
self._attr_supported_features = SUPPORT_APPLE_TV
@property

View File

@@ -43,7 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await async_setup_august(hass, entry, august_gateway)
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err:
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
@@ -141,15 +143,34 @@ class AugustData(AugustSubscriberMixin):
self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub)
if self._locks_by_id:
tasks = []
for lock_id in self._locks_by_id:
detail = self._device_detail_by_id[lock_id]
tasks.append(
self.async_status_async(
lock_id, bool(detail.bridge and detail.bridge.hyper_bridge)
)
# Do not prevent setup as the sync can timeout
# but it is not a fatal error as the lock
# will recover automatically when it comes back online.
asyncio.create_task(self._async_initial_sync())
async def _async_initial_sync(self):
"""Attempt to request an initial sync."""
# We don't care if this fails because we only want to wake
# locks that are actually online anyways and they will be
# awake when they come back online
for result in await asyncio.gather(
*[
self.async_status_async(
device_id, bool(detail.bridge and detail.bridge.hyper_bridge)
)
for device_id, detail in self._device_detail_by_id.items()
if device_id in self._locks_by_id
],
return_exceptions=True,
):
if isinstance(result, Exception) and not isinstance(
result, (asyncio.TimeoutError, ClientResponseError, CannotConnect)
):
_LOGGER.warning(
"Unexpected exception during initial sync: %s",
result,
exc_info=result,
)
await asyncio.gather(*tasks)
@callback
def async_pubnub_message(self, device_id, date_time, message):
@@ -185,12 +206,28 @@ class AugustData(AugustSubscriberMixin):
await self._async_refresh_device_detail_by_ids(self._subscriptions.keys())
async def _async_refresh_device_detail_by_ids(self, device_ids_list):
await asyncio.gather(
*(
self._async_refresh_device_detail_by_id(device_id)
for device_id in device_ids_list
)
)
"""Refresh each device in sequence.
This used to be a gather but it was less reliable with august's
recent api changes.
The august api has been timing out for some devices so
we want the ones that it isn't timing out for to keep working.
"""
for device_id in device_ids_list:
try:
await self._async_refresh_device_detail_by_id(device_id)
except asyncio.TimeoutError:
_LOGGER.warning(
"Timed out calling august api during refresh of device: %s",
device_id,
)
except (ClientResponseError, CannotConnect) as err:
_LOGGER.warning(
"Error from august api during refresh of device: %s",
device_id,
exc_info=err,
)
async def _async_refresh_device_detail_by_id(self, device_id):
if device_id in self._locks_by_id:

View File

@@ -4,7 +4,7 @@ from datetime import timedelta
from homeassistant.const import Platform
DEFAULT_TIMEOUT = 10
DEFAULT_TIMEOUT = 15
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method"

View File

@@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.19"],
"requirements": ["yalexs==1.1.20"],
"codeowners": ["@bdraco"],
"dhcp": [
{

View File

@@ -8,7 +8,11 @@
{
"hostname": "blink*",
"macaddress": "B85F98*"
}
},
{
"hostname": "blink*",
"macaddress": "00037F*"
}
],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@@ -3,7 +3,7 @@
"name": "Bosch SHC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"requirements": ["boschshcpy==0.2.28"],
"requirements": ["boschshcpy==0.2.29"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }],
"iot_class": "local_push",
"codeowners": ["@tschamm"],

View File

@@ -113,8 +113,9 @@ class ButtonEntity(RestoreEntity):
self.async_write_ha_state()
await self.async_press()
async def async_added_to_hass(self) -> None:
async def async_internal_added_to_hass(self) -> None:
"""Call when the button is added to hass."""
await super().async_internal_added_to_hass()
state = await self.async_get_last_state()
if state is not None and state.state is not None:
self.__last_pressed = dt_util.parse_datetime(state.state)

View File

@@ -232,7 +232,11 @@ class WebDavCalendarData:
new_events.append(new_event)
elif _start_of_tomorrow <= start_dt:
break
vevents = [event.instance.vevent for event in results + new_events]
vevents = [
event.instance.vevent
for event in results + new_events
if hasattr(event.instance, "vevent")
]
# dtstart can be a date or datetime depending if the event lasts a
# whole day. Convert everything to datetime to be able to sort it

View File

@@ -1,12 +1,21 @@
"""Component to embed Google Cast."""
import logging
from __future__ import annotations
import logging
from typing import Protocol
from pychromecast import Chromecast
import voluptuous as vol
from homeassistant.components.media_player import BrowseMedia
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from homeassistant.helpers.typing import ConfigType
from . import home_assistant_cast
@@ -49,9 +58,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry."""
await home_assistant_cast.async_setup_ha_cast(hass, entry)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
hass.data[DOMAIN] = {}
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True
class CastProtocol(Protocol):
"""Define the format of cast platforms."""
async def async_get_media_browser_root_object(
self, hass: HomeAssistant, cast_type: str
) -> list[BrowseMedia]:
"""Create a list of root objects for media browsing."""
async def async_browse_media(
self,
hass: HomeAssistant,
media_content_type: str,
media_content_id: str,
cast_type: str,
) -> BrowseMedia | None:
"""Browse media.
Return a BrowseMedia object or None if the media does not belong to this platform.
"""
async def async_play_media(
self,
hass: HomeAssistant,
cast_entity_id: str,
chromecast: Chromecast,
media_type: str,
media_id: str,
) -> bool:
"""Play media.
Return True if the media is played by the platform, False if not.
"""
async def _register_cast_platform(
hass: HomeAssistant, integration_domain: str, platform: CastProtocol
):
"""Register a cast platform."""
if (
not hasattr(platform, "async_get_media_browser_root_object")
or not hasattr(platform, "async_browse_media")
or not hasattr(platform, "async_play_media")
):
raise HomeAssistantError(f"Invalid cast platform {platform}")
hass.data[DOMAIN][integration_domain] = platform
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove Home Assistant Cast user."""
await home_assistant_cast.async_remove_user(hass, entry)

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import datetime, timedelta
import functools as ft
import json
import logging
from urllib.parse import quote
@@ -12,7 +11,6 @@ from urllib.parse import quote
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.plex import PlexController
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
@@ -21,7 +19,7 @@ from pychromecast.socket_client import (
)
import voluptuous as vol
from homeassistant.components import media_source, plex, zeroconf
from homeassistant.components import media_source, zeroconf
from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import (
BrowseError,
@@ -30,7 +28,6 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_player.const import (
ATTR_MEDIA_EXTRA,
MEDIA_CLASS_APP,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
@@ -48,8 +45,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
@@ -461,55 +456,28 @@ class CastDevice(MediaPlayerEntity):
media_controller = self._media_controller()
media_controller.seek(position)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
kwargs = {}
async def _async_root_payload(self, content_filter):
"""Generate root node."""
children = []
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_AUDIO:
kwargs["content_filter"] = lambda item: item.media_content_type.startswith(
"audio/"
)
if media_content_id is not None:
if plex.is_plex_media_id(media_content_id):
return await plex.async_browse_media(
self.hass,
media_content_type,
media_content_id,
platform=CAST_DOMAIN,
)
return await media_source.async_browse_media(
self.hass, media_content_id, **kwargs
)
if media_content_type == "plex":
return await plex.async_browse_media(
self.hass, None, None, platform=CAST_DOMAIN
)
if "plex" in self.hass.config.components:
children.append(
BrowseMedia(
title="Plex",
media_class=MEDIA_CLASS_APP,
media_content_id="",
media_content_type="plex",
thumbnail="https://brands.home-assistant.io/_/plex/logo.png",
can_play=False,
can_expand=True,
# Add media browsers
for platform in self.hass.data[CAST_DOMAIN].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
)
)
# Add media sources
try:
result = await media_source.async_browse_media(
self.hass, media_content_id, **kwargs
self.hass, None, content_filter=content_filter
)
children.append(result)
except BrowseError:
if not children:
raise
# If there's only one media source, resolve it
if len(children) == 1:
return await self.async_browse_media(
children[0].media_content_type,
@@ -526,6 +494,38 @@ class CastDevice(MediaPlayerEntity):
children=children,
)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
content_filter = None
if self._chromecast.cast_type in (
pychromecast.const.CAST_TYPE_AUDIO,
pychromecast.const.CAST_TYPE_GROUP,
):
def audio_content_filter(item):
"""Filter non audio content."""
return item.media_content_type.startswith("audio/")
content_filter = audio_content_filter
if media_content_id is None:
return await self._async_root_payload(content_filter)
for platform in self.hass.data[CAST_DOMAIN].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
media_content_id,
self._chromecast.cast_type,
)
if browse_media:
return browse_media
return await media_source.async_browse_media(
self.hass, media_content_id, content_filter=content_filter
)
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
# Handle media_source
@@ -547,16 +547,10 @@ class CastDevice(MediaPlayerEntity):
hass_url = get_url(self.hass, prefer_external=True)
media_id = f"{hass_url}{media_id}"
await self.hass.async_add_executor_job(
ft.partial(self.play_media, media_type, media_id, **kwargs)
)
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
metadata = extra.get("metadata")
# We do not want this to be forwarded to a group
# Handle media supported by a known cast app
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
@@ -571,7 +565,9 @@ class CastDevice(MediaPlayerEntity):
if "app_id" in app_data:
app_id = app_data.pop("app_id")
_LOGGER.info("Starting Cast app by ID %s", app_id)
self._chromecast.start_app(app_id)
await self.hass.async_add_executor_job(
self._chromecast.start_app, app_id
)
if app_data:
_LOGGER.warning(
"Extra keys %s were ignored. Please use app_name to cast media",
@@ -581,21 +577,26 @@ class CastDevice(MediaPlayerEntity):
app_name = app_data.pop("app_name")
try:
quick_play(self._chromecast, app_name, app_data)
await self.hass.async_add_executor_job(
quick_play, self._chromecast, app_name, app_data
)
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
# Handle plex
elif media_id and media_id.startswith(PLEX_URI_SCHEME):
media_id = media_id[len(PLEX_URI_SCHEME) :]
media = lookup_plex_media(self.hass, media_type, media_id)
if media is None:
return
# Try the cast platforms
for platform in self.hass.data[CAST_DOMAIN].values():
result = await platform.async_play_media(
self.hass, self.entity_id, self._chromecast, media_type, media_id
)
if result:
return
controller = PlexController()
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "default_media_receiver", app_data)
# Default to play with the default media receiver
app_data = {"media_id": media_id, "media_type": media_type, **extra}
await self.hass.async_add_executor_job(
quick_play, self._chromecast, "default_media_receiver", app_data
)
def _media_status(self):
"""

View File

@@ -1,5 +1,4 @@
"""Provide configuration end points for Automations."""
from collections import OrderedDict
import uuid
from homeassistant.components.automation.config import (
@@ -52,7 +51,18 @@ class EditAutomationConfigView(EditIdBasedConfigView):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
index = None
updated_value = {CONF_ID: config_key}
# Iterate through some keys that we want to have ordered in the output
for key in ("alias", "description", "trigger", "condition", "action"):
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(new_value)
updated = False
for index, cur_value in enumerate(data):
# When people copy paste their automations to the config file,
# they sometimes forget to add IDs. Fix it here.
@@ -60,23 +70,8 @@ class EditAutomationConfigView(EditIdBasedConfigView):
cur_value[CONF_ID] = uuid.uuid4().hex
elif cur_value[CONF_ID] == config_key:
break
else:
cur_value = OrderedDict()
cur_value[CONF_ID] = config_key
index = len(data)
data.append(cur_value)
data[index] = updated_value
updated = True
# Iterate through some keys that we want to have ordered in the output
updated_value = OrderedDict()
for key in ("id", "alias", "description", "trigger", "condition", "action"):
if key in cur_value:
updated_value[key] = cur_value[key]
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(cur_value)
updated_value.update(new_value)
data[index] = updated_value
if not updated:
data.append(updated_value)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from http import HTTPStatus
from aiohttp import web
import aiohttp.web_exceptions
import voluptuous as vol
@@ -11,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import Unauthorized
from homeassistant.exceptions import DependencyError, Unauthorized
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
@@ -127,7 +128,13 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
# pylint: disable=no-value-for-parameter
return await super().post(request)
try:
return await super().post(request)
except DependencyError as exc:
return web.Response(
text=f"Failed dependencies {', '.join(exc.failed_dependencies)}",
status=HTTPStatus.BAD_REQUEST,
)
def _prepare_result_json(self, result):
"""Convert result to JSON."""

View File

@@ -62,6 +62,9 @@ async def websocket_update_device(hass, connection, msg):
msg.pop("type")
msg_id = msg.pop("id")
if msg.get("disabled_by") is not None:
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
entry = registry.async_update_device(**msg)
connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry)))

View File

@@ -47,8 +47,8 @@ class EditSceneConfigView(EditIdBasedConfigView):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
# Iterate through some keys that we want to have ordered in the output
updated_value = {CONF_ID: config_key}
# Iterate through some keys that we want to have ordered in the output
for key in ("name", "entities"):
if key in new_value:
updated_value[key] = new_value[key]

View File

@@ -46,7 +46,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Enable asyncio debugging and start the debugger."""
get_running_loop().set_debug(True)
debugpy.listen((conf[CONF_HOST], conf[CONF_PORT]))
await hass.async_add_executor_job(
debugpy.listen, (conf[CONF_HOST], conf[CONF_PORT])
)
if conf[CONF_WAIT]:
_LOGGER.warning(

View File

@@ -7,7 +7,7 @@ from typing import Any
from pydeconz.group import DeconzScene as PydeconzScene
from homeassistant.components.scene import Scene
from homeassistant.components.scene import DOMAIN, Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -23,6 +23,7 @@ async def async_setup_entry(
) -> None:
"""Set up scenes for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
def async_add_scene(
@@ -30,7 +31,11 @@ async def async_setup_entry(
| ValuesView[PydeconzScene] = gateway.api.scenes.values(),
) -> None:
"""Add scene from deCONZ."""
entities = [DeconzScene(scene, gateway) for scene in scenes]
entities = [
DeconzScene(scene, gateway)
for scene in scenes
if scene.deconz_id not in gateway.entities[DOMAIN]
]
if entities:
async_add_entities(entities)
@@ -59,10 +64,12 @@ class DeconzScene(Scene):
async def async_added_to_hass(self) -> None:
"""Subscribe to sensors events."""
self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id
self.gateway.entities[DOMAIN].add(self._scene.deconz_id)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed."""
del self.gateway.deconz_ids[self.entity_id]
self.gateway.entities[DOMAIN].remove(self._scene.deconz_id)
self._scene = None
async def async_activate(self, **kwargs: Any) -> None:

View File

@@ -7,7 +7,6 @@
"cloud",
"counter",
"dhcp",
"diagnostics",
"energy",
"frontend",
"history",

View File

@@ -179,6 +179,7 @@ class WatcherBase:
lowercase_hostname,
)
matched_domains = set()
for entry in self._integration_matchers:
if MAC_ADDRESS in entry and not fnmatch.fnmatch(
uppercase_mac, entry[MAC_ADDRESS]
@@ -191,6 +192,11 @@ class WatcherBase:
continue
_LOGGER.debug("Matched %s against %s", data, entry)
if entry["domain"] in matched_domains:
# Only match once per domain
continue
matched_domains.add(entry["domain"])
discovery_flow.async_create_flow(
self.hass,
entry["domain"],

View File

@@ -170,7 +170,7 @@ async def _async_get_json_file_response(
return web.Response(
body=json_data,
content_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
headers={"Content-Disposition": f'attachment; filename="{filename}.json.txt"'},
)

View File

@@ -2,19 +2,24 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any
from typing import Any, TypeVar, cast
from homeassistant.core import callback
from .const import REDACTED
T = TypeVar("T")
@callback
def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any]:
def async_redact_data(data: T, to_redact: Iterable[Any]) -> T:
"""Redact sensitive data in a dict."""
if not isinstance(data, (Mapping, list)):
return data
if isinstance(data, list):
return cast(T, [async_redact_data(val, to_redact) for val in data])
redacted = {**data}
for key, value in redacted.items():
@@ -25,4 +30,4 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any]
elif isinstance(value, list):
redacted[key] = [async_redact_data(item, to_redact) for item in value]
return redacted
return cast(T, redacted)

View File

@@ -33,6 +33,13 @@ DATA_SCHEMA = vol.Schema(
vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
}
)
DATA_SCHEMA_ADV = vol.Schema(
{
vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string,
vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string,
}
)
async def async_validate_hostname(
@@ -94,8 +101,8 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
hostname = user_input[CONF_HOSTNAME]
name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname
resolver = DEFAULT_RESOLVER
resolver_ipv6 = DEFAULT_RESOLVER_IPV6
resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER)
resolver_ipv6 = user_input.get(CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6)
validate = await async_validate_hostname(hostname, resolver, resolver_ipv6)
@@ -110,13 +117,21 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={
CONF_HOSTNAME: hostname,
CONF_NAME: name,
CONF_RESOLVER: resolver,
CONF_RESOLVER_IPV6: resolver_ipv6,
CONF_IPV4: validate[CONF_IPV4],
CONF_IPV6: validate[CONF_IPV6],
},
options={
CONF_RESOLVER: resolver,
CONF_RESOLVER_IPV6: resolver_ipv6,
},
)
if self.show_advanced_options is True:
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA_ADV,
errors=errors,
)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,

View File

@@ -79,10 +79,8 @@ async def async_setup_entry(
hostname = entry.data[CONF_HOSTNAME]
name = entry.data[CONF_NAME]
resolver_ipv4 = entry.options.get(CONF_RESOLVER, entry.data[CONF_RESOLVER])
resolver_ipv6 = entry.options.get(
CONF_RESOLVER_IPV6, entry.data[CONF_RESOLVER_IPV6]
)
resolver_ipv4 = entry.options[CONF_RESOLVER]
resolver_ipv6 = entry.options[CONF_RESOLVER_IPV6]
entities = []
if entry.data[CONF_IPV4]:
entities.append(WanIpSensor(name, hostname, resolver_ipv4, False))

View File

@@ -3,7 +3,9 @@
"step": {
"user": {
"data": {
"hostname": "The hostname for which to perform the DNS query"
"hostname": "The hostname for which to perform the DNS query",
"resolver": "Resolver for IPV4 lookup",
"resolver_ipv6": "Resolver for IPV6 lookup"
}
}
},

View File

@@ -6,7 +6,9 @@
"step": {
"user": {
"data": {
"hostname": "The hostname for which to perform the DNS query"
"hostname": "The hostname for which to perform the DNS query",
"resolver": "Resolver for IPV4 lookup",
"resolver_ipv6": "Resolver for IPV6 lookup"
}
}
}

View File

@@ -2,7 +2,7 @@
"domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods",
"requirements": ["pydoods==1.0.2", "pillow==9.0.0"],
"requirements": ["pydoods==1.0.2", "pillow==9.0.1"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -1,7 +1,9 @@
"""The Flick Electric integration."""
from datetime import datetime as dt
import logging
import jwt
from pyflick import FlickAPI
from pyflick.authentication import AbstractFlickAuth
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
@@ -18,7 +20,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN
from .const import CONF_TOKEN_EXPIRY, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_ID_TOKEN = "id_token"
@@ -69,6 +73,8 @@ class HassFlickAuth(AbstractFlickAuth):
return self._entry.data[CONF_ACCESS_TOKEN]
async def _update_token(self):
_LOGGER.debug("Fetching new access token")
token = await self.get_new_token(
username=self._entry.data[CONF_USERNAME],
password=self._entry.data[CONF_PASSWORD],
@@ -78,15 +84,19 @@ class HassFlickAuth(AbstractFlickAuth):
),
)
# Reduce expiry by an hour to avoid API being called after expiry
expiry = dt.now().timestamp() + int(token[CONF_TOKEN_EXPIRES_IN] - 3600)
_LOGGER.debug("New token: %s", token)
# Flick will send the same token, but expiry is relative - so grab it from the token
token_decoded = jwt.decode(
token[CONF_ID_TOKEN], options={"verify_signature": False}
)
self._hass.config_entries.async_update_entry(
self._entry,
data={
**self._entry.data,
CONF_ACCESS_TOKEN: token,
CONF_TOKEN_EXPIRY: expiry,
CONF_TOKEN_EXPIRY: token_decoded["exp"],
},
)

View File

@@ -2,7 +2,6 @@
DOMAIN = "flick_electric"
CONF_TOKEN_EXPIRES_IN = "expires_in"
CONF_TOKEN_EXPIRY = "expires"
ATTR_START_AT = "start_at"

View File

@@ -15,8 +15,6 @@ from homeassistant.util.dt import utcnow
from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN
_LOGGER = logging.getLogger(__name__)
_AUTH_URL = "https://api.flick.energy/identity/oauth/token"
_RESOURCE = "https://api.flick.energy/customer/mobile_provider/price"
SCAN_INTERVAL = timedelta(minutes=5)
@@ -71,6 +69,8 @@ class FlickPricingSensor(SensorEntity):
async with async_timeout.timeout(60):
self._price = await self._api.getPricing()
_LOGGER.debug("Pricing data: %s", self._price)
self._attributes[ATTR_START_AT] = self._price.start_at
self._attributes[ATTR_END_AT] = self._price.end_at
for component in self._price.components:

View File

@@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
update_interval=DEFAULT_UPDATE_INTERVAL,
update_method=partial(async_update, api_category),
)
data_init_tasks.append(coordinator.async_refresh())
data_init_tasks.append(coordinator.async_config_entry_first_refresh())
await asyncio.gather(*data_init_tasks)
hass.data.setdefault(DOMAIN, {})

View File

@@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import (
async_track_time_change,
@@ -88,6 +88,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Migrate entities when the mac address gets discovered."""
unique_id = entry.unique_id
if not unique_id:
return
entry_id = entry.entry_id
@callback
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
# Old format {entry_id}.....
# New format {unique_id}....
entity_unique_id = entity_entry.unique_id
if not entity_unique_id.startswith(entry_id):
return None
new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}"
_LOGGER.info(
"Migrating unique_id from [%s] to [%s]",
entity_unique_id,
new_unique_id,
)
return {"new_unique_id": new_unique_id}
await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flux LED/MagicLight from a config entry."""
host = entry.data[CONF_HOST]
@@ -135,6 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# is either missing or we have verified it matches
async_update_entry_from_discovery(hass, entry, discovery, device.model_num)
await _async_migrate_unique_ids(hass, entry)
coordinator = FluxLedUpdateCoordinator(hass, device, entry)
hass.data[DOMAIN][entry.entry_id] = coordinator
platforms = PLATFORMS_BY_TYPE[device.device_type]

View File

@@ -64,8 +64,8 @@ class FluxButton(FluxBaseEntity, ButtonEntity):
self.entity_description = description
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} {description.name}"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_{description.key}"
async def async_press(self) -> None:
"""Send out a command."""

View File

@@ -51,6 +51,7 @@ FLUX_LED_EXCEPTIONS: Final = (
STARTUP_SCAN_TIMEOUT: Final = 5
DISCOVER_SCAN_TIMEOUT: Final = 10
DIRECTED_DISCOVERY_TIMEOUT: Final = 15
CONF_MODEL: Final = "model"
CONF_MODEL_NUM: Final = "model_num"

View File

@@ -38,7 +38,7 @@ from .const import (
CONF_REMOTE_ACCESS_ENABLED,
CONF_REMOTE_ACCESS_HOST,
CONF_REMOTE_ACCESS_PORT,
DISCOVER_SCAN_TIMEOUT,
DIRECTED_DISCOVERY_TIMEOUT,
DOMAIN,
FLUX_LED_DISCOVERY,
)
@@ -194,7 +194,7 @@ async def async_discover_device(
"""Direct discovery at a single ip instead of broadcast."""
# If we are missing the unique_id we should be able to fetch it
# from the device by doing a directed discovery at the host only
for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host):
for device in await async_discover_devices(hass, DIRECTED_DISCOVERY_TIMEOUT, host):
if device[ATTR_IPADDR] == host:
return device
return None

View File

@@ -7,19 +7,28 @@ from typing import Any
from flux_led.aiodevice import AIOWifiLedBulb
from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_HW_VERSION,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_SW_VERSION,
CONF_NAME,
)
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED
from .const import CONF_MINOR_VERSION, CONF_MODEL, DOMAIN, SIGNAL_STATE_UPDATED
from .coordinator import FluxLedUpdateCoordinator
def _async_device_info(
unique_id: str, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry
device: AIOWifiLedBulb, entry: config_entries.ConfigEntry
) -> DeviceInfo:
version_num = device.version_num
if minor_version := entry.data.get(CONF_MINOR_VERSION):
@@ -27,14 +36,18 @@ def _async_device_info(
sw_version_str = f"{sw_version:0.2f}"
else:
sw_version_str = str(device.version_num)
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, unique_id)},
manufacturer="Zengge",
model=device.model,
name=entry.data[CONF_NAME],
sw_version=sw_version_str,
hw_version=entry.data.get(CONF_MODEL),
)
device_info: DeviceInfo = {
ATTR_IDENTIFIERS: {(DOMAIN, entry.entry_id)},
ATTR_MANUFACTURER: "Zengge",
ATTR_MODEL: device.model,
ATTR_NAME: entry.data[CONF_NAME],
ATTR_SW_VERSION: sw_version_str,
}
if hw_model := entry.data.get(CONF_MODEL):
device_info[ATTR_HW_VERSION] = hw_model
if entry.unique_id:
device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, entry.unique_id)}
return device_info
class FluxBaseEntity(Entity):
@@ -50,10 +63,7 @@ class FluxBaseEntity(Entity):
"""Initialize the light."""
self._device: AIOWifiLedBulb = device
self.entry = entry
if entry.unique_id:
self._attr_device_info = _async_device_info(
entry.unique_id, self._device, entry
)
self._attr_device_info = _async_device_info(self._device, entry)
class FluxEntity(CoordinatorEntity):
@@ -64,7 +74,7 @@ class FluxEntity(CoordinatorEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str | None,
) -> None:
@@ -74,13 +84,10 @@ class FluxEntity(CoordinatorEntity):
self._responding = True
self._attr_name = name
if key:
self._attr_unique_id = f"{unique_id}_{key}"
self._attr_unique_id = f"{base_unique_id}_{key}"
else:
self._attr_unique_id = unique_id
if unique_id:
self._attr_device_info = _async_device_info(
unique_id, self._device, coordinator.entry
)
self._attr_unique_id = base_unique_id
self._attr_device_info = _async_device_info(self._device, coordinator.entry)
async def _async_ensure_device_on(self) -> None:
"""Turn the device on if it needs to be turned on before a command."""

View File

@@ -177,7 +177,7 @@ async def async_setup_entry(
[
FluxLight(
coordinator,
entry.unique_id,
entry.unique_id or entry.entry_id,
entry.data[CONF_NAME],
list(custom_effect_colors),
options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED),
@@ -195,14 +195,14 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
custom_effect_colors: list[tuple[int, int, int]],
custom_effect_speed_pct: int,
custom_effect_transition: str,
) -> None:
"""Initialize the light."""
super().__init__(coordinator, unique_id, name, None)
super().__init__(coordinator, base_unique_id, name, None)
self._attr_min_mireds = color_temperature_kelvin_to_mired(self._device.max_temp)
self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp)
self._attr_supported_color_modes = _hass_color_modes(self._device)

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.28.17"],
"requirements": ["flux_led==0.28.21"],
"quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push",

View File

@@ -51,26 +51,28 @@ async def async_setup_entry(
| FluxMusicSegmentsNumber
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
if device.pixels_per_segment is not None:
entities.append(
FluxPixelsPerSegmentNumber(
coordinator,
unique_id,
base_unique_id,
f"{name} Pixels Per Segment",
"pixels_per_segment",
)
)
if device.segments is not None:
entities.append(
FluxSegmentsNumber(coordinator, unique_id, f"{name} Segments", "segments")
FluxSegmentsNumber(
coordinator, base_unique_id, f"{name} Segments", "segments"
)
)
if device.music_pixels_per_segment is not None:
entities.append(
FluxMusicPixelsPerSegmentNumber(
coordinator,
unique_id,
base_unique_id,
f"{name} Music Pixels Per Segment",
"music_pixels_per_segment",
)
@@ -78,12 +80,12 @@ async def async_setup_entry(
if device.music_segments is not None:
entities.append(
FluxMusicSegmentsNumber(
coordinator, unique_id, f"{name} Music Segments", "music_segments"
coordinator, base_unique_id, f"{name} Music Segments", "music_segments"
)
)
if device.effect_list and device.effect_list != [EFFECT_RANDOM]:
entities.append(
FluxSpeedNumber(coordinator, unique_id, f"{name} Effect Speed", None)
FluxSpeedNumber(coordinator, base_unique_id, f"{name} Effect Speed", None)
)
if entities:
@@ -131,12 +133,12 @@ class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str | None,
) -> None:
"""Initialize the flux number."""
super().__init__(coordinator, unique_id, name, key)
super().__init__(coordinator, base_unique_id, name, key)
self._debouncer: Debouncer | None = None
self._pending_value: int | None = None

View File

@@ -54,28 +54,28 @@ async def async_setup_entry(
| FluxWhiteChannelSelect
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
if device.device_type == DeviceType.Switch:
entities.append(FluxPowerStateSelect(coordinator.device, entry))
if device.operating_modes:
entities.append(
FluxOperatingModesSelect(
coordinator, unique_id, f"{name} Operating Mode", "operating_mode"
coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode"
)
)
if device.wirings:
entities.append(
FluxWiringsSelect(coordinator, unique_id, f"{name} Wiring", "wiring")
FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring")
)
if device.ic_types:
entities.append(
FluxICTypeSelect(coordinator, unique_id, f"{name} IC Type", "ic_type")
FluxICTypeSelect(coordinator, base_unique_id, f"{name} IC Type", "ic_type")
)
if device.remote_config:
entities.append(
FluxRemoteConfigSelect(
coordinator, unique_id, f"{name} Remote Config", "remote_config"
coordinator, base_unique_id, f"{name} Remote Config", "remote_config"
)
)
if FLUX_COLOR_MODE_RGBW in device.color_modes:
@@ -111,8 +111,8 @@ class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity):
"""Initialize the power state select."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} Power Restored"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_power_restored"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_power_restored"
self._async_set_current_option_from_device()
@callback
@@ -201,12 +201,12 @@ class FluxRemoteConfigSelect(FluxConfigSelect):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str,
) -> None:
"""Initialize the remote config type select."""
super().__init__(coordinator, unique_id, name, key)
super().__init__(coordinator, base_unique_id, name, key)
assert self._device.remote_config is not None
self._name_to_state = {
_human_readable_option(option.name): option for option in RemoteConfig
@@ -238,8 +238,8 @@ class FluxWhiteChannelSelect(FluxConfigAtStartSelect):
"""Initialize the white channel select."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} White Channel"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_white_channel"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_white_channel"
@property
def current_option(self) -> str | None:

View File

@@ -25,7 +25,7 @@ async def async_setup_entry(
[
FluxPairedRemotes(
coordinator,
entry.unique_id,
entry.unique_id or entry.entry_id,
f"{entry.data[CONF_NAME]} Paired Remotes",
"paired_remotes",
)

View File

@@ -34,18 +34,18 @@ async def async_setup_entry(
"""Set up the Flux lights."""
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = []
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
name = entry.data[CONF_NAME]
if coordinator.device.device_type == DeviceType.Switch:
entities.append(FluxSwitch(coordinator, unique_id, name, None))
entities.append(FluxSwitch(coordinator, base_unique_id, name, None))
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
entities.append(FluxRemoteAccessSwitch(coordinator.device, entry))
if coordinator.device.microphone:
entities.append(
FluxMusicSwitch(coordinator, unique_id, f"{name} Music", "music")
FluxMusicSwitch(coordinator, base_unique_id, f"{name} Music", "music")
)
if entities:
@@ -74,8 +74,8 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity):
"""Initialize the light."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} Remote Access"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_remote_access"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_remote_access"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the remote access on."""

View File

@@ -1,11 +1,7 @@
"""Support for AVM Fritz!Box functions."""
import logging
from fritzconnection.core.exceptions import (
FritzConnectionException,
FritzResourceError,
FritzSecurityError,
)
from fritzconnection.core.exceptions import FritzSecurityError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
@@ -13,7 +9,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .common import AvmWrapper, FritzData
from .const import DATA_FRITZ, DOMAIN, PLATFORMS
from .const import DATA_FRITZ, DOMAIN, FRITZ_EXCEPTIONS, PLATFORMS
from .services import async_setup_services, async_unload_services
_LOGGER = logging.getLogger(__name__)
@@ -34,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await avm_wrapper.async_setup(entry.options)
except FritzSecurityError as ex:
raise ConfigEntryAuthFailed from ex
except (FritzConnectionException, FritzResourceError) as ex:
except FRITZ_EXCEPTIONS as ex:
raise ConfigEntryNotReady from ex
hass.data.setdefault(DOMAIN, {})

View File

@@ -12,10 +12,7 @@ from typing import Any, TypedDict, cast
from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzConnectionException,
FritzInternalError,
FritzLookUpError,
FritzSecurityError,
FritzServiceError,
)
@@ -46,6 +43,7 @@ from .const import (
DEFAULT_PORT,
DEFAULT_USERNAME,
DOMAIN,
FRITZ_EXCEPTIONS,
SERVICE_CLEANUP,
SERVICE_REBOOT,
SERVICE_RECONNECT,
@@ -107,7 +105,7 @@ class Device:
ip_address: str
name: str
ssid: str | None
wan_access: bool = True
wan_access: bool | None = None
class Interface(TypedDict):
@@ -188,9 +186,26 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
_LOGGER.error("Unable to establish a connection with %s", self.host)
return
_LOGGER.debug(
"detected services on %s %s",
self.host,
list(self.connection.services.keys()),
)
self.fritz_hosts = FritzHosts(fc=self.connection)
self.fritz_status = FritzStatus(fc=self.connection)
info = self.connection.call_action("DeviceInfo:1", "GetInfo")
_LOGGER.debug(
"gathered device info of %s %s",
self.host,
{
**info,
"NewDeviceLog": "***omitted***",
"NewSerialNumber": "***omitted***",
},
)
if not self._unique_id:
self._unique_id = info["NewSerialNumber"]
@@ -205,8 +220,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
"""Update FritzboxTools data."""
try:
await self.async_scan_devices()
except (FritzSecurityError, FritzConnectionException) as ex:
raise update_coordinator.UpdateFailed from ex
except FRITZ_EXCEPTIONS as ex:
raise update_coordinator.UpdateFailed(ex) from ex
@property
def unique_id(self) -> str:
@@ -277,6 +292,22 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
)
return bool(version), version
def _get_wan_access(self, ip_address: str) -> bool | None:
"""Get WAN access rule for given IP address."""
try:
return not self.connection.call_action(
"X_AVM-DE_HostFilter:1",
"GetWANAccessByIP",
NewIPv4Address=ip_address,
).get("NewDisallow")
except FRITZ_EXCEPTIONS as ex:
_LOGGER.debug(
"could not get WAN access rule for client device with IP '%s', error: %s",
ip_address,
ex,
)
return None
async def async_scan_devices(self, now: datetime | None = None) -> None:
"""Wrap up FritzboxTools class scan."""
await self.hass.async_add_executor_job(self.scan_devices, now)
@@ -315,7 +346,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
connection_type="",
ip_address=host["ip"],
ssid=None,
wan_access=False,
wan_access=None,
)
mesh_intf = {}
@@ -343,32 +374,33 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
for interf in node["node_interfaces"]:
dev_mac = interf["mac_address"]
if dev_mac not in hosts:
continue
dev_info: Device = hosts[dev_mac]
if dev_info.ip_address:
dev_info.wan_access = self._get_wan_access(dev_info.ip_address)
for link in interf["node_links"]:
intf = mesh_intf.get(link["node_interface_1_uid"])
if (
intf is not None
and link["state"] == "CONNECTED"
and dev_mac in hosts
):
dev_info: Device = hosts[dev_mac]
if intf["op_mode"] != "AP_GUEST":
dev_info.wan_access = not self.connection.call_action(
"X_AVM-DE_HostFilter:1",
"GetWANAccessByIP",
NewIPv4Address=dev_info.ip_address,
).get("NewDisallow")
if intf is not None:
if intf["op_mode"] == "AP_GUEST":
dev_info.wan_access = None
dev_info.connected_to = intf["device"]
dev_info.connection_type = intf["type"]
dev_info.ssid = intf.get("ssid")
_LOGGER.debug("Client dev_info: %s", dev_info)
if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, consider_home)
else:
device = FritzDevice(dev_mac, dev_info.name)
device.update(dev_info, consider_home)
self._devices[dev_mac] = device
new_device = True
if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, consider_home)
else:
device = FritzDevice(dev_mac, dev_info.name)
device.update(dev_info, consider_home)
self._devices[dev_mac] = device
new_device = True
dispatcher_send(self.hass, self.signal_device_update)
if new_device:
@@ -521,13 +553,7 @@ class AvmWrapper(FritzBoxTools):
"Authorization Error: Please check the provided credentials and verify that you can log into the web interface",
exc_info=True,
)
except (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzServiceError,
FritzLookUpError,
):
except FRITZ_EXCEPTIONS:
_LOGGER.error(
"Service/Action Error: cannot execute service %s with action %s",
service_name,
@@ -541,11 +567,11 @@ class AvmWrapper(FritzBoxTools):
)
return {}
async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""
return await self.hass.async_add_executor_job(
partial(self.get_wan_dsl_interface_config)
partial(self.get_wan_link_properties)
)
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
@@ -645,10 +671,12 @@ class AvmWrapper(FritzBoxTools):
return self._service_call_action("WLANConfiguration", str(index), "GetInfo")
def get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
def get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service."""
return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo")
return self._service_call_action(
"WANCommonInterfaceConfig", "1", "GetCommonLinkProperties"
)
def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]:
"""Call SetEnable action from WLANConfiguration service."""
@@ -760,7 +788,7 @@ class FritzDevice:
self._mac = mac
self._name = name
self._ssid: str | None = None
self._wan_access = False
self._wan_access: bool | None = False
def update(self, dev_info: Device, consider_home: float) -> None:
"""Update device info."""
@@ -828,7 +856,7 @@ class FritzDevice:
return self._ssid
@property
def wan_access(self) -> bool:
def wan_access(self) -> bool | None:
"""Return device wan access."""
return self._wan_access

View File

@@ -2,6 +2,14 @@
from typing import Literal
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzLookUpError,
FritzServiceError,
)
from homeassistant.backports.enum import StrEnum
from homeassistant.const import Platform
@@ -47,3 +55,11 @@ SWITCH_TYPE_PORTFORWARD = "PortForward"
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
UPTIME_DEVIATION = 5
FRITZ_EXCEPTIONS = (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzServiceError,
FritzLookUpError,
)

View File

@@ -0,0 +1,48 @@
"""Diagnostics support for AVM FRITZ!Box."""
from __future__ import annotations
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .common import AvmWrapper
from .const import DOMAIN
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict:
"""Return diagnostics for a config entry."""
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
diag_data = {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"model": avm_wrapper.model,
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,
"is_router": avm_wrapper.device_is_router,
"mesh_role": avm_wrapper.mesh_role,
"last_update success": avm_wrapper.last_update_success,
"last_exception": avm_wrapper.last_exception,
"discovered_services": list(avm_wrapper.connection.services),
"client_devices": [
{
"connected_to": device.connected_to,
"connection_type": device.connection_type,
"hostname": device.hostname,
"is_connected": device.is_connected,
"last_activity": device.last_activity,
"wan_access": device.wan_access,
}
for _, device in avm_wrapper.devices.items()
],
"wan_link_properties": await avm_wrapper.async_get_wan_link_properties(),
},
}
return diag_data

View File

@@ -277,10 +277,14 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
dsl: bool = False
dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config()
if dslinterface:
dsl = dslinterface["NewEnable"]
link_properties = await avm_wrapper.async_get_wan_link_properties()
dsl: bool = link_properties.get("NewWANAccessType") == "DSL"
_LOGGER.debug(
"WANAccessType of FritzBox %s is '%s'",
avm_wrapper.host,
link_properties.get("NewWANAccessType"),
)
entities = [
FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@@ -477,10 +477,17 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
self._attr_entity_category = EntityCategory.CONFIG
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Switch status."""
return self._avm_wrapper.devices[self._mac].wan_access
@property
def available(self) -> bool:
"""Return availability of the switch."""
if self._avm_wrapper.devices[self._mac].wan_access is None:
return False
return super().available
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""

View File

@@ -3,13 +3,14 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20220127.0"
"home-assistant-frontend==20220203.0"
],
"dependencies": [
"api",
"auth",
"config",
"device_automation",
"diagnostics",
"http",
"lovelace",
"onboarding",

View File

@@ -10,7 +10,6 @@ from aiogithubapi import (
GitHubException,
GitHubLoginDeviceModel,
GitHubLoginOauthModel,
GitHubRepositoryModel,
)
from aiogithubapi.const import OAUTH_USER_LOGIN
import voluptuous as vol
@@ -34,11 +33,12 @@ from .const import (
)
async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[str]:
"""Return a list of repositories that the user has starred."""
async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]:
"""Return a list of repositories that the user owns or has starred."""
client = GitHubAPI(token=access_token, session=async_get_clientsession(hass))
repositories = set()
async def _get_starred() -> list[GitHubRepositoryModel] | None:
async def _get_starred_repositories() -> None:
response = await client.user.starred(**{"params": {"per_page": 100}})
if not response.is_last_page:
results = await asyncio.gather(
@@ -54,16 +54,44 @@ async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[s
for result in results:
response.data.extend(result.data)
return response.data
repositories.update(response.data)
async def _get_personal_repositories() -> None:
response = await client.user.repos(**{"params": {"per_page": 100}})
if not response.is_last_page:
results = await asyncio.gather(
*(
client.user.repos(
**{"params": {"per_page": 100, "page": page_number}},
)
for page_number in range(
response.next_page_number, response.last_page_number + 1
)
)
)
for result in results:
response.data.extend(result.data)
repositories.update(response.data)
try:
result = await _get_starred()
await asyncio.gather(
*(
_get_starred_repositories(),
_get_personal_repositories(),
)
)
except GitHubException:
return DEFAULT_REPOSITORIES
if not result or len(result) == 0:
if len(repositories) == 0:
return DEFAULT_REPOSITORIES
return sorted((repo.full_name for repo in result), key=str.casefold)
return sorted(
(repo.full_name for repo in repositories),
key=str.casefold,
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -153,9 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self._login is not None
if not user_input:
repositories = await starred_repositories(
self.hass, self._login.access_token
)
repositories = await get_repositories(self.hass, self._login.access_token)
return self.async_show_form(
step_id="repositories",
data_schema=vol.Schema(
@@ -205,7 +231,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
configured_repositories: list[str] = self.config_entry.options[
CONF_REPOSITORIES
]
repositories = await starred_repositories(
repositories = await get_repositories(
self.hass, self.config_entry.data[CONF_ACCESS_TOKEN]
)

View File

@@ -3,7 +3,7 @@
"name": "GitHub",
"documentation": "https://www.home-assistant.io/integrations/github",
"requirements": [
"aiogithubapi==22.1.0"
"aiogithubapi==22.2.0"
],
"codeowners": [
"@timmo001",

View File

@@ -68,6 +68,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]}
)
self._async_abort_entries_match(
{CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]}
)
else:
self._abort_if_unique_id_configured()
@@ -103,6 +106,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_IP_ADDRESS: discovery_info.ip,
CONF_PORT: DEFAULT_PORT,
}
await self._async_set_unique_id(
async_get_pin_from_uid(discovery_info.macaddress.replace(":", "").upper())
)
return await self._async_handle_discovery()
async def async_step_zeroconf(

View File

@@ -0,0 +1,44 @@
"""Diagnostics support for HomeKit."""
from __future__ import annotations
from typing import Any
from pyhap.accessory_driver import AccessoryDriver
from pyhap.state import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import HomeKit
from .const import DOMAIN, HOMEKIT
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
driver: AccessoryDriver = homekit.driver
data: dict[str, Any] = {
"status": homekit.status,
"config-entry": {
"title": entry.title,
"version": entry.version,
"data": dict(entry.data),
"options": dict(entry.options),
},
}
if not driver:
return data
data.update(driver.get_accessories())
state: State = driver.state
data.update(
{
"client_properties": {
str(client): props for client, props in state.client_properties.items()
},
"config_version": state.config_version,
"pairing_id": state.mac,
}
)
return data

View File

@@ -1,4 +1,6 @@
"""Class to hold all light accessories."""
from __future__ import annotations
import logging
import math
@@ -12,12 +14,13 @@ from homeassistant.components.light import (
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN,
brightness_supported,
color_supported,
@@ -32,9 +35,9 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin,
color_temperature_to_hs,
color_temperature_to_rgbww,
)
from .accessories import TYPES, HomeAccessory
@@ -51,12 +54,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
RGB_COLOR = "rgb_color"
CHANGE_COALESCE_TIME_WINDOW = 0.01
DEFAULT_MIN_MIREDS = 153
DEFAULT_MAX_MIREDS = 500
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW}
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE}
@TYPES.register("Light")
@@ -79,8 +83,12 @@ class Light(HomeAccessory):
self.color_modes = color_modes = (
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
)
self._previous_color_mode = attributes.get(ATTR_COLOR_MODE)
self.color_supported = color_supported(color_modes)
self.color_temp_supported = color_temp_supported(color_modes)
self.rgbw_supported = COLOR_MODE_RGBW in color_modes
self.rgbww_supported = COLOR_MODE_RGBWW in color_modes
self.white_supported = COLOR_MODE_WHITE in color_modes
self.brightness_supported = brightness_supported(color_modes)
if self.brightness_supported:
@@ -89,7 +97,9 @@ class Light(HomeAccessory):
if self.color_supported:
self.chars.extend([CHAR_HUE, CHAR_SATURATION])
if self.color_temp_supported:
if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection(
self.color_modes
):
self.chars.append(CHAR_COLOR_TEMPERATURE)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
@@ -101,13 +111,22 @@ class Light(HomeAccessory):
# to set to the correct initial value.
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if self.color_temp_supported:
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153))
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500))
if CHAR_COLOR_TEMPERATURE in self.chars:
self.min_mireds = math.floor(
attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS)
)
self.max_mireds = math.ceil(
attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS)
)
if not self.color_temp_supported and not self.rgbww_supported:
self.max_mireds = self.min_mireds
self.char_color_temp = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE,
value=min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
value=self.min_mireds,
properties={
PROP_MIN_VALUE: self.min_mireds,
PROP_MAX_VALUE: self.max_mireds,
},
)
if self.color_supported:
@@ -165,33 +184,32 @@ class Light(HomeAccessory):
)
return
# Handle white channels
if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
temp = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {temp}")
bright_val = round(
((brightness_pct or self.char_brightness.value) * 255) / 100
)
if self.color_temp_supported:
params[ATTR_COLOR_TEMP] = temp
elif self.rgbww_supported:
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
temp, bright_val, self.min_mireds, self.max_mireds
)
elif self.rgbw_supported:
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
elif self.white_supported:
params[ATTR_WHITE] = bright_val
elif (
CHAR_HUE in char_values
or CHAR_SATURATION in char_values
# If we are adjusting brightness we need to send the full RGBW/RGBWW values
# since HomeKit does not support RGBW/RGBWW
or brightness_pct
and COLOR_MODES_WITH_WHITES.intersection(self.color_modes)
):
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
hue_sat = (
char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value),
)
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
events.append(f"set color at {hue_sat}")
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values
if COLOR_MODE_RGBWW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0)
elif COLOR_MODE_RGBW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0)
else:
params[ATTR_HS_COLOR] = hue_sat
params[ATTR_HS_COLOR] = hue_sat
if (
brightness_pct
@@ -200,6 +218,9 @@ class Light(HomeAccessory):
):
params[ATTR_BRIGHTNESS_PCT] = brightness_pct
_LOGGER.debug(
"Calling light service with params: %s -> %s", char_values, params
)
self.async_call_service(DOMAIN, service, params, ", ".join(events))
@callback
@@ -210,52 +231,59 @@ class Light(HomeAccessory):
attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE)
self.char_on.set_value(int(state == STATE_ON))
color_mode_changed = self._previous_color_mode != color_mode
self._previous_color_mode = color_mode
# Handle Brightness
if self.brightness_supported:
if (
color_mode
and COLOR_MODES_WITH_WHITES.intersection({color_mode})
and (rgb_color := attributes.get(ATTR_RGB_COLOR))
):
# HomeKit doesn't support RGBW/RGBWW so we need to
# give it the color brightness only
brightness = max(rgb_color)
else:
brightness = attributes.get(ATTR_BRIGHTNESS)
if isinstance(brightness, (int, float)):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
if (
self.brightness_supported
and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
and isinstance(brightness, (int, float))
):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
if color_mode_changed:
self.char_brightness.notify()
# Handle Color - color must always be set before color temperature
# or the iOS UI will not display it correctly.
if self.color_supported:
if ATTR_COLOR_TEMP in attributes:
if color_temp := attributes.get(ATTR_COLOR_TEMP):
hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin(
new_state.attributes[ATTR_COLOR_TEMP]
)
color_temperature_mired_to_kelvin(color_temp)
)
elif color_mode == COLOR_MODE_WHITE:
hue, saturation = 0, 0
else:
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
self.char_hue.set_value(round(hue, 0))
self.char_saturation.set_value(round(saturation, 0))
if color_mode_changed:
# If the color temp changed, be sure to force the color to update
self.char_hue.notify()
self.char_saturation.notify()
# Handle color temperature
if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP)
# Handle white channels
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temp = None
if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP)
elif color_mode == COLOR_MODE_WHITE:
color_temp = self.min_mireds
if isinstance(color_temp, (int, float)):
self.char_color_temp.set_value(round(color_temp, 0))
if color_mode_changed:
self.char_color_temp.notify()

View File

@@ -3,7 +3,7 @@
"name": "HomematicIP Cloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"requirements": ["homematicip==1.0.1"],
"requirements": ["homematicip==1.0.2"],
"codeowners": [],
"quality_scale": "platinum",
"iot_class": "cloud_push"

View File

@@ -3,10 +3,11 @@ import logging
from aiohwenergy import DisabledError
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import DOMAIN, PLATFORMS
@@ -20,6 +21,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("__init__ async_setup_entry")
# Migrate `homewizard_energy` (custom_component) to `homewizard`
if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data:
# Remove the old config entry ID from the entry data so we don't try this again
# on the next setup
data = entry.data.copy()
old_config_entry_id = data.pop("old_config_entry_id")
hass.config_entries.async_update_entry(entry, data=data)
_LOGGER.debug(
(
"Setting up imported homewizard_energy entry %s for the first time as "
"homewizard entry %s"
),
old_config_entry_id,
entry.entry_id,
)
ent_reg = er.async_get(hass)
for entity in er.async_entries_for_config_entry(ent_reg, old_config_entry_id):
_LOGGER.debug("Removing %s", entity.entity_id)
ent_reg.async_remove(entity.entity_id)
_LOGGER.debug("Re-creating %s for the new config entry", entity.entity_id)
# We will precreate the entity so that any customizations can be preserved
new_entity = ent_reg.async_get_or_create(
entity.domain,
DOMAIN,
entity.unique_id,
suggested_object_id=entity.entity_id.split(".")[1],
disabled_by=entity.disabled_by,
config_entry=entry,
original_name=entity.original_name,
original_icon=entity.original_icon,
)
_LOGGER.debug("Re-created %s", new_entity.entity_id)
# If there are customizations on the old entity, apply them to the new one
if entity.name or entity.icon:
ent_reg.async_update_entity(
new_entity.entity_id, name=entity.name, icon=entity.icon
)
# Remove the old config entry and now the entry is fully migrated
hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id))
# Create coordinator
coordinator = Coordinator(hass, entry.data[CONF_IP_ADDRESS])
try:

View File

@@ -28,6 +28,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize the HomeWizard config flow."""
self.config: dict[str, str | int] = {}
async def async_step_import(self, import_config: dict) -> FlowResult:
"""Handle a flow initiated by older `homewizard_energy` component."""
_LOGGER.debug("config_flow async_step_import")
self.hass.components.persistent_notification.async_create(
(
"The custom integration of HomeWizard Energy has been migrated to core. "
"You can safely remove the custom integration from the custom_integrations folder."
),
"HomeWizard Energy",
f"homewizard_energy_to_{DOMAIN}",
)
return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]})
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -59,12 +74,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
)
data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
if self.source == config_entries.SOURCE_IMPORT:
old_config_entry_id = self.context["old_config_entry_id"]
assert self.hass.config_entries.async_get_entry(old_config_entry_id)
data["old_config_entry_id"] = old_config_entry_id
# Add entry
return self.async_create_entry(
title=f"{device_info[CONF_PRODUCT_NAME]} ({device_info[CONF_SERIAL]})",
data={
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
},
data=data,
)
async def async_step_zeroconf(

View File

@@ -8,6 +8,7 @@ import aiohwenergy
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry
@@ -28,7 +29,9 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
"""Initialize Update Coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
self.api = aiohwenergy.HomeWizardEnergy(host)
session = async_get_clientsession(hass)
self.api = aiohwenergy.HomeWizardEnergy(host, clientsession=session)
async def _async_update_data(self) -> DeviceResponseEntry:
"""Fetch all device and sensor data from api."""

View File

@@ -0,0 +1,34 @@
"""Diagnostics support for P1 Monitor."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import HWEnergyDeviceUpdateCoordinator
TO_REDACT = {CONF_IP_ADDRESS, "serial", "wifi_ssid"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
meter_data = {
"device": coordinator.api.device.todict(),
"data": coordinator.api.data.todict(),
"state": coordinator.api.state.todict()
if coordinator.api.state is not None
else None,
}
return {
"entry": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(meter_data, TO_REDACT),
}

View File

@@ -5,7 +5,7 @@
"codeowners": ["@DCSBL"],
"dependencies": [],
"requirements": [
"aiohwenergy==0.7.0"
"aiohwenergy==0.8.0"
],
"zeroconf": ["_hwenergy._tcp.local."],
"config_flow": true,

View File

@@ -242,7 +242,7 @@ class HoneywellUSThermostat(ClimateEntity):
# Get current mode
mode = self._device.system_mode
# Set hold if this is not the case
if getattr(self._device, f"hold_{mode}") is False:
if getattr(self._device, f"hold_{mode}", None) is False:
# Get next period key
next_period_key = f"{mode.capitalize()}NextPeriod"
# Get next period raw value

View File

@@ -49,11 +49,12 @@ class HueBridge:
self.logger = logging.getLogger(__name__)
# store actual api connection to bridge as api
app_key: str = self.config_entry.data[CONF_API_KEY]
websession = aiohttp_client.async_get_clientsession(hass)
if self.api_version == 1:
self.api = HueBridgeV1(self.host, app_key, websession)
self.api = HueBridgeV1(
self.host, app_key, aiohttp_client.async_get_clientsession(hass)
)
else:
self.api = HueBridgeV2(self.host, app_key, websession)
self.api = HueBridgeV2(self.host, app_key)
# store (this) bridge object in hass data
hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self

View File

@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.11"],
"requirements": ["aiohue==4.0.1"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -76,7 +76,6 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
"""Perform migration of devices and entities to V2 Id's."""
host = entry.data[CONF_HOST]
api_key = entry.data[CONF_API_KEY]
websession = aiohttp_client.async_get_clientsession(hass)
dev_reg = async_get_device_registry(hass)
ent_reg = async_get_entity_registry(hass)
LOGGER.info("Start of migration of devices and entities to support API schema 2")
@@ -93,7 +92,7 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
dev_ids[normalized_mac] = hass_dev.id
# initialize bridge connection just for the migration
async with HueBridgeV2(host, api_key, websession) as api:
async with HueBridgeV2(host, api_key) as api:
sensor_class_mapping = {
SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER,

View File

@@ -3,7 +3,7 @@
"name": "Image",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
"requirements": ["pillow==9.0.0"],
"requirements": ["pillow==9.0.1"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"

View File

@@ -3,7 +3,7 @@
"name": "IntelliFire",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/intellifire",
"requirements": ["intellifire4py==0.5"],
"requirements": ["intellifire4py==0.6"],
"dependencies": [],
"codeowners": ["@jeeftor"],
"iot_class": "local_polling"

View File

@@ -158,6 +158,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
existing_entry = await self.async_set_unique_id(isy_mac)
if not existing_entry:
return
if existing_entry.source == config_entries.SOURCE_IGNORE:
raise data_entry_flow.AbortFlow("already_configured")
parsed_url = urlparse(existing_entry.data[CONF_HOST])
if parsed_url.hostname != ip_address:
new_netloc = ip_address

View File

@@ -2,10 +2,12 @@
from __future__ import annotations
from collections.abc import Callable
import logging
from xknx import XKNX
from xknx.devices import DateTime, ExposeSensor
from xknx.dpt import DPTNumeric
from xknx.dpt import DPTNumeric, DPTString
from xknx.exceptions import ConversionError
from xknx.remote_value import RemoteValueSensor
from homeassistant.const import (
@@ -22,6 +24,8 @@ from homeassistant.helpers.typing import ConfigType, StateType
from .const import KNX_ADDRESS
from .schema import ExposeSchema
_LOGGER = logging.getLogger(__name__)
@callback
def create_knx_exposure(
@@ -101,7 +105,10 @@ class KNXExposeSensor:
"""Initialize state of the exposure."""
init_state = self.hass.states.get(self.entity_id)
state_value = self._get_expose_value(init_state)
self.device.sensor_value.value = state_value
try:
self.device.sensor_value.value = state_value
except ConversionError:
_LOGGER.exception("Error during sending of expose sensor value")
@callback
def shutdown(self) -> None:
@@ -132,6 +139,13 @@ class KNXExposeSensor:
and issubclass(self.device.sensor_value.dpt_class, DPTNumeric)
):
return float(value)
if (
value is not None
and isinstance(self.device.sensor_value, RemoteValueSensor)
and issubclass(self.device.sensor_value.dpt_class, DPTString)
):
# DPT 16.000 only allows up to 14 Bytes
return str(value)[:14]
return value
async def _async_entity_changed(self, event: Event) -> None:
@@ -148,9 +162,10 @@ class KNXExposeSensor:
async def _async_set_knx_value(self, value: StateType) -> None:
"""Set new value on xknx ExposeSensor."""
if value is None:
return
await self.device.set(value)
try:
await self.device.set(value)
except ConversionError:
_LOGGER.exception("Error during sending of expose sensor value")
class KNXExposeTime:

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.19.0"
"xknx==0.19.2"
],
"codeowners": [
"@Julius2342",

View File

@@ -227,7 +227,7 @@ def _async_subscribe_pico_remote_events(
action = ACTION_RELEASE
type_ = device["type"]
name = device["name"]
area, name = device["name"].split("_", 1)
button_number = device["button_number"]
# The original implementation used LIP instead of LEAP
# so we need to convert the button number to maintain compat
@@ -252,7 +252,7 @@ def _async_subscribe_pico_remote_events(
ATTR_BUTTON_NUMBER: lip_button_number,
ATTR_LEAP_BUTTON_NUMBER: button_number,
ATTR_DEVICE_NAME: name,
ATTR_AREA_NAME: name.split("_")[0],
ATTR_AREA_NAME: area,
ATTR_ACTION: action,
},
)

View File

@@ -2,7 +2,7 @@
"domain": "lutron_caseta",
"name": "Lutron Cas\u00e9ta",
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
"requirements": ["pylutron-caseta==0.13.0"],
"requirements": ["pylutron-caseta==0.13.1"],
"config_flow": true,
"zeroconf": ["_leap._tcp.local."],
"homekit": {

View File

@@ -222,17 +222,15 @@ class MotionEyeOptionsFlow(OptionsFlow):
if self.show_advanced_options:
# The input URL is not validated as being a URL, to allow for the possibility
# the template input won't be a valid URL until after it's rendered.
schema.update(
{
vol.Required(
CONF_STREAM_URL_TEMPLATE,
default=self._config_entry.options.get(
CONF_STREAM_URL_TEMPLATE,
"",
),
): str
# the template input won't be a valid URL until after it's rendered
stream_kwargs = {}
if CONF_STREAM_URL_TEMPLATE in self._config_entry.options:
stream_kwargs["description"] = {
"suggested_value": self._config_entry.options[
CONF_STREAM_URL_TEMPLATE
]
}
)
schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, **stream_kwargs)] = str
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))

View File

@@ -20,6 +20,8 @@ from homeassistant.const import (
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_VALUE_TEMPLATE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
@@ -27,6 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.event as evt
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
@@ -95,7 +98,7 @@ async def _async_setup_entity(
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
class MqttBinarySensor(MqttEntity, BinarySensorEntity):
class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
"""Representation a binary sensor that is updated by MQTT."""
_entity_id_format = binary_sensor.ENTITY_ID_FORMAT
@@ -113,6 +116,42 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity):
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
async def async_added_to_hass(self) -> None:
"""Restore state for entities with expire_after set."""
await super().async_added_to_hass()
if (
(expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None
and expire_after > 0
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
):
expiration_at = last_state.last_changed + timedelta(seconds=expire_after)
if expiration_at < (time_now := dt_util.utcnow()):
# Skip reactivating the binary_sensor
_LOGGER.debug("Skip state recovery after reload for %s", self.entity_id)
return
self._expired = False
self._state = last_state.state
self._expiration_trigger = async_track_point_in_utc_time(
self.hass, self._value_is_expired, expiration_at
)
_LOGGER.debug(
"State recovered after reload for %s, remaining time before expiring %s",
self.entity_id,
expiration_at - time_now,
)
async def async_will_remove_from_hass(self) -> None:
"""Remove exprire triggers."""
# Clean up expire triggers
if self._expiration_trigger:
_LOGGER.debug("Clean up expire after trigger for %s", self.entity_id)
self._expiration_trigger()
self._expiration_trigger = None
self._expired = False
await MqttEntity.async_will_remove_from_hass(self)
@staticmethod
def config_schema():
"""Return the config schema."""

View File

@@ -139,7 +139,7 @@ async def info_for_device(hass, device_id):
"topic": topic,
"messages": [
{
"payload": msg.payload,
"payload": str(msg.payload),
"qos": msg.qos,
"retain": msg.retain,
"time": msg.timestamp,

View File

@@ -523,6 +523,11 @@ class MqttDiscoveryUpdate(Entity):
async def async_removed_from_registry(self) -> None:
"""Clear retained discovery topic in broker."""
if not self._removed_from_hass:
# Stop subscribing to discovery updates to not trigger when we clear the
# discovery topic
self._cleanup_discovery_on_remove()
# Clear the discovery topic so the entity is not rediscovered after a restart
discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC]
publish(self.hass, discovery_topic, "", retain=True)

View File

@@ -23,12 +23,15 @@ from homeassistant.const import (
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
@@ -140,7 +143,7 @@ async def _async_setup_entity(
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
class MqttSensor(MqttEntity, SensorEntity):
class MqttSensor(MqttEntity, SensorEntity, RestoreEntity):
"""Representation of a sensor that can be updated using MQTT."""
_entity_id_format = ENTITY_ID_FORMAT
@@ -160,6 +163,42 @@ class MqttSensor(MqttEntity, SensorEntity):
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
async def async_added_to_hass(self) -> None:
"""Restore state for entities with expire_after set."""
await super().async_added_to_hass()
if (
(expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None
and expire_after > 0
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
):
expiration_at = last_state.last_changed + timedelta(seconds=expire_after)
if expiration_at < (time_now := dt_util.utcnow()):
# Skip reactivating the sensor
_LOGGER.debug("Skip state recovery after reload for %s", self.entity_id)
return
self._expired = False
self._state = last_state.state
self._expiration_trigger = async_track_point_in_utc_time(
self.hass, self._value_is_expired, expiration_at
)
_LOGGER.debug(
"State recovered after reload for %s, remaining time before expiring %s",
self.entity_id,
expiration_at - time_now,
)
async def async_will_remove_from_hass(self) -> None:
"""Remove exprire triggers."""
# Clean up expire triggers
if self._expiration_trigger:
_LOGGER.debug("Clean up expire after trigger for %s", self.entity_id)
self._expiration_trigger()
self._expiration_trigger = None
self._expired = False
await MqttEntity.async_will_remove_from_hass(self)
@staticmethod
def config_schema():
"""Return the config schema."""
@@ -197,7 +236,7 @@ class MqttSensor(MqttEntity, SensorEntity):
self.hass, self._value_is_expired, expiration_at
)
payload = self._template(msg.payload)
payload = self._template(msg.payload, default=self._state)
if payload is not None and self.device_class in (
SensorDeviceClass.DATE,

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from google_nest_sdm import diagnostics
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import InfoTrait
from google_nest_sdm.exceptions import ApiException
@@ -11,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DATA_SUBSCRIBER, DOMAIN
from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN
REDACT_DEVICE_TRAITS = {InfoTrait.NAME}
@@ -20,6 +21,9 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict:
"""Return diagnostics for a config entry."""
if DATA_SDM not in config_entry.data:
return {}
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
return {"error": "No subscriber configured"}
@@ -30,22 +34,14 @@ async def async_get_config_entry_diagnostics(
return {"error": str(err)}
return {
**diagnostics.get_diagnostics(),
"devices": [
get_device_data(device) for device in device_manager.devices.values()
]
],
}
def get_device_data(device: Device) -> dict[str, Any]:
"""Return diagnostic information about a device."""
# Return a simplified view of the API object, but skipping any id fields or
# traits that include unique identifiers or personally identifiable information.
# See https://developers.google.com/nest/device-access/traits for API details
return {
"type": device.type,
"traits": {
trait: data
for trait, data in device.raw_data.get("traits", {}).items()
if trait not in REDACT_DEVICE_TRAITS
},
}
# Library performs its own redaction for device data
return device.get_diagnostics()

View File

@@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
"pyatmo==6.2.2"
"pyatmo==6.2.4"
],
"after_dependencies": [
"cloud",

View File

@@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.9.0"],
"requirements": ["pynetgear==0.9.1"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,

View File

@@ -2,6 +2,10 @@
"domain": "oncue",
"name": "Oncue by Kohler",
"config_flow": true,
"dhcp": [{
"hostname": "kohlergen*",
"macaddress": "00146F*"
}],
"documentation": "https://www.home-assistant.io/integrations/oncue",
"requirements": ["aiooncue==0.3.2"],
"codeowners": ["@bdraco"],

View File

@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await openuv.async_update()
except OpenUvError as err:
except HomeAssistantError as err:
LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/overkiz",
"requirements": [
"pyoverkiz==1.3.1"
"pyoverkiz==1.3.2"
],
"zeroconf": [
{

View File

@@ -121,14 +121,22 @@ async def async_setup_entry(
if coordinator.data:
if coordinator.data.electricity:
for description in SENSOR_TYPES_ELECTRICITY:
if description.key == KEY_LAST_ELECTRICITY_COST:
if (
description.key == KEY_LAST_ELECTRICITY_COST
and coordinator.data.electricity[-1] is not None
and coordinator.data.electricity[-1].cost is not None
):
description.native_unit_of_measurement = (
coordinator.data.electricity[-1].cost.currency_unit
)
entities.append(OVOEnergySensor(coordinator, description, client))
if coordinator.data.gas:
for description in SENSOR_TYPES_GAS:
if description.key == KEY_LAST_GAS_COST:
if (
description.key == KEY_LAST_GAS_COST
and coordinator.data.gas[-1] is not None
and coordinator.data.gas[-1].cost is not None
):
description.native_unit_of_measurement = coordinator.data.gas[
-1
].cost.currency_unit

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