Compare commits

...

242 Commits

Author SHA1 Message Date
Paulus Schoutsen
30db51a49c Merge pull request #70054 from home-assistant/rc 2022-04-14 13:36:07 -07:00
Paulus Schoutsen
a537534880 Add media player features enum 2022-04-14 12:48:51 -07:00
Paulus Schoutsen
ea8ee02403 Check supported features in media player reproduce state (#70055) 2022-04-14 12:46:21 -07:00
Paulus Schoutsen
d244af6df1 Bumped version to 2022.4.4 2022-04-14 10:09:26 -07:00
Raman Gupta
74d38e00e4 Fix tomorrow.io units... again... (#70029) 2022-04-14 10:09:20 -07:00
Joakim Sørensen
e01faa7a8f Handle KeyError when loading backups (#70028) 2022-04-14 10:09:20 -07:00
Barry Williams
8bdce8ef68 use newer version of openhomedevice (#70022) 2022-04-14 10:00:27 -07:00
David F. Mulcahey
31df67a4c1 ZHA diagnostics fixes (#70000) 2022-04-14 10:00:26 -07:00
Aaron Bach
fe7c3a7ba5 Fix missing interior battery sensor for Ambient PWS (#69994) 2022-04-14 10:00:25 -07:00
epenet
276e8f185b Suppress UpnpResponseError in SamsungTV (#69984) 2022-04-14 10:00:24 -07:00
Michael Chisholm
741252a32d Fix config_flow error for UPnP info with single service (#69979) 2022-04-14 10:00:24 -07:00
Michael Chisholm
f8db38c0b6 Fix config_flow error from dlna_dmr for UPnP discovery info containing a single service (#69977) 2022-04-14 10:00:23 -07:00
uvjustin
4ce6b6dd22 Use ha-av instead of av and bump to v9.1.1-3 (#69974) 2022-04-14 10:00:22 -07:00
Joakim Sørensen
de0126c880 Fix available property in the base supervisor entity (#69966) 2022-04-14 10:00:21 -07:00
J. Nick Koston
7bd60bf0fb Fix HomeKit Controller device class for CO Sensors (#69949) 2022-04-14 10:00:20 -07:00
J. Nick Koston
69828da4bc Fix race during homekit controller pairing (#69948) 2022-04-14 10:00:20 -07:00
Diogo Gomes
261ae2ef33 Fix Prosegur availability through Alexa (#69941) 2022-04-14 10:00:18 -07:00
Sander
814cbcd13a Remove duplicate program (#69734) 2022-04-14 10:00:17 -07:00
Paulus Schoutsen
398c7be850 Merge pull request #69935 from home-assistant/rc 2022-04-12 16:19:07 -07:00
Paulus Schoutsen
25fc64a9e0 Guard against non http schemes (#69938) 2022-04-12 15:27:38 -07:00
Paulus Schoutsen
a543160070 Not all music are URLs (#69936) 2022-04-12 15:27:37 -07:00
rappenze
51bfe53444 Fix fibaro light state for rgb lights and HC3 (#69884) 2022-04-12 15:27:36 -07:00
Paulus Schoutsen
cc6afdba3c Bumped version to 2022.4.3 2022-04-12 14:14:13 -07:00
puddly
8a8ee3c732 Downgrade ZHA dependency zigpy-deconz from 0.15.0 to 0.14.0 (#69927) 2022-04-12 14:14:09 -07:00
Erik Montnemery
27721d5b84 Fix adjusting statistics in ft³ (#69913)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-12 14:14:08 -07:00
Erik Montnemery
fee80a9d4a Fix adjusting 5-minute statistics (#69921) 2022-04-12 14:13:53 -07:00
Franck Nijhof
e49da79d1b Fix climate HVAC device condition (#69908) 2022-04-12 14:13:18 -07:00
epenet
ec541ca7ed Bump renault-api to 0.1.11 (#69900) 2022-04-12 14:13:17 -07:00
epenet
f5bb9e6047 Fix unique id in SamsungTV config flow (#69899)
* Fix unique id in SamsungTV config flow

* coverage

Co-authored-by: J. Nick Koston <nick@koston.org>
2022-04-12 14:13:17 -07:00
Joakim Sørensen
242bd921df Handle add-on issues (#69897) 2022-04-12 14:13:16 -07:00
puddly
ba16156a79 Bump zigpy to 0.44.2 and and zha-quirks to 0.0.72 (#69879) 2022-04-12 14:13:15 -07:00
starkillerOG
84d8a7857d Motion blinds fix set absolute position service (#69873) 2022-04-12 14:13:14 -07:00
Erik Montnemery
9607dfe57c Use quickplay when casting splash for mediaplayer.turn_on (#69866) 2022-04-12 14:13:14 -07:00
Allen Porter
aeb8dc2c07 Fix google calendar timestamp out of range (#69863) 2022-04-12 14:13:13 -07:00
Mick Vleeshouwer
71fb2d09b7 Fix #69694 (#69850) 2022-04-12 14:13:12 -07:00
Guido Schmitz
fd8fb59f7a Bump devolo-home-control-api to 0.18.1 (#69840) 2022-04-12 14:13:11 -07:00
David F. Mulcahey
49bf1d6bff Add diagnostics support for ZHA (#69756) 2022-04-12 14:13:10 -07:00
Raj Laud
8bd07bcff2 Handle Squeezebox media ids that are not URLs (#69696) 2022-04-12 14:13:10 -07:00
J. Nick Koston
85bc863830 Fix profiler object growth logging test (#69211) 2022-04-12 14:13:09 -07:00
Marvin Wichmann
094c185dee Update xknx to 0.20.2 (RC) (#69859) 2022-04-11 19:49:18 +02:00
Franck Nijhof
a1fddc3c4d Merge pull request #69835 from home-assistant/rc 2022-04-11 11:02:51 +02:00
Dave T
f6aead6773 Don't test config on yaml import for generic camera (#69714) 2022-04-10 23:15:04 -07:00
Paulus Schoutsen
2fad42ce06 Bumped version to 2022.4.2 2022-04-10 22:59:28 -07:00
J. Nick Koston
3e92659260 Downgrade av to 8.1.0 to fix memory leak (#69833) 2022-04-10 22:59:22 -07:00
jjlawren
02eec73644 Retry on more Plex connection failures during startup (#69822) 2022-04-10 22:59:21 -07:00
jjlawren
8e3e6efb21 Speed up Plex playback for multiple videos (#69821) 2022-04-10 22:59:20 -07:00
Raman Gupta
5d4c1d9fe4 Reduce API limit for tomorrow.io (#69818) 2022-04-10 22:59:20 -07:00
rikroe
2871ac4f8f Fix converting (value, unit) tuples if value is None (#69802)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2022-04-10 22:59:19 -07:00
Michael Davie
506f8c1d94 Bump slixmpp to 1.8.2 (#69794) 2022-04-10 22:59:18 -07:00
Allen Porter
5c4df657b2 Bump rtsp-to-webrtc to 0.5.1 (#69776) 2022-04-10 22:59:17 -07:00
Allen Porter
16a1a93332 Handle expired credentials in reauth in google calendar initialization (#69772)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-10 22:59:16 -07:00
Maximilian
7c06514bb4 Upgrade pynina to 0.1.8 (#69771) 2022-04-10 22:59:15 -07:00
Christopher Bailey
0ebd9e093d Fix unifiprotect for 2.0.0-beta2 of UniFi Protect (#69762) 2022-04-10 22:59:15 -07:00
Mike Fugate
d9253fd310 Fix SleepIQ firmness number step and min values (#69757)
* fix sleepiq firmness number step and min values

* add asserts for min/max/step attributes
2022-04-10 22:59:14 -07:00
Malte Franken
0d7cbb8266 Bump aio_georss_gdacs to 0.7 (#69743) 2022-04-10 22:59:13 -07:00
J. Nick Koston
2ca8a0ef4a Increase tplink effects random seed allowed range to 1-600 (#69725)
* Increase tplink effects random seed allowed range to 1-600

Reported https://community.home-assistant.io/t/tp-link-integration-support-for-kl430-led-light-strip/190635/62?u=bdraco

* cover
2022-04-10 22:59:13 -07:00
Dave T
2c48f28f13 Support webp still image format in generic camera (#69718) 2022-04-10 22:59:12 -07:00
Allen Porter
2298a1fa70 Refresh google calendar tokens with invalid expiration times (#69679)
* Refresh google calendar tokens with invalid expiration times

* Update tests/components/google/conftest.py

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

* Remove unnecessary async methods in functions being touched already

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-10 22:59:11 -07:00
Shay Levy
87ba8a56ee Fix Shelly gen2 cover unavailable when not calibrated (#69671) 2022-04-10 22:59:10 -07:00
Francois Chagnon
39e4d3e63b Add None guard for zwave_js humidifier entity (#69667)
* Add None guard for humidifier entity is_on

* Add guards in more places

* Update homeassistant/components/zwave_js/humidifier.py

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

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-10 22:59:10 -07:00
epenet
269405aee0 Suppress Upnp parsing errors in SamsungTV (#69664) 2022-04-10 22:59:09 -07:00
KNXBroker
b1eda25ca3 Fix soundtouch service calls (#69655) 2022-04-10 22:59:08 -07:00
epenet
39e9270b79 Fix upnp subscription in SamsungTV (#69652)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-04-10 22:59:07 -07:00
starkillerOG
5a408d4083 Fix Netgear switch state update (#69597) 2022-04-10 22:59:07 -07:00
azrdev
509d6ffcb2 Update python-mpd2 to 3.0.5 (#69304) 2022-04-10 22:59:06 -07:00
Paulus Schoutsen
919f4dd719 Merge pull request #69509 from home-assistant/rc 2022-04-07 23:10:23 -07:00
Allen Porter
d9cbbd3b05 Fix bugs calendar oauth token date handling (#69641) 2022-04-07 21:53:56 -07:00
Matt Zimmerman
7e317bed3e [powerwall] Skip backup reserve sensor if data is unavailable (#69637) 2022-04-07 20:34:25 -07:00
David F. Mulcahey
8017cb274e Fix Samjin Multi acceleration in ZHA (#69636) 2022-04-07 20:28:15 -07:00
David F. Mulcahey
4d4eb5c850 Bump ZHA quirks to 0.0.71 (#69633) 2022-04-07 17:16:51 -07:00
puddly
1866e58ac5 Move new zha_event command parameters into a params key to ensure backwards compatibility (#69631) 2022-04-07 15:33:50 -07:00
north3221
b50a78d1d9 Fix tado default overlay for when set pre new overlay feature (#69584) 2022-04-07 15:23:25 -07:00
puddly
88a081be24 Fix ZHA group creation (#69629) 2022-04-07 15:05:11 -07:00
J. Nick Koston
3dd0ddb73e Mark backgrounds optional for tplink random effects (#69622) 2022-04-07 15:05:10 -07:00
Álvaro Fernández Rojas
9063428358 Update aioairzone to v0.3.3 (#69615) 2022-04-07 15:05:09 -07:00
Álvaro Fernández Rojas
ee06b2a1b5 Update aioairzone to v0.3.1 (#68975) 2022-04-07 15:05:08 -07:00
Diogo Gomes
62d67a4287 Fix utility_meter reset service (#69612) 2022-04-07 15:02:49 -07:00
Jason Hunter
0b2f0a9f7c Log which device has the time discrepancy (#69595) 2022-04-07 15:02:49 -07:00
Dave T
7803845af1 Generic fix stream thumbnail (#69378) 2022-04-07 15:02:48 -07:00
J. Nick Koston
2dd3dc2d2d Run energy db calls in the db executor (#69544)
Fixes #69537
2022-04-07 15:26:15 +02:00
J. Nick Koston
ceb8d86a7e Fix registered entities without a category not being exclude-able in the HomeKit UI (#69543) 2022-04-07 15:26:12 +02:00
Joakim Sørensen
e726ef662c Fix adding OS entities for supervised installations (#69539) 2022-04-07 15:26:08 +02:00
Allen Porter
8c9534d2ba Gracefully handle empty summary in google calendar (#69520)
Gracefully handle empty summary in google calendar matching the old behavior
before some code cleanup.
2022-04-07 15:26:04 +02:00
Paulus Schoutsen
5cadea91bb Bumped version to 2022.4.1 2022-04-06 22:36:39 -07:00
J. Nick Koston
f9d447e4cd Fix reloading the sun integration (#69495) 2022-04-06 22:35:55 -07:00
Shay Levy
23bb38c5cf Fix remote_rpi_gpio missing requirement (#69488) 2022-04-06 22:35:55 -07:00
Joakim Sørensen
4c16563675 Bump pyhaversion from 22.04.0 to 22.4.1 (#69486) 2022-04-06 22:35:54 -07:00
J. Nick Koston
9351fcf369 Fix reload race in unifiprotect (#69485)
- The integration already has a reload listener installed
  once it is setup. We should not reload from the config
  flow since they compete
2022-04-06 22:35:53 -07:00
Michael
2d74beaa67 Ignore IPv6 link local address on ssdp discovery in Fritz!Smarthome (#69455) 2022-04-06 22:35:52 -07:00
J. Nick Koston
87ab96f9c1 Fix elkm1 connection when panel drops VN request (#69454) 2022-04-06 22:35:52 -07:00
Paulus Schoutsen
0eed329bc8 Fix telegram broadcast (#69452) 2022-04-06 22:35:51 -07:00
Dave T
ea5e894ac7 Continue on template error during yaml import for generic (#69440)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-04-06 22:35:50 -07:00
Raman Gupta
91d2fafe1d Add comments to zwave_js node metadata WS API (#67210)
* Add comments to zwave_js node metadata WS API

* Add test dat
2022-04-06 22:35:50 -07:00
Franck Nijhof
7dd19066e8 Merge pull request #69413 from home-assistant/rc 2022-04-06 15:14:55 +02:00
Franck Nijhof
be3c1055dd Bumped version to 2022.4.0 2022-04-06 14:01:53 +02:00
René Klomp
5a24dbbbf2 Update pysma to 0.6.11 (#69397) 2022-04-06 14:00:49 +02:00
Erik Montnemery
8174b831cf Restore attributes of template binary sensor (#69350) 2022-04-06 14:00:46 +02:00
Raman Gupta
8c794ecf93 Fix regression in zwave_js (#69312)
* Handle unique ID update during discovery step

* Use callback to convert unique IDs to strings

* Adjust test to make sure logic works

* Fix other tests

* Move comment

* Move migration to async_setup

* Remove async_migrate_entry since we take care of it during setup

* Remove unused test
2022-04-06 14:00:42 +02:00
hesselonline
072cd29b90 Fix Wallbox charger status (#68708)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-06 14:00:31 +02:00
Paulus Schoutsen
e3b20cf43f Bumped version to 2022.4.0b6 2022-04-05 16:40:33 -07:00
Zack Barett
2296d0fbee 20220405.0 (#69377) 2022-04-05 16:40:27 -07:00
J. Nick Koston
1e6f8fc48a Abort samsungtv discovery of legacy devices when unique id not available (#69376) 2022-04-05 16:40:26 -07:00
Franck Nijhof
4038575806 Disable Spotify Media Player entity by default (#69372) 2022-04-05 16:40:25 -07:00
Johan Nenzén
531aa87170 Bump pyplaato to 0.0.16 (#69361) 2022-04-05 16:40:24 -07:00
Marvin Wichmann
1896e39f60 Update XKNX to version 0.20.1 (#69353) 2022-04-05 16:40:23 -07:00
starkillerOG
a42327ffce bump pynetgear to 0.9.4 (#69346)
* Bump home-assistant/wheels from 2022.01.0 to 2022.01.1

Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2022.01.0 to 2022.01.1.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2022.01.0...2022.01.1)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump home-assistant/wheels from 2022.01.1 to 2022.01.2

Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2022.01.1 to 2022.01.2.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2022.01.1...2022.01.2)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump home-assistant/builder from 2021.12.0 to 2022.01.0

Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2021.12.0 to 2022.01.0.
- [Release notes](https://github.com/home-assistant/builder/releases)
- [Commits](https://github.com/home-assistant/builder/compare/2021.12.0...2022.01.0)

---
updated-dependencies:
- dependency-name: home-assistant/builder
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump pynetgear to 0.9.4

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-05 16:40:22 -07:00
shbatm
def04f1ae8 Bump PyISY to 3.0.6 to fix group statuses (#69345) 2022-04-05 16:40:21 -07:00
J. Nick Koston
a39a6fce2a Fix roomba doing I/O in the event loop (#69339) 2022-04-05 16:40:21 -07:00
J. Nick Koston
7b36434101 Try exact match first for update state (#69335)
- Exact matches are much cheaper than creating an AwesomeVersion
  and calling the __gt__ method, and since most of the time the
  result is expected to be off, we want to optimize for this case
2022-04-05 16:40:20 -07:00
Philip Allgaier
a3ac495e03 Prevent issues with config update of "Timer" integration (unknown "restore" key) (#69332) 2022-04-05 16:40:19 -07:00
Joakim Sørensen
186d8c9d50 Bump pyhaversion from 22.02.0 to 22.04.0 (#69329) 2022-04-05 16:40:19 -07:00
Martin Hjelmare
e94fad469f Use recorder executor in demo (#69327) 2022-04-05 16:40:18 -07:00
J. Nick Koston
90d5bd12fb Ensure state is restored when turning on tplink lights without a color mode (#69308) 2022-04-05 16:40:17 -07:00
MoellerDi
685af1dd5c Fix "Camera not found" error in microsoft_face integration (#69295) 2022-04-05 16:40:17 -07:00
Erik Montnemery
44fefa42a8 Improve integration translation strings (#69246)
* Improve integration translation strings

* Update
2022-04-05 16:40:16 -07:00
Paulus Schoutsen
681242f102 Bumped version to 2022.4.0b5 2022-04-04 23:57:57 -07:00
Daniel Hjelseth Høyer
df2a31a70b Update Tibber lib (#69300) 2022-04-04 23:57:46 -07:00
starkillerOG
dc7d140c29 bump pynetgear to 0.9.3 (#69292) 2022-04-04 23:57:46 -07:00
Erik Montnemery
96ac47f36e Correct unit_of_measurement for trigger-based template sensor (#69291) 2022-04-04 23:57:45 -07:00
Joakim Sørensen
b66770d349 Make hassio coordinator refresh data (#69272) 2022-04-04 23:57:44 -07:00
Erik Montnemery
eab7876330 Improve utility_meter translation strings (#69252) 2022-04-04 23:57:43 -07:00
Erik Montnemery
45843297f9 Improve tod translation strings (#69251) 2022-04-04 23:57:43 -07:00
Erik Montnemery
4313be1ca2 Improve threshold translation strings (#69250) 2022-04-04 23:57:42 -07:00
Erik Montnemery
8191172f07 Improve switch_as_x translation strings (#69249) 2022-04-04 23:57:41 -07:00
Erik Montnemery
408f87c7e6 Improve min_max translation strings (#69248)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-04 23:57:41 -07:00
Erik Montnemery
37c0200f83 Improve derivative translation strings (#69245) 2022-04-04 23:57:40 -07:00
Raman Gupta
66cc2c7846 Fix tomorrowio sensor units and conversions (#69166)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-04-04 23:57:39 -07:00
Radu Cotescu
40b9f2f578 Input binary sensors are not generated for Shelly 1 Pro (#69046)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2022-04-04 23:57:39 -07:00
Toke Høiland-Jørgensen
2efa9f00d5 Fix network starting with no configured IPv4 addresses (#69030)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-04-04 23:57:38 -07:00
Franck Nijhof
e6dcaaadd8 Bumped version to 2022.4.0b4 2022-04-04 11:14:39 +02:00
Erik Montnemery
73cd4a3366 Unsubscribe from listeners when removing threshold binary sensor (#69236) 2022-04-04 11:13:53 +02:00
Erik Montnemery
9ec9dc047b Unsubscribe from listeners when removing integration sensor (#69235) 2022-04-04 11:13:50 +02:00
Erik Montnemery
a3ad44f7b6 Unsubscribe from listeners when removing derivative sensor (#69234) 2022-04-04 11:13:46 +02:00
Erik Montnemery
da3f9fdf7d Drop unsupported SI-prefix peta from integration sensor (#69229) 2022-04-04 11:13:43 +02:00
Simone Chemelli
eeeff761e9 Avoid fritz API calls during shutdown (#69225) 2022-04-04 11:13:40 +02:00
J. Nick Koston
3cd9c7d19d Only fire device_registry_updated for suggested_area if the suggestion results in an area change (#69215) 2022-04-04 11:13:36 +02:00
Franck Nijhof
62b44e6903 Exclude more media player attributes from recorder (#69209) 2022-04-04 11:13:33 +02:00
Daniel Hjelseth Høyer
9c6a69775a Tibber, Use a dedicated executor pool for database operations (#69208) 2022-04-04 11:13:30 +02:00
Franck Nijhof
8ce1b104eb Exclude weather forecast attribute from recorder (#69205) 2022-04-04 11:13:26 +02:00
Franck Nijhof
2916d35626 Exclude update entity picture attribute from recorder (#69201) 2022-04-04 11:13:23 +02:00
Franck Nijhof
814f92a75c Set brand icon as entity picture on update entities (#69200)
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
2022-04-04 11:13:19 +02:00
J. Nick Koston
1ae45fa7ad Exclude static vacuum attributes from being recorded in the database (#69199) 2022-04-04 11:13:16 +02:00
J. Nick Koston
e18d61224d Exclude static siren attributes from being recorded in the database (#69196) 2022-04-04 11:13:11 +02:00
J. Nick Koston
7cfaf46287 Exclude static select attributes from being recorded in the database (#69195) 2022-04-04 11:13:08 +02:00
J. Nick Koston
cebf53c340 Exclude static number attributes from being recorded in the database (#69194) 2022-04-04 11:13:03 +02:00
J. Nick Koston
8fa8716387 Exclude static humidifier attributes from being recorded in the database (#69193) 2022-04-04 11:12:59 +02:00
J. Nick Koston
5a96b0ffbc Exclude static fan attributes from being recorded in the database (#69192) 2022-04-04 11:12:56 +02:00
Erik Montnemery
2e13fb3c24 Unsubscribe listeners when entity meter sensor is removed (#69172) 2022-04-04 11:12:52 +02:00
Jesse Hills
90cb9ccde2 ESPHome: Remove disconnect callbacks after they are done (#69169) 2022-04-04 11:12:49 +02:00
J. Nick Koston
9a91a7edf5 Exclude supported features and attribution from being recorded in the database (#69165) 2022-04-04 11:12:46 +02:00
Raman Gupta
c769b20256 Make zwave_js config entry unique ID a string (#69163)
* Make zwave_js config entry unique ID a string

* Add test

* Fix tests
2022-04-04 11:12:42 +02:00
J. Nick Koston
47c1e48166 Exclude static water_heater attributes from being recorded in the database (#69159) 2022-04-04 11:12:39 +02:00
J. Nick Koston
0e6550ab70 Exclude static climate attributes from being recorded in the database (#69158) 2022-04-04 11:12:36 +02:00
J. Nick Koston
5fa2dc540b Exclude static and token attributes from being recorded for media_player (#69156) 2022-04-04 11:12:32 +02:00
J. Nick Koston
b503be9301 Exclude static light attributes from being recorded in the database (#69155) 2022-04-04 11:12:29 +02:00
Diogo Gomes
d99e04e2bc Makes sure YAML defined tariffs are unique (#69151) 2022-04-04 11:12:25 +02:00
Marc Mueller
39a38d9866 Update hangups to 0.4.18 (#69149) 2022-04-04 11:12:22 +02:00
Alex Yao
bc106cc430 Bump Yeelight to v0.7.10 (#69147) 2022-04-04 11:12:18 +02:00
Franck Nijhof
2ae390a342 Set brand icon on WLED update entity (#69145) 2022-04-04 11:12:15 +02:00
Glenn Waters
5cc58579bb Environment Canada: allow AQHI to pull from forecast when current not available (#69142)
* Allow AQHI to pull from forecast when current not available.

* Remove redundant check.

* Remove lambda.
2022-04-04 11:12:12 +02:00
Bouwe Westerdijk
61129734f5 Bump plugwise to v0.17.3 (#69139) 2022-04-04 11:12:09 +02:00
Raman Gupta
a26e221ebc Fix kodi log spamming again (#69137)
* Fix kodi log spamming again

* use try except else
2022-04-04 11:12:05 +02:00
Michael
026c843545 ignore ip6 link local address on ssdp discovery (#69135) 2022-04-04 11:12:02 +02:00
Mick Vleeshouwer
f2d41a0011 Bugfix for overkiz.alarm_control_panel platform exception (#69131)
* Revert changes

* Remove debug statement
2022-04-04 11:11:58 +02:00
Nenad Bogojevic
bee853d415 Fix withings race condition for access token (#69107) 2022-04-04 11:11:55 +02:00
Erik Montnemery
3ba8ddb192 Return unsubscribe callback from start.async_at_start (#69083)
* Return unsubscribe callback from start.async_at_start

* Update calling code
2022-04-04 11:11:52 +02:00
Wictor
53107e4f2c Refactor telegram_bot polling/webhooks platforms and add tests (#66433)
Co-authored-by: Pär Berge <paer.berge@gmail.com>
2022-04-04 11:11:48 +02:00
Franck Nijhof
b539700e45 Bumped version to 2022.4.0b3 2022-04-02 11:40:11 +02:00
Franck Nijhof
dd2c2eb974 Update wled to 0.13.2 (#69116) 2022-04-02 11:39:44 +02:00
Raman Gupta
c1c8299511 Fix kodi log spamming (#69113) 2022-04-02 11:39:40 +02:00
Franck Nijhof
c72db40082 Bump num of conflicts in pip check (#69112) 2022-04-02 11:39:37 +02:00
Marcel van der Veldt
9327938dcc Allow lowercase none for effect value in Hue lights (#69111) 2022-04-02 11:39:33 +02:00
Marcel van der Veldt
c7f807cf33 Adjust check for orphaned Hue device entries for grouped lights (#69110)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-04-02 11:39:30 +02:00
Raman Gupta
691eac7a7f Fix unit prefixes for derivative and integration (#69109) 2022-04-02 11:39:27 +02:00
Keilin Bickar
e341e55e88 Bump asyncsleepiq to 1.2.3 (#69104) 2022-04-02 11:39:24 +02:00
Glenn Waters
51e8ddbd27 Environment Canada: Fix for when temperature is zero (#69101) 2022-04-02 11:39:20 +02:00
puddly
c2c397527e Bump ZHA dependency zigpy-deconz from 0.14.0 to 0.15.0 (#69099) 2022-04-02 11:39:17 +02:00
Bram Kragten
328f479bc4 Update frontend to 20220401.0 (#69095) 2022-04-02 11:39:13 +02:00
Bouwe Westerdijk
5df7882ce4 Remove use of HVAC_MODE_OFF in plugwise climate, it's not implemented (#69094) 2022-04-02 11:39:10 +02:00
Franck Nijhof
7fe6174bd9 Rename current_version to installed_version in Update platform (#69093) 2022-04-02 11:39:06 +02:00
Dave T
d4a31b037f Add image test cases to generic (#69040) 2022-04-02 11:39:03 +02:00
Mick Vleeshouwer
5db1c67812 Fix Protexial alarm in Overkiz integration (#68996) 2022-04-02 11:38:59 +02:00
Paulus Schoutsen
f40513c95b Bumped version to 2022.4.0b2 2022-04-01 09:30:33 -07:00
Mick Vleeshouwer
2d7cbeb1fc Bump pyoverkiz to 1.3.14 in Overkiz integration (#69084) 2022-04-01 09:30:08 -07:00
Erik Montnemery
718bc734f2 Add bluepy as a dependency for zengge (#69067)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-04-01 09:30:07 -07:00
Erik Montnemery
66c131515a Update zengge codeowners (#69068) 2022-04-01 09:28:01 -07:00
Erik Montnemery
200a24d869 Fix utility_meter startup (#69064) 2022-04-01 09:25:55 -07:00
Matthias Alphart
c030af0919 coerce number selector values to int (#69059) 2022-04-01 09:25:55 -07:00
Erik Montnemery
ad2067381b Bump pychromecast to 11.0.0 (#69057) 2022-04-01 09:25:54 -07:00
Joakim Sørensen
cc9e55594f Add auto_update property to supervisor and addon update entities (#69055) 2022-04-01 09:25:53 -07:00
Joakim Sørensen
fadb63c43a Add auto_update property to UpdateEntity (#69054) 2022-04-01 09:25:53 -07:00
Raman Gupta
0cf817698d Fix zwave_js device action logic (#69049)
* Fix zwave_js device action logic

* Add test for this behavior
2022-04-01 09:25:52 -07:00
Shay Levy
432768f503 Remove webostv deprecated YAML import (#69043)
* webostv: remove deprecated YAML import

* Remove unused CUSTOMIZE_SCHEMA and WEBOSTV_CONFIG_FILE
2022-04-01 09:25:51 -07:00
J. Nick Koston
65ccb7446f Prevent HomeKit from offering hidden entities (#69042) 2022-04-01 09:25:50 -07:00
Mick Vleeshouwer
90bec9cfcd Enable select platform in Overkiz integration (#68995) 2022-04-01 09:25:50 -07:00
Erik Montnemery
ab21ac370c Mend incorrectly imported MQTT config entries (#68987)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-01 09:25:49 -07:00
Matthias Alphart
43b9e4febf Update KNX flow strings to use "data_description" and remove invalid options (#68935)
* use `data_description`

* use selectors so `data_description` shows

* remove unused config option

`individual_address` has no effect for tunneling. The address is always assigned by the tunnelling server.

* remove unsupported option for tunneling V1 devices
2022-04-01 09:25:48 -07:00
Chris Talkington
3659bceedb Update rokuecp to 0.16.0 (#68822) 2022-04-01 09:25:47 -07:00
J. Nick Koston
e4395dee6c Convert statistics to use history api for database access (#68411) 2022-04-01 09:25:47 -07:00
Paulus Schoutsen
0b8bba0db9 Bumped version to 2022.4.0b1 2022-03-31 15:10:09 -07:00
Dave T
2cb77c4702 Improve image checks for generic camera (#69037) 2022-03-31 15:10:01 -07:00
Franck Nijhof
dcbca89d06 Remove deprecated YAML configuration from Yamaha Music Cast (#69033) 2022-03-31 15:10:00 -07:00
Franck Nijhof
9614e8496b Remove deprecated YAML configuration from Fronius (#69032) 2022-03-31 15:10:00 -07:00
Franck Nijhof
deed4c5980 Remove deprecated YAML configuration from EZVIZ (#69031) 2022-03-31 15:09:59 -07:00
Franck Nijhof
9c9bacad31 Remove deprecated YAML configuration from Sensibo (#69028) 2022-03-31 15:09:58 -07:00
Franck Nijhof
daf2f746ed Remove deprecated YAML configuration from Met.no (#69027) 2022-03-31 15:09:57 -07:00
Franck Nijhof
d0bb58c698 Remove deprecated YAML configuration from Yale Smart Alarm (#69025) 2022-03-31 15:09:57 -07:00
Franck Nijhof
652ce897c7 Remove deprecated YAML configuration from Brunt (#69024) 2022-03-31 15:09:56 -07:00
Franck Nijhof
5120a03470 Remove deprecated template support in persistent notifications (#69021) 2022-03-31 15:09:55 -07:00
Joakim Sørensen
b349322055 Cleanup Version after removing YAML (#69020) 2022-03-31 15:09:55 -07:00
Paulus Schoutsen
00b363d896 Enforce EntityCategory enum (#69015)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-03-31 15:09:54 -07:00
Paulus Schoutsen
a1ebea271c Enforce RegistryEntryDisabler enum (#69017)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-03-31 15:09:26 -07:00
Paulus Schoutsen
c73f423102 Device Automation: enforce passing in device-automation-enum (#69013) 2022-03-31 15:06:57 -07:00
Paulus Schoutsen
a630e7dc49 Version: remove deprecated YAML import (#69010) 2022-03-31 15:06:56 -07:00
Paulus Schoutsen
b7e3e7a8e6 Launch Library: remove deprecated YAML import (#69008) 2022-03-31 15:06:56 -07:00
Paulus Schoutsen
7eecf48645 DNS IP: Remove deprecated YAML import (#69007)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-03-31 15:06:55 -07:00
Paulus Schoutsen
bb26ded115 iCloud: remove deprecated YAML import (#69006)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-03-31 15:06:54 -07:00
Paulus Schoutsen
8c65e930d5 Update tradfri deprecation message (#69005) 2022-03-31 15:06:53 -07:00
Paulus Schoutsen
ff332049e1 Nanoleaf: remove deprecated YAML import (#69004) 2022-03-31 15:06:52 -07:00
Paulus Schoutsen
fa12fd3776 Solax: remove deprecated YAML import (#69003) 2022-03-31 15:06:51 -07:00
Paulus Schoutsen
0adeebfe33 Remove deprecated Switchbot import (#69002) 2022-03-31 15:06:50 -07:00
Paulus Schoutsen
2328813e69 Don't log the stack trace (#69000) 2022-03-31 15:06:50 -07:00
starkillerOG
013f376ef3 bump pynetgear to 0.9.2 (#68986) 2022-03-31 15:06:49 -07:00
Erik Montnemery
a8e6ad9f3d Deprecate temperature conversion in base entity class (#68978)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-03-31 15:06:48 -07:00
Erik Montnemery
a8dcecf1ec Make utility_meter tariffs a list (#68967) 2022-03-31 15:06:47 -07:00
Erik Montnemery
76db6acfb2 Improve utility_meter services.yaml (#68960) 2022-03-31 15:06:47 -07:00
Paulus Schoutsen
844d0680f0 Change privacy mode to config (#68954) 2022-03-31 15:06:46 -07:00
Raman Gupta
c684603037 Add support for new select selector properties (#68952)
* Add support for new select selector properties

* fix mode option

* Apply suggestions from code review

* Correct validation for empty options, update tests

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-03-31 15:06:45 -07:00
Allen Porter
e6b88a5214 Fix google calendar blocking call, running outside of executor (#68948) 2022-03-31 15:06:45 -07:00
Robert Hillis
a16d86585b Rename google hangouts to google chat (#68941) 2022-03-31 15:06:44 -07:00
J. Nick Koston
df308f703f Exclude large and chatty attributes from being recorded for update entities (#68940)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-03-31 15:06:43 -07:00
Philip Allgaier
40e7055934 Prevent issues with setting up "Timer" integration (unknown "restore" key) (#68936)
* Prevent issues with setting up "Timer" for existing entities

* Use default constant

* Update homeassistant/components/timer/__init__.py

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-03-31 15:06:42 -07:00
Dave T
45bd4038f4 Generic camera: Allow gif image type in still image checker (#68933) 2022-03-31 15:06:42 -07:00
Dave T
3aa0294b5d Generic camera: Allow svg detect to accept leading whitespace (#68932) 2022-03-31 15:06:41 -07:00
Erik Montnemery
143e9f4fc3 Improve utility_meter services.yaml (#68930) 2022-03-31 15:06:40 -07:00
Nathan Spencer
0de84d882a Bump pylitterbot to 2022.3.0 (#68929) 2022-03-31 15:06:39 -07:00
Bram Kragten
ff786c3be8 Handle config entries of integrations that are removed (#68928) 2022-03-31 15:06:39 -07:00
Michael
d617271ca0 Add "station is open" sensor to Tankerkoenig (#68925) 2022-03-31 15:06:38 -07:00
Erik Montnemery
d38382fbe3 Rename helper_config_entry_flow to schema_config_entry_flow (#68924) 2022-03-31 15:06:37 -07:00
Paulus Schoutsen
475b9e212d Mark all input integrations as helpers (#68922) 2022-03-31 15:06:37 -07:00
puddly
a8ad3292c8 Bump zigpy to 0.44.1 and zha-quirks to 0.0.69 (#68921)
* Make unit tests pass

* Flip response type check to not rely on it being a list
https://github.com/zigpy/zigpy/pull/716#issuecomment-1025236190

* Bump zigpy and quirks versions to ZCLR8 releases

* Fix renamed zigpy cluster attributes

* Handle the default response for ZLL `get_group_identifiers`

* Add more error context to `stage failed` errors

* Fix unit test returning lists as ZCL request responses

* Always load quirks when testing ZHA

* Bump zha-quirks to 0.0.69
2022-03-31 15:06:36 -07:00
rianadon
78d8bc66d1 Calculate temperature precision based on user units (#59560)
* Calculate temperature precision based on user units

* Fix a few more failing tests

* Fix failing test

Co-authored-by: Erik <erik@montnemery.com>
2022-03-31 15:06:35 -07:00
425 changed files with 5832 additions and 4316 deletions

View File

@@ -1197,6 +1197,7 @@ omit =
homeassistant/components/tado/water_heater.py
homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/__init__.py
homeassistant/components/tankerkoenig/binary_sensor.py
homeassistant/components/tankerkoenig/const.py
homeassistant/components/tankerkoenig/sensor.py
homeassistant/components/tapsaff/binary_sensor.py

View File

@@ -1187,6 +1187,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/yi/ @bachya
/homeassistant/components/youless/ @gjong
/tests/components/youless/ @gjong
/homeassistant/components/zengge/ @emontnemery
/homeassistant/components/zeroconf/ @bdraco
/tests/components/zeroconf/ @bdraco
/homeassistant/components/zerproc/ @emlove

View File

@@ -3,7 +3,7 @@
"name": "Airzone",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.2.3"],
"requirements": ["aioairzone==0.3.3"],
"codeowners": ["@Noltari"],
"iot_class": "local_polling",
"loggers": ["aioairzone"]

View File

@@ -28,6 +28,7 @@ TYPE_BATT6 = "batt6"
TYPE_BATT7 = "batt7"
TYPE_BATT8 = "batt8"
TYPE_BATT9 = "batt9"
TYPE_BATTIN = "battin"
TYPE_BATTOUT = "battout"
TYPE_BATT_CO2 = "batt_co2"
TYPE_BATT_LIGHTNING = "batt_lightning"
@@ -140,6 +141,13 @@ BINARY_SENSOR_DESCRIPTIONS = (
entity_category=EntityCategory.DIAGNOSTIC,
on_state=0,
),
AmbientBinarySensorDescription(
key=TYPE_BATTIN,
name="Interior Battery",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
on_state=0,
),
AmbientBinarySensorDescription(
key=TYPE_BATT10,
name="Soil Monitor Battery 10",

View File

@@ -108,7 +108,7 @@ class BackupManager:
size=round(backup_path.stat().st_size / 1_048_576, 2),
)
backups[backup.slug] = backup
except (OSError, TarError, json.JSONDecodeError) as err:
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
return backups

View File

@@ -46,6 +46,17 @@ class BMWSensorEntityDescription(SensorEntityDescription):
value: Callable = lambda x, y: x
def convert_and_round(
state: tuple,
converter: Callable[[float | None, str], float],
precision: int,
) -> float | None:
"""Safely convert and round a value from a Tuple[value, unit]."""
if state[0] is None:
return None
return round(converter(state[0], UNIT_MAP.get(state[1], state[1])), precision)
SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
# --- Generic ---
"charging_start_time": BMWSensorEntityDescription(
@@ -78,45 +89,35 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
icon="mdi:speedometer",
unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: round(
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
),
"remaining_range_total": BMWSensorEntityDescription(
key="remaining_range_total",
icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: round(
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
),
"remaining_range_electric": BMWSensorEntityDescription(
key="remaining_range_electric",
icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: round(
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
),
"remaining_range_fuel": BMWSensorEntityDescription(
key="remaining_range_fuel",
icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: round(
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
),
"remaining_fuel": BMWSensorEntityDescription(
key="remaining_fuel",
icon="mdi:gas-station",
unit_metric=VOLUME_LITERS,
unit_imperial=VOLUME_GALLONS,
value=lambda x, hass: round(
hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2
),
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
),
"fuel_percent": BMWSensorEntityDescription(
key="fuel_percent",

View File

@@ -111,9 +111,3 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input)
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import config from configuration.yaml."""
await self.async_set_unique_id(import_config[CONF_USERNAME].lower())
self._abort_if_unique_id_configured()
return await self.async_step_user(import_config)

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import MutableMapping
import logging
from typing import Any
from aiohttp.client_exceptions import ClientResponseError
@@ -16,12 +15,11 @@ from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -39,28 +37,9 @@ from .const import (
REGULAR_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Component setup, run import config flow for each entry in config."""
_LOGGER.warning(
"Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,

View File

@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==10.3.0"],
"requirements": ["pychromecast==11.0.0"],
"after_dependencies": [
"cloud",
"http",

View File

@@ -469,7 +469,8 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
# The only way we can turn the Chromecast is on is by launching an app
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
self._chromecast.play_media(CAST_SPLASH, "image/png")
app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"}
quick_play(self._chromecast, "default_media_receiver", app_data)
else:
self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)

View File

@@ -75,15 +75,19 @@ def async_condition_from_config(
hass: HomeAssistant, config: ConfigType
) -> condition.ConditionCheckerType:
"""Create a function to test a device condition."""
if config[CONF_TYPE] == "is_hvac_mode":
attribute = const.ATTR_HVAC_MODE
else:
attribute = const.ATTR_PRESET_MODE
def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
"""Test if an entity is a certain state."""
state = hass.states.get(config[ATTR_ENTITY_ID])
return state.attributes.get(attribute) == config[attribute] if state else False
if (state := hass.states.get(config[ATTR_ENTITY_ID])) is None:
return False
if config[CONF_TYPE] == "is_hvac_mode":
return state.state == config[const.ATTR_HVAC_MODE]
return (
state.attributes.get(const.ATTR_PRESET_MODE)
== config[const.ATTR_PRESET_MODE]
)
return test_is_state

View File

@@ -0,0 +1,32 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from .const import (
ATTR_FAN_MODES,
ATTR_HVAC_MODES,
ATTR_MAX_HUMIDITY,
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MIN_TEMP,
ATTR_PRESET_MODES,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_STEP,
)
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static attributes from being recorded in the database."""
return {
ATTR_HVAC_MODES,
ATTR_FAN_MODES,
ATTR_SWING_MODES,
ATTR_MIN_TEMP,
ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_TARGET_TEMP_STEP,
ATTR_PRESET_MODES,
}

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
)
from homeassistant.loader import async_get_config_flows
from homeassistant.loader import Integration, async_get_config_flows
async def async_setup(hass):
@@ -63,19 +63,33 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
integrations = {}
type_filter = request.query["type"]
async def load_integration(
hass: HomeAssistant, domain: str
) -> Integration | None:
"""Load integration."""
try:
return await loader.async_get_integration(hass, domain)
except loader.IntegrationNotFound:
return None
# Fetch all the integrations so we can check their type
for integration in await asyncio.gather(
*(
loader.async_get_integration(hass, domain)
load_integration(hass, domain)
for domain in {entry.domain for entry in entries}
)
):
integrations[integration.domain] = integration
if integration:
integrations[integration.domain] = integration
entries = [
entry
for entry in entries
if integrations[entry.domain].integration_type == type_filter
if (type_filter != "helper" and entry.domain not in integrations)
or (
entry.domain in integrations
and integrations[entry.domain].integration_type == type_filter
)
]
return self.json([entry_json(entry) for entry in entries])

View File

@@ -5,6 +5,7 @@ from random import random
from homeassistant import config_entries, setup
from homeassistant.components import persistent_notification
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
@@ -245,7 +246,7 @@ async def _insert_statistics(hass):
}
statistic_id = f"{DOMAIN}:energy_consumption"
sum_ = 0
last_stats = await hass.async_add_executor_job(
last_stats = await get_instance(hass).async_add_executor_job(
get_last_statistics, hass, 1, statistic_id, True
)
if "domain:energy_consumption" in last_stats:

View File

@@ -31,7 +31,7 @@ async def async_setup_platform(
unique_id="update_no_install",
name="Demo Update No Install",
title="Awesomesoft Inc.",
current_version="1.0.0",
installed_version="1.0.0",
latest_version="1.0.1",
release_summary="Awesome update, fixing everything!",
release_url="https://www.example.com/release/1.0.1",
@@ -41,14 +41,14 @@ async def async_setup_platform(
unique_id="update_2_date",
name="Demo No Update",
title="AdGuard Home",
current_version="1.0.0",
installed_version="1.0.0",
latest_version="1.0.0",
),
DemoUpdate(
unique_id="update_addon",
name="Demo add-on",
title="AdGuard Home",
current_version="1.0.0",
installed_version="1.0.0",
latest_version="1.0.1",
release_summary="Awesome update, fixing everything!",
release_url="https://www.example.com/release/1.0.1",
@@ -57,7 +57,7 @@ async def async_setup_platform(
unique_id="update_light_bulb",
name="Demo Living Room Bulb Update",
title="Philips Lamps Firmware",
current_version="1.93.3",
installed_version="1.93.3",
latest_version="1.94.2",
release_summary="Added support for effects",
release_url="https://www.example.com/release/1.93.3",
@@ -67,7 +67,7 @@ async def async_setup_platform(
unique_id="update_support_progress",
name="Demo Update with Progress",
title="Philips Lamps Firmware",
current_version="1.93.3",
installed_version="1.93.3",
latest_version="1.94.2",
support_progress=True,
release_summary="Added support for effects",
@@ -104,7 +104,7 @@ class DemoUpdate(UpdateEntity):
unique_id: str,
name: str,
title: str | None,
current_version: str | None,
installed_version: str | None,
latest_version: str | None,
release_summary: str | None = None,
release_url: str | None = None,
@@ -114,7 +114,7 @@ class DemoUpdate(UpdateEntity):
device_class: UpdateDeviceClass | None = None,
) -> None:
"""Initialize the Demo select entity."""
self._attr_current_version = current_version
self._attr_installed_version = installed_version
self._attr_device_class = device_class
self._attr_latest_version = latest_version
self._attr_name = name or DEVICE_DEFAULT_NAME
@@ -149,7 +149,7 @@ class DemoUpdate(UpdateEntity):
await _fake_install()
self._attr_in_progress = False
self._attr_current_version = (
self._attr_installed_version = (
version if version is not None else self.latest_version
)
self.async_write_ha_state()

View File

@@ -16,10 +16,10 @@ from homeassistant.const import (
TIME_SECONDS,
)
from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler,
HelperFlowFormStep,
HelperFlowMenuStep,
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
SchemaFlowMenuStep,
)
from .const import (
@@ -37,8 +37,9 @@ UNIT_PREFIXES = [
{"value": "m", "label": "m (milli)"},
{"value": "k", "label": "k (kilo)"},
{"value": "M", "label": "M (mega)"},
{"value": "G", "label": "T (tera)"},
{"value": "T", "label": "P (peta)"},
{"value": "G", "label": "G (giga)"},
{"value": "T", "label": "T (tera)"},
{"value": "P", "label": "P (peta)"},
]
TIME_UNITS = [
{"value": TIME_SECONDS, "label": "Seconds"},
@@ -78,16 +79,16 @@ CONFIG_SCHEMA = vol.Schema(
}
).extend(OPTIONS_SCHEMA.schema)
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"user": HelperFlowFormStep(CONFIG_SCHEMA)
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
}
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
}
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow for Derivative."""
config_flow = CONFIG_FLOW

View File

@@ -250,8 +250,10 @@ class DerivativeSensor(RestoreEntity, SensorEntity):
self._state = derivative
self.async_write_ha_state()
async_track_state_change_event(
self.hass, self._sensor_source_id, calc_derivative
self.async_on_remove(
async_track_state_change_event(
self.hass, self._sensor_source_id, calc_derivative
)
)
@property

View File

@@ -1,8 +1,9 @@
{
"title": "Derivative sensor",
"config": {
"step": {
"user": {
"title": "New Derivative sensor",
"title": "Add Derivative sensor",
"description": "Create a sensor that estimates the derivative of a sensor.",
"data": {
"name": "Name",
@@ -15,14 +16,14 @@
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative."
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
}
}
}
},
"options": {
"step": {
"options": {
"init": {
"data": {
"name": "[%key:component::derivative::config::step::user::data::name%]",
"round": "[%key:component::derivative::config::step::user::data::round%]",

View File

@@ -13,15 +13,16 @@
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative."
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
},
"title": "New Derivative sensor"
"description": "Create a sensor that estimates the derivative of a sensor.",
"title": "Add Derivative sensor"
}
}
},
"options": {
"step": {
"options": {
"init": {
"data": {
"name": "Name",
"round": "Precision",
@@ -33,9 +34,10 @@
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative.."
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.."
}
}
}
}
},
"title": "Derivative sensor"
}

View File

@@ -20,7 +20,6 @@ from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.frame import report
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import IntegrationNotFound, bind_hass
from homeassistant.requirements import async_get_integration_with_requirements
@@ -88,24 +87,6 @@ TYPES = {
}
@bind_hass
async def async_get_device_automations(
hass: HomeAssistant,
automation_type: DeviceAutomationType | str,
device_ids: Iterable[str] | None = None,
) -> Mapping[str, Any]:
"""Return all the device automations for a type optionally limited to specific device ids."""
if isinstance(automation_type, str):
report(
"uses str for async_get_device_automations automation_type. This is "
"deprecated and will stop working in Home Assistant 2022.4, it should be "
"updated to use DeviceAutomationType instead",
error_if_core=False,
)
automation_type = DeviceAutomationType[automation_type.upper()]
return await _async_get_device_automations(hass, automation_type, device_ids)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up device automation."""
websocket_api.async_register_command(hass, websocket_device_automation_list_actions)
@@ -156,26 +137,18 @@ async def async_get_device_automation_platform( # noqa: D103
@overload
async def async_get_device_automation_platform( # noqa: D103
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
) -> "DeviceAutomationPlatformType":
...
async def async_get_device_automation_platform(
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
) -> "DeviceAutomationPlatformType":
"""Load device automation platform for integration.
Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
"""
if isinstance(automation_type, str):
report(
"uses str for async_get_device_automation_platform automation_type. This "
"is deprecated and will stop working in Home Assistant 2022.4, it should "
"be updated to use DeviceAutomationType instead",
error_if_core=False,
)
automation_type = DeviceAutomationType[automation_type.upper()]
platform_name = automation_type.value.section
try:
integration = await async_get_integration_with_requirements(hass, domain)
@@ -215,10 +188,11 @@ async def _async_get_device_automations_from_domain(
)
async def _async_get_device_automations(
@bind_hass
async def async_get_device_automations(
hass: HomeAssistant,
automation_type: DeviceAutomationType,
device_ids: Iterable[str] | None,
device_ids: Iterable[str] | None = None,
) -> Mapping[str, list[dict[str, Any]]]:
"""List device automations."""
device_registry = dr.async_get(hass)
@@ -336,7 +310,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg):
"""Handle request for device actions."""
device_id = msg["device_id"]
actions = (
await _async_get_device_automations(
await async_get_device_automations(
hass, DeviceAutomationType.ACTION, [device_id]
)
).get(device_id)
@@ -355,7 +329,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg):
"""Handle request for device conditions."""
device_id = msg["device_id"]
conditions = (
await _async_get_device_automations(
await async_get_device_automations(
hass, DeviceAutomationType.CONDITION, [device_id]
)
).get(device_id)
@@ -374,7 +348,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
"""Handle request for device triggers."""
device_id = msg["device_id"]
triggers = (
await _async_get_device_automations(
await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, [device_id]
)
).get(device_id)

View File

@@ -66,9 +66,9 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
) -> None:
"""Initialize a devolo binary sensor."""
self._binary_sensor_property = device_instance.binary_sensor_property.get(
self._binary_sensor_property = device_instance.binary_sensor_property[
element_uid
)
]
super().__init__(
homecontrol=homecontrol,
@@ -82,10 +82,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
)
if self._attr_device_class is None:
if device_instance.binary_sensor_property.get(element_uid).sub_type != "":
self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}"
if device_instance.binary_sensor_property[element_uid].sub_type != "":
self._attr_name += (
f" {device_instance.binary_sensor_property[element_uid].sub_type}"
)
else:
self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}"
self._attr_name += f" {device_instance.binary_sensor_property[element_uid].sensor_type}"
self._value = self._binary_sensor_property.state
@@ -114,9 +116,9 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
key: int,
) -> None:
"""Initialize a devolo remote control."""
self._remote_control_property = device_instance.remote_control_property.get(
self._remote_control_property = device_instance.remote_control_property[
element_uid
)
]
super().__init__(
homecontrol=homecontrol,

View File

@@ -63,7 +63,7 @@ class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity):
@property
def current_cover_position(self) -> int:
"""Return the current position. 0 is closed. 100 is open."""
return self._value
return int(self._value)
@property
def is_closed(self) -> bool:

View File

@@ -46,7 +46,7 @@ class DevoloDeviceEntity(Entity):
self.subscriber: Subscriber | None = None
self.sync_callback = self._sync
self._value: int
self._value: float
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""

View File

@@ -2,7 +2,7 @@
"domain": "devolo_home_control",
"name": "devolo Home Control",
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
"requirements": ["devolo-home-control-api==0.17.4"],
"requirements": ["devolo-home-control-api==0.18.1"],
"after_dependencies": ["zeroconf"],
"config_flow": true,
"codeowners": ["@2Fake", "@Shutgun"],

View File

@@ -83,7 +83,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity):
"""Abstract representation of a multi level sensor within devolo Home Control."""
@property
def native_value(self) -> int:
def native_value(self) -> float:
"""Return the state of the sensor."""
return self._value

View File

@@ -54,8 +54,8 @@ class DevoloSirenDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, SirenEntity):
)
self._attr_available_tones = [
*range(
self._multi_level_switch_property.min,
self._multi_level_switch_property.max + 1,
int(self._multi_level_switch_property.min),
int(self._multi_level_switch_property.max) + 1,
)
]
self._attr_supported_features = (

View File

@@ -50,9 +50,9 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity):
device_instance=device_instance,
element_uid=element_uid,
)
self._binary_switch_property = self._device_instance.binary_switch_property.get(
self._attr_unique_id
)
self._binary_switch_property = self._device_instance.binary_switch_property[
self._attr_unique_id # type: ignore[index]
]
self._attr_is_on = self._binary_switch_property.state
def turn_on(self, **kwargs: Any) -> None:

View File

@@ -134,10 +134,16 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
if not discovery_service_list:
return self.async_abort(reason="not_dmr")
discovery_service_ids = {
service.get("serviceId")
for service in discovery_service_list.get("service") or []
}
services = discovery_service_list.get("service")
if not services:
discovery_service_ids: set[str] = set()
elif isinstance(services, list):
discovery_service_ids = {service.get("serviceId") for service in services}
else:
# Only one service defined (etree_to_dict failed to make a list)
discovery_service_ids = {services.get("serviceId")}
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
return self.async_abort(reason="not_dmr")

View File

@@ -77,10 +77,16 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
if not discovery_service_list:
return self.async_abort(reason="not_dms")
discovery_service_ids = {
service.get("serviceId")
for service in discovery_service_list.get("service") or []
}
services = discovery_service_list.get("service")
if not services:
discovery_service_ids: set[str] = set()
elif isinstance(services, list):
discovery_service_ids = {service.get("serviceId") for service in services}
else:
# Only one service defined (etree_to_dict failed to make a list)
discovery_service_ids = {services.get("serviceId")}
if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids):
return self.async_abort(reason="not_dms")

View File

@@ -1,15 +1,11 @@
"""The dnsip component."""
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DNS IP from a config entry."""

View File

@@ -82,14 +82,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Return Option handler."""
return DnsIPOptionsFlowHandler(config_entry)
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import a configuration from config.yaml."""
hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME)
self._async_abort_entries_match({CONF_HOSTNAME: hostname})
config[CONF_HOSTNAME] = hostname
return await self.async_step_user(user_input=config)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:

View File

@@ -6,20 +6,14 @@ import logging
import aiodns
from aiodns.error import DNSError
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CONF_HOSTNAME,
@@ -27,10 +21,6 @@ from .const import (
CONF_IPV6,
CONF_RESOLVER,
CONF_RESOLVER_IPV6,
DEFAULT_HOSTNAME,
DEFAULT_IPV6,
DEFAULT_RESOLVER,
DEFAULT_RESOLVER_IPV6,
DOMAIN,
)
@@ -38,38 +28,6 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=120)
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(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,
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_devices: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the DNS IP sensor."""
_LOGGER.warning(
"Configuration of the DNS IP platform in YAML is deprecated and will be "
"removed in Home Assistant 2022.4; Your existing configuration "
"has been imported into the UI automatically and can be safely removed "
"from your configuration.yaml file"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback

View File

@@ -363,6 +363,9 @@ async def async_wait_for_elk_to_sync(
# VN is the first command sent for panel, when we get
# it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response)
# Some panels do not respond to the vn request so we
# check for lw as well
elk.add_handler("LW", first_response)
elk.add_handler("sync_complete", sync_complete)
for name, event, timeout in (
("login", login_event, login_timeout),

View File

@@ -489,7 +489,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
# Fetch the needed statistics metadata
statistics_metadata.update(
await hass.async_add_executor_job(
await recorder.get_instance(hass).async_add_executor_job(
functools.partial(
recorder.statistics.get_metadata,
hass,

View File

@@ -260,7 +260,7 @@ async def ws_get_fossil_energy_consumption(
statistic_ids.append(msg["co2_statistic_id"])
# Fetch energy + CO2 statistics
statistics = await hass.async_add_executor_job(
statistics = await recorder.get_instance(hass).async_add_executor_job(
recorder.statistics.statistics_during_period,
hass,
start_time,

View File

@@ -2,7 +2,7 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.5.20"],
"requirements": ["env_canada==0.5.21"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

@@ -209,13 +209,23 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
),
)
def _get_aqhi_value(data):
if (aqhi := data.current) is not None:
return aqhi
if data.forecasts and (hourly := data.forecasts.get("hourly")) is not None:
if values := list(hourly.values()):
return values[0]
return None
AQHI_SENSOR = ECSensorEntityDescription(
key="aqhi",
name="AQHI",
device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.current,
value_fn=_get_aqhi_value,
)
ALERT_TYPES: tuple[ECSensorEntityDescription, ...] = (

View File

@@ -80,12 +80,16 @@ class ECWeather(CoordinatorEntity, WeatherEntity):
@property
def temperature(self):
"""Return the temperature."""
if self.ec_data.conditions.get("temperature", {}).get("value"):
return float(self.ec_data.conditions["temperature"]["value"])
if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get(
"temperature"
if (
temperature := self.ec_data.conditions.get("temperature", {}).get("value")
) is not None:
return float(temperature)
if (
self.ec_data.hourly_forecasts
and (temperature := self.ec_data.hourly_forecasts[0].get("temperature"))
is not None
):
return float(self.ec_data.hourly_forecasts[0]["temperature"])
return float(temperature)
return None
@property

View File

@@ -520,6 +520,7 @@ async def _cleanup_instance(
data = domain_data.pop_entry_data(entry)
for disconnect_cb in data.disconnect_callbacks:
disconnect_cb()
data.disconnect_callbacks = []
for cleanup_callback in data.cleanup_callbacks:
cleanup_callback()
await data.client.disconnect()

View File

@@ -7,18 +7,16 @@ from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
import voluptuous as vol
from homeassistant.components import ffmpeg
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import get_ffmpeg_manager
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_IMPORT,
SOURCE_INTEGRATION_DISCOVERY,
ConfigEntry,
)
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
ATTR_DIRECTION,
@@ -27,7 +25,6 @@ from .const import (
ATTR_SERIAL,
ATTR_SPEED,
ATTR_TYPE,
CONF_CAMERAS,
CONF_FFMPEG_ARGUMENTS,
DATA_COORDINATOR,
DEFAULT_CAMERA_USERNAME,
@@ -47,62 +44,9 @@ from .const import (
from .coordinator import EzvizDataUpdateCoordinator
from .entity import EzvizEntity
CAMERA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA},
}
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: entity_platform.AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a Ezviz IP Camera from platform config."""
_LOGGER.warning(
"Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards"
)
# Check if entry config exists and skips import if it does.
if hass.config_entries.async_entries(DOMAIN):
return
# Check if importing camera account.
if CONF_CAMERAS in config:
cameras_conf = config[CONF_CAMERAS]
for serial, camera in cameras_conf.items():
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
ATTR_SERIAL: serial,
CONF_USERNAME: camera[CONF_USERNAME],
CONF_PASSWORD: camera[CONF_PASSWORD],
},
)
)
# Check if importing main ezviz cloud account.
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,

View File

@@ -307,50 +307,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
async def async_step_import(self, import_config):
"""Handle config import from yaml."""
_LOGGER.debug("import config: %s", import_config)
# Check importing camera.
if ATTR_SERIAL in import_config:
return await self.async_step_import_camera(import_config)
# Validate and setup of main ezviz cloud account.
try:
return await self._validate_and_create_auth(import_config)
except InvalidURL:
_LOGGER.error("Error importing Ezviz platform config: invalid host")
return self.async_abort(reason="invalid_host")
except InvalidHost:
_LOGGER.error("Error importing Ezviz platform config: cannot connect")
return self.async_abort(reason="cannot_connect")
except (AuthTestResultFailed, PyEzvizError):
_LOGGER.error("Error importing Ezviz platform config: invalid auth")
return self.async_abort(reason="invalid_auth")
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Error importing ezviz platform config: unexpected exception"
)
return self.async_abort(reason="unknown")
async def async_step_import_camera(self, data):
"""Create RTSP auth entry per camera in config."""
await self.async_set_unique_id(data[ATTR_SERIAL])
self._abort_if_unique_id_configured()
_LOGGER.debug("Create camera with: %s", data)
cam_serial = data.pop(ATTR_SERIAL)
data[CONF_TYPE] = ATTR_TYPE_CAMERA
return self.async_create_entry(title=cam_serial, data=data)
class EzvizOptionsFlowHandler(OptionsFlow):
"""Handle Ezviz client options."""

View File

@@ -5,7 +5,6 @@ MANUFACTURER = "Ezviz"
# Configuration
ATTR_SERIAL = "serial"
CONF_CAMERAS = "cameras"
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
ATTR_HOME = "HOME_MODE"
ATTR_AWAY = "AWAY_MODE"

View File

@@ -0,0 +1,12 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from . import ATTR_PRESET_MODES
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static attributes from being recorded in the database."""
return {ATTR_PRESET_MODES}

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from contextlib import suppress
from functools import partial
from homeassistant.components.light import (
@@ -198,16 +199,21 @@ class FibaroLight(FibaroDevice, LightEntity):
Dimmable and RGB lights can be on based on different
properties, so we need to check here several values.
JSON for HC2 uses always string, HC3 uses int for integers.
"""
props = self.fibaro_device.properties
if self.current_binary_state:
return True
if "brightness" in props and props.brightness != "0":
return True
if "currentProgram" in props and props.currentProgram != "0":
return True
if "currentProgramID" in props and props.currentProgramID != "0":
return True
with suppress(ValueError, TypeError):
if "brightness" in props and int(props.brightness) != 0:
return True
with suppress(ValueError, TypeError):
if "currentProgram" in props and int(props.currentProgram) != 0:
return True
with suppress(ValueError, TypeError):
if "currentProgramID" in props and int(props.currentProgramID) != 0:
return True
return False

View File

@@ -91,6 +91,11 @@ def _cleanup_entity_filter(device: er.RegistryEntry) -> bool:
)
def _ha_is_stopping(activity: str) -> None:
"""Inform that HA is stopping."""
_LOGGER.info("Cannot execute %s: HomeAssistant is shutting down", activity)
class ClassSetupMissing(Exception):
"""Raised when a Class func is called before setup."""
@@ -351,6 +356,10 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
def scan_devices(self, now: datetime | None = None) -> None:
"""Scan for new devices and return a list of found device ids."""
if self.hass.is_stopping:
_ha_is_stopping("scan devices")
return
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
self._update_available, self._latest_firmware = self._update_device_info()
@@ -603,6 +612,10 @@ class AvmWrapper(FritzBoxTools):
) -> dict:
"""Return service details."""
if self.hass.is_stopping:
_ha_is_stopping(f"{service_name}/{action_name}")
return {}
if f"{service_name}{service_suffix}" not in self.connection.services:
return {}

View File

@@ -1,6 +1,7 @@
"""Config flow to configure the FRITZ!Box Tools integration."""
from __future__ import annotations
import ipaddress
import logging
import socket
from typing import Any
@@ -129,6 +130,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
)
self.context[CONF_HOST] = self._host
if ipaddress.ip_address(self._host).is_link_local:
return self.async_abort(reason="ignore_ip6_link_local")
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
if uuid.startswith("uuid:"):
uuid = uuid[5:]

View File

@@ -32,6 +32,7 @@
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {

View File

@@ -3,13 +3,13 @@
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect",
"connection_error": "Failed to connect",
"invalid_auth": "Invalid authentication",
"upnp_not_configured": "Missing UPnP settings on device."
},
@@ -31,16 +31,6 @@
"description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.",
"title": "Updating FRITZ!Box Tools - credentials"
},
"start_config": {
"data": {
"host": "Host",
"password": "Password",
"port": "Port",
"username": "Username"
},
"description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.",
"title": "Setup FRITZ!Box Tools - mandatory"
},
"user": {
"data": {
"host": "Host",

View File

@@ -1,6 +1,7 @@
"""Config flow for AVM FRITZ!SmartHome."""
from __future__ import annotations
import ipaddress
from typing import Any
from urllib.parse import urlparse
@@ -120,6 +121,12 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
assert isinstance(host, str)
self.context[CONF_HOST] = host
if (
ipaddress.ip_address(host).version == 6
and ipaddress.ip_address(host).is_link_local
):
return self.async_abort(reason="ignore_ip6_link_local")
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
if uuid.startswith("uuid:"):
uuid = uuid[5:]

View File

@@ -28,6 +28,7 @@
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"

View File

@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
"no_devices_found": "No devices found on the network",
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
"reauth_successful": "Re-authentication was successful"

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.const import CONF_HOST, CONF_RESOURCE
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
@@ -110,10 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(self, conf: dict) -> FlowResult:
"""Import a configuration from config.yaml."""
return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]})
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
"""Handle a flow initiated by the DHCP client."""
for entry in self._async_current_entries(include_ignore=False):

View File

@@ -1,23 +1,17 @@
"""Support for Fronius devices."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Final
import voluptuous as vol
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_RESOURCE,
ELECTRIC_CURRENT_AMPERE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_WATT_HOUR,
@@ -29,10 +23,8 @@ from homeassistant.const import (
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -49,38 +41,8 @@ if TYPE_CHECKING:
FroniusStorageUpdateCoordinator,
)
_LOGGER: Final = logging.getLogger(__name__)
ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh"
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_MONITORED_CONDITIONS): object,
}
),
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Import Fronius configuration from yaml."""
_LOGGER.warning(
"Loading Fronius via platform setup is deprecated. Please remove it from your yaml configuration"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async def async_setup_entry(
hass: HomeAssistant,

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20220330.0"],
"requirements": ["home-assistant-frontend==20220405.0"],
"dependencies": [
"api",
"auth",

View File

@@ -3,7 +3,7 @@
"name": "Global Disaster Alert and Coordination System (GDACS)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gdacs",
"requirements": ["aio_georss_gdacs==0.5"],
"requirements": ["aio_georss_gdacs==0.7"],
"codeowners": ["@exxamalte"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@@ -4,12 +4,13 @@ from __future__ import annotations
import contextlib
from errno import EHOSTUNREACH, EIO
from functools import partial
import imghdr
import io
import logging
from types import MappingProxyType
from typing import Any
from urllib.parse import urlparse, urlunparse
import PIL
from async_timeout import timeout
import av
from httpx import HTTPStatusError, RequestError, TimeoutException
@@ -57,7 +58,7 @@ DEFAULT_DATA = {
CONF_VERIFY_SSL: True,
}
SUPPORTED_IMAGE_TYPES = ["png", "jpeg", "svg+xml"]
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
def build_schema(
@@ -108,13 +109,32 @@ def build_schema(
return vol.Schema(spec)
def build_schema_content_type(user_input: dict[str, Any] | MappingProxyType[str, Any]):
"""Create schema for conditional 2nd page specifying stream content_type."""
return vol.Schema(
{
vol.Required(
CONF_CONTENT_TYPE,
description={
"suggested_value": user_input.get(CONF_CONTENT_TYPE, "image/jpeg")
},
): str,
}
)
def get_image_type(image):
"""Get the format of downloaded bytes that could be an image."""
fmt = imghdr.what(None, h=image)
fmt = None
imagefile = io.BytesIO(image)
with contextlib.suppress(PIL.UnidentifiedImageError):
img = PIL.Image.open(imagefile)
fmt = img.format.lower()
if fmt is None:
# if imghdr can't figure it out, could be svg.
# if PIL can't figure it out, could be svg.
with contextlib.suppress(UnicodeDecodeError):
if image.decode("utf-8").startswith("<svg"):
if image.decode("utf-8").lstrip().startswith("<svg"):
return "svg+xml"
return fmt
@@ -123,14 +143,14 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]:
"""Verify that the still image is valid before we create an entity."""
fmt = None
if not (url := info.get(CONF_STILL_IMAGE_URL)):
return {}, None
return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg")
if not isinstance(url, template_helper.Template) and url:
url = cv.template(url)
url.hass = hass
try:
url = url.async_render(parse_result=False)
except TemplateError as err:
_LOGGER.error("Error parsing template %s: %s", url, err)
_LOGGER.warning("Problem rendering template %s: %s", url, err)
return {CONF_STILL_IMAGE_URL: "template_error"}, None
verify_ssl = info.get(CONF_VERIFY_SSL)
auth = generate_auth(info)
@@ -222,6 +242,11 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
"""Initialize Generic ConfigFlow."""
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
@staticmethod
def async_get_options_flow(
config_entry: ConfigEntry,
@@ -232,8 +257,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
def check_for_existing(self, options):
"""Check whether an existing entry is using the same URLs."""
return any(
entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL]
and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE]
entry.options.get(CONF_STILL_IMAGE_URL) == options.get(CONF_STILL_IMAGE_URL)
and entry.options.get(CONF_STREAM_SOURCE) == options.get(CONF_STREAM_SOURCE)
for entry in self._async_current_entries()
)
@@ -258,10 +283,17 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
if not errors:
user_input[CONF_CONTENT_TYPE] = still_format
user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=name, data={}, options=user_input
)
if user_input.get(CONF_STILL_IMAGE_URL):
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=name, data={}, options=user_input
)
# If user didn't specify a still image URL,
# we can't (yet) autodetect it from the stream.
# Show a conditional 2nd page to ask them the content type.
self.cached_user_input = user_input
self.cached_title = name
return await self.async_step_content_type()
else:
user_input = DEFAULT_DATA.copy()
@@ -271,13 +303,28 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_content_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user's choice for stream content_type."""
if user_input is not None:
user_input = self.cached_user_input | user_input
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(
title=self.cached_title, data={}, options=user_input
)
return self.async_show_form(
step_id="content_type",
data_schema=build_schema_content_type({}),
errors={},
)
async def async_step_import(self, import_config) -> FlowResult:
"""Handle config import from yaml."""
# abort if we've already got this one.
if self.check_for_existing(import_config):
return self.async_abort(reason="already_exists")
errors, still_format = await async_test_still(self.hass, import_config)
errors = errors | await async_test_stream(self.hass, import_config)
# Don't bother testing the still or stream details on yaml import.
still_url = import_config.get(CONF_STILL_IMAGE_URL)
stream_url = import_config.get(CONF_STREAM_SOURCE)
name = import_config.get(
@@ -285,15 +332,10 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
)
if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config:
import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
if not errors:
import_config[CONF_CONTENT_TYPE] = still_format
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(title=name, data={}, options=import_config)
_LOGGER.error(
"Error importing generic IP camera platform config: unexpected error '%s'",
list(errors.values()),
)
return self.async_abort(reason="unknown")
still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg")
import_config[CONF_CONTENT_TYPE] = still_format
await self.async_set_unique_id(self.flow_id)
return self.async_create_entry(title=name, data={}, options=import_config)
class GenericOptionsFlowHandler(OptionsFlow):
@@ -302,6 +344,8 @@ class GenericOptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Generic IP Camera options flow."""
self.config_entry = config_entry
self.cached_user_input: dict[str, Any] = {}
self.cached_title = ""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -310,29 +354,52 @@ class GenericOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
if user_input is not None:
errors, still_format = await async_test_still(self.hass, user_input)
errors, still_format = await async_test_still(
self.hass, self.config_entry.options | user_input
)
errors = errors | await async_test_stream(self.hass, user_input)
still_url = user_input.get(CONF_STILL_IMAGE_URL)
stream_url = user_input.get(CONF_STREAM_SOURCE)
if not errors:
return self.async_create_entry(
title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME,
data={
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
CONF_CONTENT_TYPE: still_format,
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
CONF_LIMIT_REFETCH_TO_URL_CHANGE
],
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
},
)
title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
data = {
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
CONF_CONTENT_TYPE: still_format
or self.config_entry.options.get(CONF_CONTENT_TYPE),
CONF_USERNAME: user_input.get(CONF_USERNAME),
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
CONF_LIMIT_REFETCH_TO_URL_CHANGE
],
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
}
if still_url:
return self.async_create_entry(
title=title,
data=data,
)
self.cached_title = title
self.cached_user_input = data
return await self.async_step_content_type()
return self.async_show_form(
step_id="init",
data_schema=build_schema(user_input or self.config_entry.options, True),
errors=errors,
)
async def async_step_content_type(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user's choice for stream content_type."""
if user_input is not None:
user_input = self.cached_user_input | user_input
return self.async_create_entry(title=self.cached_title, data=user_input)
return self.async_show_form(
step_id="content_type",
data_schema=build_schema_content_type(self.cached_user_input),
errors={},
)

View File

@@ -2,7 +2,7 @@
"domain": "generic",
"name": "Generic Camera",
"config_flow": true,
"requirements": ["av==9.0.0"],
"requirements": ["ha-av==9.1.1-3", "pillow==9.0.1"],
"documentation": "https://www.home-assistant.io/integrations/generic",
"codeowners": ["@davet2001"],
"iot_class": "local_push"

View File

@@ -30,11 +30,16 @@
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"content_type": {
"description": "Specify the content type for the stream.",
"data": {
"content_type": "Content Type"
}
},
"confirm": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
@@ -51,10 +56,15 @@
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]",
"content_type": "[%key:component::generic::config::step::user::data::content_type%]",
"framerate": "[%key:component::generic::config::step::user::data::framerate%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
},
"content_type": {
"description": "[%key:component::generic::config::step::content_type::description%]",
"data": {
"content_type": "[%key:component::generic::config::step::content_type::data::content_type%]"
}
}
},
"error": {

View File

@@ -23,10 +23,15 @@
"confirm": {
"description": "Do you want to start set up?"
},
"content_type": {
"data": {
"content_type": "Content Type"
},
"description": "Specify the content type for the stream."
},
"user": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",
@@ -57,10 +62,15 @@
"unknown": "Unexpected error"
},
"step": {
"content_type": {
"data": {
"content_type": "Content Type"
},
"description": "Specify the content type for the stream."
},
"init": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",

View File

@@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import logging
from typing import Any
import aiohttp
from httplib2.error import ServerNotFoundError
from oauth2client.file import Storage
import voluptuous as vol
@@ -24,7 +25,11 @@ from homeassistant.const import (
CONF_OFFSET,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import config_entry_oauth2_flow
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -185,8 +190,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry
)
)
assert isinstance(implementation, DeviceAuth)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
# Force a token refresh to fix a bug where tokens were persisted with
# expires_in (relative time delta) and expires_at (absolute time) swapped.
# A google session token typically only lasts a few days between refresh.
now = datetime.now()
if session.token["expires_at"] >= (now + timedelta(days=365)).timestamp():
session.token["expires_in"] = 0
session.token["expires_at"] = now.timestamp()
try:
await session.async_ensure_token_valid()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope
if required_scope not in session.token.get("scope", []):
raise ConfigEntryAuthFailed(

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable
import datetime
import logging
import time
from typing import Any
from googleapiclient import discovery as google_discovery
@@ -58,7 +59,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
"refresh_token": creds.refresh_token,
"scope": " ".join(creds.scopes),
"token_type": "Bearer",
"expires_in": creds.token_expiry.timestamp(),
"expires_in": creds.token_expiry.timestamp() - time.time(),
}
@@ -157,16 +158,16 @@ def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentia
client_id=conf[CONF_CLIENT_ID],
client_secret=conf[CONF_CLIENT_SECRET],
refresh_token=token["refresh_token"],
token_expiry=token["expires_at"],
token_expiry=datetime.datetime.fromtimestamp(token["expires_at"]),
token_uri=oauth2client.GOOGLE_TOKEN_URI,
scopes=[conf[CONF_CALENDAR_ACCESS].scope],
user_agent=None,
)
def _api_time_format(time: datetime.datetime | None) -> str | None:
def _api_time_format(date_time: datetime.datetime | None) -> str | None:
"""Convert a datetime to the api string format."""
return time.isoformat("T") if time else None
return date_time.isoformat("T") if date_time else None
class GoogleCalendarService:
@@ -183,9 +184,13 @@ class GoogleCalendarService:
"""Get the calendar service with valid credetnails."""
await self._session.async_ensure_token_valid()
creds = _async_google_creds(self._hass, self._session.token)
return google_discovery.build(
"calendar", "v3", credentials=creds, cache_discovery=False
)
def _build() -> google_discovery.Resource:
return google_discovery.build(
"calendar", "v3", credentials=creds, cache_discovery=False
)
return await self._hass.async_add_executor_job(_build)
async def async_list_calendars(
self,

View File

@@ -183,7 +183,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
valid_items = filter(self._event_filter, items)
self._event = copy.deepcopy(next(valid_items, None))
if self._event:
(summary, offset) = extract_offset(self._event["summary"], self._offset)
(summary, offset) = extract_offset(
self._event.get("summary", ""), self._offset
)
self._event["summary"] = summary
self._offset_reached = is_offset_reached(
get_date(self._event["start"]), offset

View File

@@ -34,7 +34,7 @@ class OAuth2FlowHandler(
return logging.getLogger(__name__)
async def async_step_import(self, info: dict[str, Any]) -> FlowResult:
"""Import existing auth from Nest."""
"""Import existing auth into a new config entry."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
implementations = await config_entry_oauth2_flow.async_get_implementations(

View File

@@ -6,7 +6,7 @@
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Nest integration needs to re-authenticate your account"
"description": "The Google Calendar integration needs to re-authenticate your account"
},
"auth": {
"title": "Link Google Account"

View File

@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Account is already configured",
"already_in_progress": "Configuration flow is already in progress",
"code_expired": "Authentication code expired, please try again.",
"code_expired": "Authentication code expired or credential setup is invalid, please try again.",
"invalid_access_token": "Invalid access token",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth_error": "Received invalid token data.",
@@ -23,7 +23,7 @@
"title": "Pick Authentication Method"
},
"reauth_confirm": {
"description": "The Nest integration needs to re-authenticate your account",
"description": "The Google Calendar integration needs to re-authenticate your account",
"title": "Reauthenticate Integration"
}
}

View File

@@ -454,7 +454,7 @@ class GroupEntity(Entity):
self.async_update_group_state()
self.async_write_ha_state()
start.async_at_start(self.hass, _update_at_start)
self.async_on_remove(start.async_at_start(self.hass, _update_at_start))
@callback
def async_defer_or_update_ha_state(self) -> None:
@@ -689,7 +689,7 @@ class Group(Entity):
async def async_added_to_hass(self):
"""Handle addition to Home Assistant."""
start.async_at_start(self.hass, self._async_start)
self.async_on_remove(start.async_at_start(self.hass, self._async_start))
async def async_will_remove_from_hass(self):
"""Handle removal from Home Assistant."""

View File

@@ -10,11 +10,11 @@ import voluptuous as vol
from homeassistant.const import CONF_ENTITIES
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er, selector
from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler,
HelperFlowFormStep,
HelperFlowMenuStep,
HelperOptionsFlowHandler,
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
SchemaFlowMenuStep,
SchemaOptionsFlowHandler,
entity_selector_without_own_entities,
)
@@ -25,11 +25,11 @@ from .const import CONF_HIDE_MEMBERS
def basic_group_options_schema(
domain: str,
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
options: dict[str, Any],
) -> vol.Schema:
"""Generate options schema."""
handler = cast(HelperOptionsFlowHandler, handler)
handler = cast(SchemaOptionsFlowHandler, handler)
return vol.Schema(
{
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
@@ -58,7 +58,7 @@ def basic_group_config_schema(domain: str) -> vol.Schema:
def binary_sensor_options_schema(
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
options: dict[str, Any],
) -> vol.Schema:
"""Generate options schema."""
@@ -78,7 +78,7 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend(
def light_switch_options_schema(
domain: str,
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
options: dict[str, Any],
) -> vol.Schema:
"""Generate options schema."""
@@ -119,45 +119,45 @@ def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]
return _set_group_type
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"user": HelperFlowMenuStep(GROUP_TYPES),
"binary_sensor": HelperFlowFormStep(
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowMenuStep(GROUP_TYPES),
"binary_sensor": SchemaFlowFormStep(
BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor")
),
"cover": HelperFlowFormStep(
"cover": SchemaFlowFormStep(
basic_group_config_schema("cover"), set_group_type("cover")
),
"fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
"light": HelperFlowFormStep(
"fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
"light": SchemaFlowFormStep(
basic_group_config_schema("light"), set_group_type("light")
),
"lock": HelperFlowFormStep(
"lock": SchemaFlowFormStep(
basic_group_config_schema("lock"), set_group_type("lock")
),
"media_player": HelperFlowFormStep(
"media_player": SchemaFlowFormStep(
basic_group_config_schema("media_player"), set_group_type("media_player")
),
"switch": HelperFlowFormStep(
"switch": SchemaFlowFormStep(
basic_group_config_schema("switch"), set_group_type("switch")
),
}
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"init": HelperFlowFormStep(None, next_step=choose_options_step),
"binary_sensor": HelperFlowFormStep(binary_sensor_options_schema),
"cover": HelperFlowFormStep(partial(basic_group_options_schema, "cover")),
"fan": HelperFlowFormStep(partial(basic_group_options_schema, "fan")),
"light": HelperFlowFormStep(partial(light_switch_options_schema, "light")),
"lock": HelperFlowFormStep(partial(basic_group_options_schema, "lock")),
"media_player": HelperFlowFormStep(
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"init": SchemaFlowFormStep(None, next_step=choose_options_step),
"binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema),
"cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")),
"fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")),
"light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")),
"lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")),
"media_player": SchemaFlowFormStep(
partial(basic_group_options_schema, "media_player")
),
"switch": HelperFlowFormStep(partial(light_switch_options_schema, "switch")),
"switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")),
}
class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow for groups."""
config_flow = CONFIG_FLOW

View File

@@ -1,9 +1,9 @@
{
"domain": "hangouts",
"name": "Google Hangouts",
"name": "Google Chat",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hangouts",
"requirements": ["hangups==0.4.17"],
"requirements": ["hangups==0.4.18"],
"codeowners": [],
"iot_class": "cloud_push",
"loggers": ["hangups", "urwid"]

View File

@@ -16,7 +16,7 @@
"password": "[%key:common::config_flow::data::password%]",
"authorization_code": "Authorization Code (required for manual authentication)"
},
"title": "Google Hangouts Login"
"title": "Google Chat Login"
},
"2fa": {
"data": {

View File

@@ -22,7 +22,7 @@
"email": "Email",
"password": "Password"
},
"title": "Google Hangouts Login"
"title": "Google Chat Login"
}
}
}

View File

@@ -42,7 +42,7 @@ from homeassistant.helpers.device_registry import (
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.loader import bind_hass
from homeassistant.util.dt import utcnow
@@ -51,6 +51,7 @@ from .auth import async_setup_auth_view
from .const import (
ATTR_ADDON,
ATTR_ADDONS,
ATTR_AUTO_UPDATE,
ATTR_CHANGELOG,
ATTR_DISCOVERY,
ATTR_FOLDERS,
@@ -98,6 +99,7 @@ DATA_INFO = "hassio_info"
DATA_OS_INFO = "hassio_os_info"
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
DATA_ADDONS_INFO = "hassio_addons_info"
DATA_ADDONS_STATS = "hassio_addons_stats"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
@@ -422,6 +424,16 @@ def get_supervisor_info(hass):
return hass.data.get(DATA_SUPERVISOR_INFO)
@callback
@bind_hass
def get_addons_info(hass):
"""Return Addons info.
Async friendly.
"""
return hass.data.get(DATA_ADDONS_INFO)
@callback
@bind_hass
def get_addons_stats(hass):
@@ -597,16 +609,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
DOMAIN, service, async_service_handler, schema=settings.schema
)
async def update_addon_stats(slug):
"""Update single addon stats."""
stats = await hassio.get_addon_stats(slug)
return (slug, stats)
async def update_addon_changelog(slug):
"""Return the changelog for an add-on."""
changelog = await hassio.get_addon_changelog(slug)
return (slug, changelog)
async def update_info_data(now):
"""Update last available supervisor information."""
@@ -627,23 +629,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
hassio.get_os_info(),
)
addons = [
addon
for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", [])
if addon[ATTR_STATE] == ATTR_STARTED
]
stats_data = await asyncio.gather(
*[update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
)
hass.data[DATA_ADDONS_STATS] = dict(stats_data)
hass.data[DATA_ADDONS_CHANGELOGS] = dict(
await asyncio.gather(
*[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
)
)
if ADDONS_COORDINATOR in hass.data:
await hass.data[ADDONS_COORDINATOR].async_refresh()
except HassioAPIError as err:
_LOGGER.warning("Can't read Supervisor data: %s", err)
@@ -726,7 +711,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
dev_reg = await async_get_registry(hass)
coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
hass.data[ADDONS_COORDINATOR] = coordinator
await coordinator.async_refresh()
await coordinator.async_config_entry_first_refresh()
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@@ -833,18 +818,24 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
hass,
_LOGGER,
name=DOMAIN,
update_method=self._async_update_data,
update_interval=HASSIO_UPDATE_INTERVAL,
)
self.hassio: HassIO = hass.data[DOMAIN]
self.data = {}
self.entry_id = config_entry.entry_id
self.dev_reg = dev_reg
self.is_hass_os = "hassos" in get_info(self.hass)
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
await self.force_data_refresh()
except HassioAPIError as err:
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
new_data = {}
supervisor_info = get_supervisor_info(self.hass)
addons_info = get_addons_info(self.hass)
addons_stats = get_addons_stats(self.hass)
addons_changelogs = get_addons_changelogs(self.hass)
store_data = get_store(self.hass)
@@ -857,7 +848,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
new_data[DATA_KEY_ADDONS] = {
addon[ATTR_SLUG]: {
**addon,
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
**((addons_stats or {}).get(addon[ATTR_SLUG]) or {}),
ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get(
ATTR_AUTO_UPDATE, False
),
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
ATTR_REPOSITORY: repositories.get(
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
@@ -897,6 +891,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
async_remove_addons_from_dev_reg(self.dev_reg, stale_addons)
if not self.is_hass_os and (
dev := self.dev_reg.async_get_device({(DOMAIN, "OS")})
):
# Remove the OS device if it exists and the installation is not hassos
self.dev_reg.async_remove_device(dev.id)
# If there are new add-ons, we should reload the config entry so we can
# create new devices and entities. We can return an empty dict because
# coordinator will be recreated.
@@ -914,3 +914,65 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
"""Force update of the supervisor info."""
self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
await self.async_refresh()
async def force_data_refresh(self) -> None:
"""Force update of the addon info."""
await self.hassio.refresh_updates()
(
self.hass.data[DATA_INFO],
self.hass.data[DATA_CORE_INFO],
self.hass.data[DATA_SUPERVISOR_INFO],
self.hass.data[DATA_OS_INFO],
) = await asyncio.gather(
self.hassio.get_info(),
self.hassio.get_core_info(),
self.hassio.get_supervisor_info(),
self.hassio.get_os_info(),
)
addons = [
addon
for addon in self.hass.data[DATA_SUPERVISOR_INFO].get("addons", [])
if addon[ATTR_STATE] == ATTR_STARTED
]
stats_data = await asyncio.gather(
*[self._update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
)
self.hass.data[DATA_ADDONS_STATS] = dict(stats_data)
self.hass.data[DATA_ADDONS_CHANGELOGS] = dict(
await asyncio.gather(
*[self._update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
)
)
self.hass.data[DATA_ADDONS_INFO] = dict(
await asyncio.gather(
*[self._update_addon_info(addon[ATTR_SLUG]) for addon in addons]
)
)
async def _update_addon_stats(self, slug):
"""Update single addon stats."""
try:
stats = await self.hassio.get_addon_stats(slug)
return (slug, stats)
except HassioAPIError as err:
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
return (slug, None)
async def _update_addon_changelog(self, slug):
"""Return the changelog for an add-on."""
try:
changelog = await self.hassio.get_addon_changelog(slug)
return (slug, changelog)
except HassioAPIError as err:
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
return (slug, None)
async def _update_addon_info(self, slug):
"""Return the info for an add-on."""
try:
info = await self.hassio.get_addon_info(slug)
return (slug, info)
except HassioAPIError as err:
_LOGGER.warning("Could not fetch info for %s: %s", slug, err)
return (slug, None)

View File

@@ -39,6 +39,7 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
EVENT_SUPERVISOR_EVENT = "supervisor_event"
ATTR_AUTO_UPDATE = "auto_update"
ATTR_VERSION = "version"
ATTR_VERSION_LATEST = "version_latest"
ATTR_UPDATE_AVAILABLE = "update_available"

View File

@@ -90,7 +90,7 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Return True if entity is available."""
return (
super().available
and DATA_KEY_OS in self.coordinator.data
and DATA_KEY_SUPERVISOR in self.coordinator.data
and self.entity_description.key
in self.coordinator.data[DATA_KEY_SUPERVISOR]
)

View File

@@ -168,6 +168,14 @@ class HassIO:
"""
return self.send_command("/homeassistant/stop")
@_api_bool
def refresh_updates(self):
"""Refresh available updates.
This method return a coroutine.
"""
return self.send_command("/refresh_updates", timeout=None)
@api_data
def retrieve_discovery_messages(self):
"""Return all discovery data from Hass.io API.

View File

@@ -24,6 +24,7 @@ from . import (
async_update_supervisor,
)
from .const import (
ATTR_AUTO_UPDATE,
ATTR_CHANGELOG,
ATTR_VERSION,
ATTR_VERSION_LATEST,
@@ -99,6 +100,11 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
"""Return the add-on data."""
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug]
@property
def auto_update(self):
"""Return true if auto-update is enabled for the add-on."""
return self._addon_data[ATTR_AUTO_UPDATE]
@property
def title(self) -> str | None:
"""Return the title of the update."""
@@ -110,8 +116,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
return self._addon_data[ATTR_VERSION_LATEST]
@property
def current_version(self) -> str | None:
"""Version currently in use."""
def installed_version(self) -> str | None:
"""Version installed and in use."""
return self._addon_data[ATTR_VERSION]
@property
@@ -133,9 +139,12 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
return None
if f"# {self.latest_version}" in notes and f"# {self.current_version}" in notes:
if (
f"# {self.latest_version}" in notes
and f"# {self.installed_version}" in notes
):
# Split the release notes to only what is between the versions if we can
new_notes = notes.split(f"# {self.current_version}")[0]
new_notes = notes.split(f"# {self.installed_version}")[0]
if f"# {self.latest_version}" in new_notes:
# Make sure the latest version is still there.
# This can be False if the order of the release notes are not correct
@@ -176,7 +185,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
def installed_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION]
@@ -210,6 +219,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Supervisor."""
_attr_auto_update = True
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_title = "Home Assistant Supervisor"
@@ -219,7 +229,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
def installed_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION]
@@ -264,7 +274,7 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST]
@property
def current_version(self) -> str:
def installed_version(self) -> str:
"""Return native value of entity."""
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION]

View File

@@ -247,7 +247,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity):
async def _update_at_start(_):
await self.async_update()
async_at_start(self.hass, _update_at_start)
self.async_on_remove(async_at_start(self.hass, _update_at_start))
@property
def native_value(self) -> str | None:

View File

@@ -312,7 +312,6 @@ class Dishwasher(
"""Dishwasher class."""
PROGRAMS = [
{"name": "Dishcare.Dishwasher.Program.PreRinse"},
{"name": "Dishcare.Dishwasher.Program.Auto1"},
{"name": "Dishcare.Dishwasher.Program.Auto2"},
{"name": "Dishcare.Dishwasher.Program.Auto3"},

View File

@@ -466,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
all_supported_entities = _async_get_matching_entities(self.hass, domains)
all_supported_entities = _async_get_matching_entities(
self.hass, domains, include_entity_category=True
)
# In accessory mode we can only have one
default_value = next(
iter(
@@ -505,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
entity_filter = self.hk_options.get(CONF_FILTER, {})
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
all_supported_entities = _async_get_matching_entities(self.hass, domains)
all_supported_entities = _async_get_matching_entities(
self.hass, domains, include_entity_category=True
)
if not entities:
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
# Strip out entities that no longer exist to prevent error in the UI
@@ -559,21 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
all_supported_entities = _async_get_matching_entities(self.hass, domains)
if not entities:
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
ent_reg = entity_registry.async_get(self.hass)
excluded_entities = set()
for entity_id in all_supported_entities:
if ent_reg_ent := ent_reg.async_get(entity_id):
if (
ent_reg_ent.entity_category is not None
or ent_reg_ent.hidden_by is not None
):
excluded_entities.add(entity_id)
# Remove entity category entities since we will exclude them anyways
all_supported_entities = {
k: v
for k, v in all_supported_entities.items()
if k not in excluded_entities
}
# Strip out entities that no longer exist to prevent error in the UI
default_value = [
entity_id for entity_id in entities if entity_id in all_supported_entities
@@ -652,16 +642,37 @@ async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
def _exclude_by_entity_registry(
ent_reg: entity_registry.EntityRegistry,
entity_id: str,
include_entity_category: bool,
) -> bool:
"""Filter out hidden entities and ones with entity category (unless specified)."""
return bool(
(entry := ent_reg.async_get(entity_id))
and (
entry.hidden_by is not None
or (not include_entity_category and entry.entity_category is not None)
)
)
def _async_get_matching_entities(
hass: HomeAssistant, domains: list[str] | None = None
hass: HomeAssistant,
domains: list[str] | None = None,
include_entity_category: bool = False,
) -> dict[str, str]:
"""Fetch all entities or entities in the given domains."""
ent_reg = entity_registry.async_get(hass)
return {
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
for state in sorted(
hass.states.async_all(domains and set(domains)),
key=lambda item: item.entity_id,
)
if not _exclude_by_entity_registry(
ent_reg, state.entity_id, include_entity_category
)
}

View File

@@ -63,7 +63,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit BO sensor."""
_attr_device_class = BinarySensorDeviceClass.GAS
_attr_device_class = BinarySensorDeviceClass.CO
def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking."""

View File

@@ -293,7 +293,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(updates=updated_ip_port)
for progress in self._async_in_progress(include_uninitialized=True):
if progress["context"].get("unique_id") == normalized_hkid:
context = progress["context"]
if context.get("unique_id") == normalized_hkid and not context.get(
"pairing"
):
if paired:
# If the device gets paired, we want to dismiss
# an existing discovery since we can no longer
@@ -350,6 +353,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self._async_setup_controller()
if pair_info and self.finish_pairing:
self.context["pairing"] = True
code = pair_info["pairing_code"]
try:
code = ensure_pin_format(

View File

@@ -81,6 +81,10 @@ async def async_setup_devices(bridge: "HueBridge"):
dev_reg, entry.entry_id
):
if device not in known_devices:
# handle case where a virtual device was created for a Hue group
hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN)
if hue_dev_id in api.groups:
continue
dev_reg.async_remove_device(device.id)
# add listener for updates on Hue devices controller

View File

@@ -194,7 +194,7 @@ class HueLight(HueBaseEntity, LightEntity):
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
effect = effect_str = kwargs.get(ATTR_EFFECT)
if effect_str == EFFECT_NONE:
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
effect = EffectStatus.NO_EFFECT
elif effect_str is not None:
# work out if we got a regular effect or timed effect

View File

@@ -0,0 +1,16 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from . import ATTR_AVAILABLE_MODES, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static attributes from being recorded in the database."""
return {
ATTR_MIN_HUMIDITY,
ATTR_MAX_HUMIDITY,
ATTR_AVAILABLE_MODES,
}

View File

@@ -1,13 +1,10 @@
"""The iCloud component."""
import logging
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from .account import IcloudAccount
@@ -15,9 +12,6 @@ from .const import (
CONF_GPS_ACCURACY_THRESHOLD,
CONF_MAX_INTERVAL,
CONF_WITH_FAMILY,
DEFAULT_GPS_ACCURACY_THRESHOLD,
DEFAULT_MAX_INTERVAL,
DEFAULT_WITH_FAMILY,
DOMAIN,
PLATFORMS,
STORAGE_KEY,
@@ -69,47 +63,7 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema(
}
)
ACCOUNT_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_WITH_FAMILY, default=DEFAULT_WITH_FAMILY): cv.boolean,
vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int,
vol.Optional(
CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD
): cv.positive_int,
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))},
extra=vol.ALLOW_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up iCloud from legacy config file."""
if (conf := config.get(DOMAIN)) is None:
return True
# Note: need to remember to cleanup device_tracker (remove async_setup_scanner)
_LOGGER.warning(
"Configuration of the iCloud integration in YAML is deprecated and "
"will be removed in Home Assistant 2022.4; Your existing configuration "
"has been imported into the UI automatically and can be safely removed "
"from your configuration.yaml file"
)
for account_conf in conf:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf
)
)
return True
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -172,10 +172,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._validate_and_create_entry(user_input, "user")
async def async_step_import(self, user_input):
"""Import a config entry."""
return await self.async_step_user(user_input)
async def async_step_reauth(self, user_input=None):
"""Update password for a config entry that can't authenticate."""
# Store existing entry data so it can be used later and set unique ID

View File

@@ -1,5 +1,6 @@
{
"domain": "input_boolean",
"integration_type": "helper",
"name": "Input Boolean",
"documentation": "https://www.home-assistant.io/integrations/input_boolean",
"codeowners": ["@home-assistant/core"],

View File

@@ -1,5 +1,6 @@
{
"domain": "input_button",
"integration_type": "helper",
"name": "Input Button",
"documentation": "https://www.home-assistant.io/integrations/input_button",
"codeowners": ["@home-assistant/core"],

View File

@@ -1,5 +1,6 @@
{
"domain": "input_datetime",
"integration_type": "helper",
"name": "Input Datetime",
"documentation": "https://www.home-assistant.io/integrations/input_datetime",
"codeowners": ["@home-assistant/core"],

View File

@@ -1,5 +1,6 @@
{
"domain": "input_number",
"integration_type": "helper",
"name": "Input Number",
"documentation": "https://www.home-assistant.io/integrations/input_number",
"codeowners": ["@home-assistant/core"],

View File

@@ -1,5 +1,6 @@
{
"domain": "input_select",
"integration_type": "helper",
"name": "Input Select",
"documentation": "https://www.home-assistant.io/integrations/input_select",
"codeowners": ["@home-assistant/core"],

View File

@@ -1,5 +1,6 @@
{
"domain": "input_text",
"integration_type": "helper",
"name": "Input Text",
"documentation": "https://www.home-assistant.io/integrations/input_text",
"codeowners": ["@home-assistant/core"],

View File

@@ -16,10 +16,10 @@ from homeassistant.const import (
TIME_SECONDS,
)
from homeassistant.helpers import selector
from homeassistant.helpers.helper_config_entry_flow import (
HelperConfigFlowHandler,
HelperFlowFormStep,
HelperFlowMenuStep,
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
SchemaFlowFormStep,
SchemaFlowMenuStep,
)
from .const import (
@@ -37,8 +37,8 @@ UNIT_PREFIXES = [
{"value": "none", "label": "none"},
{"value": "k", "label": "k (kilo)"},
{"value": "M", "label": "M (mega)"},
{"value": "G", "label": "T (tera)"},
{"value": "T", "label": "P (peta)"},
{"value": "G", "label": "G (giga)"},
{"value": "T", "label": "T (tera)"},
]
TIME_UNITS = [
{"value": TIME_SECONDS, "label": "s (seconds)"},
@@ -83,21 +83,21 @@ CONFIG_SCHEMA = vol.Schema(
{"select": {"options": UNIT_PREFIXES}}
),
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector(
{"select": {"options": TIME_UNITS}}
{"select": {"options": TIME_UNITS, "mode": "dropdown"}}
),
}
)
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"user": HelperFlowFormStep(CONFIG_SCHEMA)
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
}
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
}
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config or options flow for Integration."""
config_flow = CONFIG_FLOW

View File

@@ -253,8 +253,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
self._state = integral
self.async_write_ha_state()
async_track_state_change_event(
self.hass, [self._sensor_source_id], calc_integration
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._sensor_source_id], calc_integration
)
)
@property

View File

@@ -1,26 +1,34 @@
{
"title": "Integration - Riemann sum integral sensor",
"config": {
"step": {
"user": {
"title": "New Integration sensor",
"description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.",
"title": "Add Riemann sum integral sensor",
"description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.",
"data": {
"method": "Integration method",
"name": "Name",
"round": "Precision",
"source": "Input sensor",
"unit_prefix": "Metric prefix",
"unit_time": "Integration time"
"unit_time": "Time unit"
},
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"unit_prefix": "The output will be scaled according to the selected metric prefix.",
"unit_time": "The output will be scaled according to the selected time unit."
}
}
}
},
"options": {
"step": {
"options": {
"description": "Precision controls the number of decimal digits in the output.",
"init": {
"data": {
"round": "[%key:component::integration::config::step::user::data::round%]"
},
"data_description": {
"round": "[%key:component::integration::config::step::user::data_description::round%]"
}
}
}

View File

@@ -8,21 +8,29 @@
"round": "Precision",
"source": "Input sensor",
"unit_prefix": "Metric prefix",
"unit_time": "Integration time"
"unit_time": "Time unit"
},
"description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.",
"title": "New Integration sensor"
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"unit_prefix": "The output will be scaled according to the selected metric prefix.",
"unit_time": "The output will be scaled according to the selected time unit."
},
"description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.",
"title": "Add Riemann sum integral sensor"
}
}
},
"options": {
"step": {
"options": {
"init": {
"data": {
"round": "Precision"
},
"description": "Precision controls the number of decimal digits in the output."
"data_description": {
"round": "Controls the number of decimal digits in the output."
}
}
}
}
},
"title": "Integration - Riemann sum integral sensor"
}

View File

@@ -2,7 +2,7 @@
"domain": "isy994",
"name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.5"],
"requirements": ["pyisy==3.0.6"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

View File

@@ -296,9 +296,3 @@ class KNXClimate(KnxEntity, ClimateEntity):
await super().async_added_to_hass()
if self._device.mode is not None:
self._device.mode.register_device_updated_cb(self.after_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
await super().async_will_remove_from_hass()
if self._device.mode is not None:
self._device.mode.unregister_device_updated_cb(self.after_update_callback)

View File

@@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import selector
from homeassistant.helpers.storage import STORAGE_DIR
from .const import (
@@ -63,6 +63,13 @@ CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure"
CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP"
CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode"
_IA_SELECTOR = selector.selector({"text": {}})
_IP_SELECTOR = selector.selector({"text": {}})
_PORT_SELECTOR = vol.All(
selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}),
vol.Coerce(int),
)
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a KNX config flow."""
@@ -164,7 +171,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
**DEFAULT_ENTRY_DATA, # type: ignore[misc]
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[CONF_KNX_INDIVIDUAL_ADDRESS],
CONF_KNX_ROUTE_BACK: (
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
),
@@ -202,18 +208,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
port = self._selected_tunnel.port
if not self._selected_tunnel.supports_tunnelling_tcp:
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP)
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE)
fields = {
vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods),
vol.Required(CONF_HOST, default=ip_address): str,
vol.Required(CONF_PORT, default=port): cv.port,
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
): str,
vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR,
vol.Required(CONF_PORT, default=port): _PORT_SELECTOR,
}
if self.show_advanced_options:
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
return self.async_show_form(
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
@@ -245,9 +249,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
fields = {
vol.Required(CONF_KNX_SECURE_USER_ID): int,
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): str,
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): str,
vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All(
selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}),
vol.Coerce(int),
),
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector(
{"text": {"type": "password"}}
),
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector(
{"text": {"type": "password"}}
),
}
return self.async_show_form(
@@ -290,8 +301,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "file_not_found"
fields = {
vol.Required(CONF_KNX_KNXKEY_FILENAME): str,
vol.Required(CONF_KNX_KNXKEY_PASSWORD): str,
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}),
}
return self.async_show_form(
@@ -319,13 +330,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
fields = {
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
): str,
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): str,
vol.Required(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port,
): _IA_SELECTOR,
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR,
vol.Required(
CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
): _PORT_SELECTOR,
}
if self.show_advanced_options:
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
@@ -370,17 +383,17 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS,
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
): str,
): selector.selector({"text": {}}),
vol.Required(
CONF_KNX_MCAST_GRP,
default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP),
): str,
): _IP_SELECTOR,
vol.Required(
CONF_KNX_MCAST_PORT,
default=self.current_config.get(
CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT
),
): cv.port,
): _PORT_SELECTOR,
}
if self.show_advanced_options:
@@ -394,7 +407,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
CONF_KNX_LOCAL_IP,
default=local_ip,
)
] = str
] = _IP_SELECTOR
data_schema[
vol.Required(
CONF_KNX_STATE_UPDATER,
@@ -403,7 +416,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
CONF_KNX_DEFAULT_STATE_UPDATER,
),
)
] = bool
] = selector.selector({"boolean": {}})
data_schema[
vol.Required(
CONF_KNX_RATE_LIMIT,
@@ -412,7 +425,18 @@ class KNXOptionsFlowHandler(OptionsFlow):
CONF_KNX_DEFAULT_RATE_LIMIT,
),
)
] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT))
] = vol.All(
selector.selector(
{
"number": {
"min": 1,
"max": CONF_MAX_RATE_LIMIT,
"mode": "box",
}
}
),
vol.Coerce(int),
)
return self.async_show_form(
step_id="init",
@@ -444,10 +468,10 @@ class KNXOptionsFlowHandler(OptionsFlow):
): vol.In(connection_methods),
vol.Required(
CONF_HOST, default=self.current_config.get(CONF_HOST)
): str,
): _IP_SELECTOR,
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
): _PORT_SELECTOR,
}
),
last_step=True,

View File

@@ -45,4 +45,5 @@ class KnxEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.unregister_device_updated_cb(self.after_update_callback)
# will also remove callbacks
self._device.shutdown()

View File

@@ -3,7 +3,7 @@
"name": "KNX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": ["xknx==0.20.0"],
"requirements": ["xknx==0.20.2"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "silver",
"iot_class": "local_push",

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