Compare commits

...

224 Commits

Author SHA1 Message Date
Paulus Schoutsen
fc2d30c993 Merge pull request #66351 from home-assistant/rc 2022-02-11 14:11:43 -08:00
Paulus Schoutsen
cb7f7dff72 Bumped version to 2022.2.6 2022-02-11 13:31:16 -08:00
J. Nick Koston
c254598331 Add unique id to lutron caseta config entry when missing (#66346) 2022-02-11 13:30:42 -08:00
Franck Nijhof
646c56e0e9 Fix CPUSpeed with missing info (#66339) 2022-02-11 13:30:42 -08:00
Franck Nijhof
f3a3ff28f2 Fix PVOutput when no data is available (#66338) 2022-02-11 13:30:41 -08:00
Allen Porter
087f443368 Fix nest streams that get stuck broken (#66334) 2022-02-11 13:30:40 -08:00
epenet
fcee1ff865 Fix raspihats initialization (#66330)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2022-02-11 13:30:39 -08:00
J. Nick Koston
6084b323df Reduce number of parallel api calls to august (#66328) 2022-02-11 13:30:39 -08:00
starkillerOG
6857562e9e bump motionblinds to 0.5.12 (#66323) 2022-02-11 13:30:38 -08:00
starkillerOG
aef2588f9c bump motionblinds to 0.5.11 (#65988) 2022-02-11 13:30:37 -08:00
Joakim Sørensen
27c5460feb Add guard for invalid EntityCategory value (#66316) 2022-02-11 13:29:07 -08:00
Allen Porter
60b4600019 Bump google-nest-sdm to 1.7.1 (minor patch) (#66304) 2022-02-11 13:26:51 -08:00
Allen Porter
27752f7ad3 Bump google-nest-sdm to 1.7.0 (#66145) 2022-02-11 13:26:51 -08:00
Allen Porter
669c99474b Bump python-nest to 4.2.0 for python 3.10 fixes (#66090) 2022-02-11 13:26:50 -08:00
J. Nick Koston
76872e3789 Fix august token refresh when data contains characters outside of latin1 (#66303)
* WIP

* bump version

* bump
2022-02-11 13:25:01 -08:00
Joakim Plate
2594500452 Correct philips_js usage of the overloaded coordinator (#66287) 2022-02-11 13:25:00 -08:00
uvjustin
65c8363323 Catch ConnectionResetError when writing MJPEG in camera (#66245) 2022-02-11 13:24:59 -08:00
ufodone
dfcad3a13d Disable zone bypass switch feature (#66243)
* Add configuration option to disable the creation of zone bypass switches

* Removed temporary workaround and bumped pyenvisalink version to pick up the correct fix.

* Remove zone bypass configuration option and disable zone bypass switches per code review instructions.
2022-02-11 13:24:35 -08:00
Milan Meulemans
92bc780dd7 Bump aioaseko to 0.0.2 to fix issue (#66240) 2022-02-11 13:22:13 -08:00
Michael
eb781060e8 bump py-synologydsm-api to 1.0.6 (#66226) 2022-02-11 13:22:13 -08:00
Maximilian
a2e7897b1e Add missing nina warnings (#66211) 2022-02-11 13:22:12 -08:00
jjlawren
854308fec2 Handle more Sonos favorites in media browser (#66205) 2022-02-11 13:22:11 -08:00
Otto Winter
0199e8cc43 Bump aioesphomeapi from 10.8.1 to 10.8.2 (#66189) 2022-02-11 13:22:11 -08:00
Erik Montnemery
7cc9a4310d Fix controlling nested groups (#66176) 2022-02-11 13:22:10 -08:00
epenet
5976238126 Fix hdmi-cec initialization (#66172) 2022-02-11 13:22:09 -08:00
Franck Nijhof
caedef5f1a Reduce Spotify API usage (#66315) 2022-02-11 13:16:41 -08:00
Franck Nijhof
a96b91d120 Merge pull request #66161 from home-assistant/rc 2022-02-09 11:47:23 +01:00
Franck Nijhof
2e6ee5165e Bumped version to 2022.2.5 2022-02-09 10:56:05 +01:00
Erik Montnemery
7dd7c1dadd Fix MQTT debug info (#66146) 2022-02-09 10:55:26 +01:00
Aaron Bach
4c548af6ef Bump simplisafe-python to 2022.02.1 (#66140) 2022-02-09 10:55:23 +01:00
Michael
200e07b8d6 Fix system is loaded flag during reboot/shutdown of Synology DSM (#66125) 2022-02-09 10:55:20 +01:00
Richard Benson
ae5a885387 Bump amcrest to 1.9.4 (#66124)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-02-09 10:55:17 +01:00
Simone Chemelli
bebdaacf47 Change detection of router devices for Fritz (#65965) 2022-02-09 10:55:13 +01:00
Erik Montnemery
339fc0a2af Fix flaky homewizard test (#65490) 2022-02-09 10:55:09 +01:00
Dave T
f44ca5f9d5 Fix generic camera typo in attr_frame_interval (#65390) 2022-02-09 10:55:04 +01:00
Paulus Schoutsen
a869c1bc88 Merge pull request #66103 from home-assistant/rc 2022-02-08 14:48:49 -08:00
Erik Montnemery
d5443b8dee Fix ENTITY_CATEGORIES_SCHEMA (#66108)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-08 14:07:59 -08:00
Erik Montnemery
6ec09320dd Fix cleanup of MQTT debug info (#66104) 2022-02-08 14:07:43 -08:00
Paulus Schoutsen
550f80ddd2 Bumped version to 2022.2.4 2022-02-08 12:03:54 -08:00
Raman Gupta
23d2168952 Fix schema for zwave_js WS API (#66052) 2022-02-08 12:03:42 -08:00
J. Nick Koston
c1cb0a0f8e Fix missing exception catch in august to prevent failed setup (#66045) 2022-02-08 12:03:41 -08:00
Erik Montnemery
e53227be79 Fix race in MQTT sensor and binary_sensor expire_after (#66040) 2022-02-08 12:03:40 -08:00
J. Nick Koston
c8c1543b26 Fix decoding discovery with old Magic Home firmwares (#66038) 2022-02-08 12:03:39 -08:00
jjlawren
715fe95abd Clean up Sonos unsubscribe/resubscribe exception handling and logging (#66025) 2022-02-08 12:03:38 -08:00
Erik Montnemery
02cb879717 Speed up deletion of duplicated statistics (#66014) 2022-02-08 12:03:37 -08:00
Joakim Sørensen
9734216215 Use strings directly instead of Enums in version config (#66007) 2022-02-08 12:03:37 -08:00
Erik Montnemery
0f06ebde06 Revert "Make idle chromecasts appear as idle instead of off" (#66005) 2022-02-08 12:03:35 -08:00
Erik Montnemery
7195372616 Suppress unwanted error messages during recorder migration (#66004) 2022-02-08 12:03:35 -08:00
Joakim Sørensen
ac63a7e01e Add diagnostics to Version integration (#65999)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-02-08 12:03:34 -08:00
jjlawren
f08ebf5b7e Bump plexapi to 4.9.2 (#65972) 2022-02-08 12:03:33 -08:00
Tiernan
49d6048278 Fix TOD incorrectly determining the state between sunrise and sunset (#65884)
* Fix TOD component incorrectly determining the state between sunrise and sunset (#30199)

* TOD fix

* Comment added

* Review

* Review

* Review

* Update time after day fix workaround for compatibility with
current version.
Only apply fix when using times and not when using sun events.
Add unit test for behaviour.

Co-authored-by: Nikolay Vasilchuk <Anonym.tsk@gmail.com>
2022-02-08 12:03:32 -08:00
Paulus Schoutsen
ceae63d457 Fix UPNP access to SSDP info (#65728) 2022-02-08 12:03:31 -08:00
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 8a57f772ca.

* New typing attempt

* Revert "New typing attempt"

This reverts commit e26e4aae69.

* 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
322 changed files with 7661 additions and 2336 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

@@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl
homeassistant/components/entur_public_transport/* @hfurubotten
homeassistant/components/environment_canada/* @gwww @michaeldavie
tests/components/environment_canada/* @gwww @michaeldavie
homeassistant/components/envisalink/* @ufodone
homeassistant/components/ephember/* @ttroy50
homeassistant/components/epson/* @pszafer
tests/components/epson/* @pszafer

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

@@ -2,7 +2,7 @@
"domain": "amcrest",
"name": "Amcrest",
"documentation": "https://www.home-assistant.io/integrations/amcrest",
"requirements": ["amcrest==1.9.3"],
"requirements": ["amcrest==1.9.4"],
"dependencies": ["ffmpeg"],
"codeowners": ["@flacjacket"],
"iot_class": "local_polling"

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

@@ -3,7 +3,7 @@
"name": "Aseko Pool Live",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"requirements": ["aioaseko==0.0.1"],
"requirements": ["aioaseko==0.0.2"],
"codeowners": [
"@milanmeu"
],

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 (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
@@ -73,6 +75,7 @@ async def async_setup_august(
hass.config_entries.async_update_entry(config_entry, data=config_data)
await august_gateway.async_authenticate()
await august_gateway.async_refresh_access_token_if_needed()
hass.data.setdefault(DOMAIN, {})
data = hass.data[DOMAIN][config_entry.entry_id] = {
@@ -104,11 +107,10 @@ class AugustData(AugustSubscriberMixin):
async def async_setup(self):
"""Async setup of august device data and activities."""
token = self._august_gateway.access_token
user_data, locks, doorbells = await asyncio.gather(
self._api.async_get_user(token),
self._api.async_get_operable_locks(token),
self._api.async_get_doorbells(token),
)
# This used to be a gather but it was less reliable with august's recent api changes.
user_data = await self._api.async_get_user(token)
locks = await self._api.async_get_operable_locks(token)
doorbells = await self._api.async_get_doorbells(token)
if not doorbells:
doorbells = []
if not locks:
@@ -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.22"],
"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

@@ -222,7 +222,12 @@ async def async_get_mjpeg_stream(
"""Fetch an mjpeg stream from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
return await camera.handle_async_mjpeg_stream(request)
try:
stream = await camera.handle_async_mjpeg_stream(request)
except ConnectionResetError:
stream = None
_LOGGER.debug("Error while writing MJPEG stream to transport")
return stream
async def async_get_still_stream(
@@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView):
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
"""Serve camera stream, possibly with interval."""
if (interval_str := request.query.get("interval")) is None:
stream = await camera.handle_async_mjpeg_stream(request)
try:
stream = await camera.handle_async_mjpeg_stream(request)
except ConnectionResetError:
stream = None
_LOGGER.debug("Error while writing MJPEG stream to transport")
if stream is None:
raise web.HTTPBadGateway()
return stream

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,13 +45,12 @@ 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,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
@@ -461,55 +457,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 +495,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 +548,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 +566,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 +578,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):
"""
@@ -635,7 +637,7 @@ class CastDevice(MediaPlayerEntity):
return STATE_PLAYING
return STATE_IDLE
if self._chromecast is not None and self._chromecast.is_idle:
return STATE_IDLE
return STATE_OFF
return None
@property

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

@@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity):
if info:
self._attr_extra_state_attributes = {
ATTR_ARCH: info["arch_string_raw"],
ATTR_BRAND: info["brand_raw"],
ATTR_ARCH: info.get("arch_string_raw"),
ATTR_BRAND: info.get("brand_raw"),
}
if HZ_ADVERTISED in info:
self._attr_extra_state_attributes[ATTR_HZ] = round(

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

@@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
keep_alive,
hass.loop,
connection_timeout,
False,
)
hass.data[DATA_EVL] = controller
@@ -181,12 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER.debug("The envisalink sent a partition update event")
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
@callback
def async_zone_bypass_update(data):
"""Handle zone bypass status updates."""
_LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones")
async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data)
@callback
def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit."""
@@ -206,7 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
controller.callback_login_failure = async_login_fail_callback
controller.callback_login_timeout = async_connection_fail_callback
controller.callback_login_success = async_connection_success_callback
controller.callback_zone_bypass_update = async_zone_bypass_update
_LOGGER.info("Start envisalink")
controller.start()
@@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
)
)
# Only DSC panels support getting zone bypass status
if panel_type == PANEL_TYPE_DSC:
hass.async_create_task(
async_load_platform(
hass, "switch", "envisalink", {CONF_ZONES: zones}, config
)
)
# Zone bypass switches are not currently created due to an issue with some panels.
# These switches will be re-added in the future after some further refactoring of the integration.
hass.services.async_register(
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA

View File

@@ -2,7 +2,7 @@
"domain": "envisalink",
"name": "Envisalink",
"documentation": "https://www.home-assistant.io/integrations/envisalink",
"requirements": ["pyenvisalink==4.3"],
"codeowners": [],
"requirements": ["pyenvisalink==4.4"],
"codeowners": ["@ufodone"],
"iot_class": "local_push"
}

View File

@@ -3,7 +3,7 @@
"name": "ESPHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==10.8.1"],
"requirements": ["aioesphomeapi==10.8.2"],
"zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter", "@jesserockz"],
"after_dependencies": ["zeroconf", "tag"],

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.22"],
"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):
@@ -157,7 +155,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
self.hass = hass
self.host = host
self.mesh_role = MeshRoles.NONE
self.device_is_router: bool = True
self.device_conn_type: str | None = None
self.device_is_router: bool = False
self.password = password
self.port = port
self.username = username
@@ -188,9 +187,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"]
@@ -198,15 +214,23 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
self._current_firmware = info.get("NewSoftwareVersion")
self._update_available, self._latest_firmware = self._update_device_info()
self.device_is_router = "WANIPConn1" in self.connection.services
if "Layer3Forwarding1" in self.connection.services:
if connection_type := self.connection.call_action(
"Layer3Forwarding1", "GetDefaultConnectionService"
).get("NewDefaultConnectionService"):
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
self.device_conn_type = connection_type[2:][:-2]
self.device_is_router = self.connection.call_action(
self.device_conn_type, "GetInfo"
).get("NewEnable")
@callback
async def _async_update_data(self) -> None:
"""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 +301,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 +355,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
connection_type="",
ip_address=host["ip"],
ssid=None,
wan_access=False,
wan_access=None,
)
mesh_intf = {}
@@ -343,32 +383,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 +562,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 +576,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 +680,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 +797,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 +865,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,49 @@
"""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,
"connection_type": avm_wrapper.device_conn_type,
"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

@@ -81,16 +81,12 @@ def port_entities_list(
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD)
entities_list: list[FritzBoxPortSwitch] = []
connection_type = avm_wrapper.get_default_connection()
if not connection_type:
if not avm_wrapper.device_conn_type:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD)
return []
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2]
# Query port forwardings and setup a switch for each forward for the current device
resp = avm_wrapper.get_num_port_mapping(con_type)
resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type)
if not resp:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
return []
@@ -107,7 +103,7 @@ def port_entities_list(
for i in range(port_forwards_count):
portmap = avm_wrapper.get_port_mapping(con_type, i)
portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i)
if not portmap:
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
continue
@@ -133,7 +129,7 @@ def port_entities_list(
portmap,
port_name,
i,
con_type,
avm_wrapper.device_conn_type,
)
)
@@ -477,10 +473,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

@@ -96,7 +96,7 @@ class GenericCamera(Camera):
if self._stream_source is not None:
self._stream_source.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._attr_frames_interval = 1 / device_info[CONF_FRAMERATE]
self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
self._supported_features = SUPPORT_STREAM if self._stream_source else 0
self.content_type = device_info[CONF_CONTENT_TYPE]
self.verify_ssl = device_info[CONF_VERIFY_SSL]

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

@@ -57,6 +57,8 @@ KEY_POSITION = "position"
DEFAULT_NAME = "Cover Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{

View File

@@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE}
DEFAULT_NAME = "Fan Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{

View File

@@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute
DEFAULT_NAME = "Light Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

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

@@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None):
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901
"""Set up the CEC capability."""
hass.data[DOMAIN] = {}
# Parse configuration into a dict of device name to physical address
# represented as a list of four elements.
device_aliases = {}

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

@@ -35,7 +35,7 @@ from homeassistant.const import (
Platform,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.helpers.entity import validate_entity_category
from .const import (
CONF_INVERT,
@@ -320,7 +320,7 @@ class BinarySensorSchema(KNXPlatformSchema):
),
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -356,7 +356,7 @@ class ButtonSchema(KNXPlatformSchema):
vol.Exclusive(
CONF_TYPE, "length_or_type", msg=length_or_type_msg
): object,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
vol.Any(
@@ -500,7 +500,7 @@ class ClimateSchema(KNXPlatformSchema):
): vol.In(HVAC_MODES),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -555,7 +555,7 @@ class CoverSchema(KNXPlatformSchema):
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)
@@ -618,7 +618,7 @@ class FanSchema(KNXPlatformSchema):
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_MAX_STEP): cv.byte,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -722,7 +722,7 @@ class LightSchema(KNXPlatformSchema):
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
vol.Any(
@@ -802,7 +802,7 @@ class NumberSchema(KNXPlatformSchema):
vol.Optional(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_MIN): vol.Coerce(float),
vol.Optional(CONF_STEP): cv.positive_float,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
number_limit_sub_validator,
@@ -824,7 +824,7 @@ class SceneSchema(KNXPlatformSchema):
vol.Required(CONF_SCENE_NUMBER): vol.All(
vol.Coerce(int), vol.Range(min=1, max=64)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -855,7 +855,7 @@ class SelectSchema(KNXPlatformSchema):
],
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
select_options_sub_validator,
@@ -880,7 +880,7 @@ class SensorSchema(KNXPlatformSchema):
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
vol.Required(CONF_TYPE): sensor_type_validator,
vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -901,7 +901,7 @@ class SwitchSchema(KNXPlatformSchema):
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
)
@@ -948,7 +948,7 @@ class WeatherSchema(KNXPlatformSchema):
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
}
),
)

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