Compare commits

...

177 Commits

Author SHA1 Message Date
Franck Nijhof 098c6d446e Merge pull request #70155 from home-assistant/rc 2022-04-16 19:42:51 +02:00
Franck Nijhof f790a343c0 Bumped version to 2022.4.5 2022-04-16 18:17:07 +02:00
G Johansson 70a4de5efe Fix StationInfo not string in Trafikverket Train (#70153) 2022-04-16 18:16:13 +02:00
J. Nick Koston 2205b63778 Ensure powerwall retries setup when api returns too many requests (#70143) 2022-04-16 18:16:09 +02:00
Brandon Rothweiler cdc979e1e6 Bump pymazda to 0.3.3 (#70136) 2022-04-16 18:16:06 +02:00
Matthias Alphart 73478dc76d update xknx to 0.20.3 (#70123) 2022-04-16 18:16:03 +02:00
Raman Gupta a9c670c56f Correct tomorrowio weather units (#70107) 2022-04-16 18:16:00 +02:00
Joakim Sørensen 410e0f52a3 Limit Supervisor refresh updates (#70075) 2022-04-16 18:15:57 +02:00
Shay Levy 0a6182264a Set source & sound mode at start in media player reproduce state (#70064) 2022-04-16 18:15:53 +02:00
Simone Chemelli 236acd6206 Fix retry when Met config entry fails (#70012) 2022-04-16 18:15:49 +02:00
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
261 changed files with 4004 additions and 1333 deletions
@@ -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"]
@@ -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",
+1 -1
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
@@ -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",
@@ -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)
@@ -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
@@ -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,
}
+2 -1
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:
@@ -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
@@ -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%]",
@@ -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"
}
@@ -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,
@@ -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:
@@ -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."""
@@ -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"],
@@ -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
@@ -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 = (
@@ -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:
@@ -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")
@@ -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")
@@ -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),
+1 -1
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,
@@ -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,
@@ -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",
@@ -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, ...] = (
@@ -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()
+12
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}
+12 -6
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
+13
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 {}
@@ -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:]
@@ -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": {
@@ -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",
@@ -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:]
@@ -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%]"
@@ -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"
@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20220401.0"],
"requirements": ["home-assistant-frontend==20220405.0"],
"dependencies": [
"api",
"auth",
+1 -1
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"
+98 -37
View File
@@ -58,7 +58,7 @@ DEFAULT_DATA = {
CONF_VERIFY_SSL: True,
}
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"}
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
def build_schema(
@@ -109,6 +109,20 @@ 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 = None
@@ -129,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)
@@ -228,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,
@@ -238,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()
)
@@ -264,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()
@@ -277,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(
@@ -291,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):
@@ -308,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
@@ -316,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={},
)
@@ -2,7 +2,7 @@
"domain": "generic",
"name": "Generic Camera",
"config_flow": true,
"requirements": ["av==9.0.0", "pillow==9.0.1"],
"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"
+12 -2
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": {
@@ -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",
+22 -2
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(
+5 -4
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:
+3 -1
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
@@ -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(
+1 -1
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"
@@ -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"
}
}
+2 -2
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."""
@@ -3,7 +3,7 @@
"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"]
+93 -43
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
@@ -609,21 +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_addon_info(slug):
"""Return the info for an add-on."""
info = await hassio.get_addon_info(slug)
return (slug, info)
async def update_info_data(now):
"""Update last available supervisor information."""
@@ -644,28 +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]
)
)
hass.data[DATA_ADDONS_INFO] = dict(
await asyncio.gather(
*[update_addon_info(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)
@@ -748,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)
@@ -855,16 +818,21 @@ 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)
@@ -880,8 +848,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
new_data[DATA_KEY_ADDONS] = {
addon[ATTR_SLUG]: {
**addon,
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get(
**((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]),
@@ -923,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.
@@ -940,3 +914,79 @@ 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."""
(
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)
async def _async_refresh(
self,
log_failures: bool = True,
raise_on_auth_failed: bool = False,
scheduled: bool = False,
) -> None:
"""Refresh data."""
if not scheduled:
# Force refreshing updates for non-scheduled updates
try:
await self.hassio.refresh_updates()
except HassioAPIError as err:
_LOGGER.warning("Error on Supervisor API: %s", err)
await super()._async_refresh(log_failures, raise_on_auth_failed, scheduled)
+1 -1
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]
)
@@ -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.
@@ -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:
@@ -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"},
@@ -652,7 +652,7 @@ def _exclude_by_entity_registry(
(entry := ent_reg.async_get(entity_id))
and (
entry.hidden_by is not None
or (not include_entity_category or entry.entity_category is not None)
or (not include_entity_category and entry.entity_category is not None)
)
)
@@ -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."""
@@ -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(
@@ -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,
}
@@ -39,7 +39,6 @@ UNIT_PREFIXES = [
{"value": "M", "label": "M (mega)"},
{"value": "G", "label": "G (giga)"},
{"value": "T", "label": "T (tera)"},
{"value": "P", "label": "P (peta)"},
]
TIME_UNITS = [
{"value": TIME_SECONDS, "label": "s (seconds)"},
@@ -84,7 +83,7 @@ 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"}}
),
}
)
@@ -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
@@ -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%]"
}
}
}
@@ -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"
}
@@ -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": [
-6
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)
+2 -1
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()
+1 -1
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.3"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "silver",
"iot_class": "local_push",
@@ -448,7 +448,8 @@ class KodiEntity(MediaPlayerEntity):
self._connect_error = True
_LOGGER.warning("Unable to connect to Kodi via websocket")
await self._clear_connection(False)
self._connect_error = False
else:
self._connect_error = False
async def _ping(self):
try:
@@ -458,7 +459,8 @@ class KodiEntity(MediaPlayerEntity):
self._connect_error = True
_LOGGER.warning("Unable to ping Kodi via websocket")
await self._clear_connection()
self._connect_error = False
else:
self._connect_error = False
async def _async_connect_websocket_if_disconnected(self, *_):
"""Reconnect the websocket if it fails."""
@@ -0,0 +1,22 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from . import (
ATTR_EFFECT_LIST,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_SUPPORTED_COLOR_MODES,
)
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static attributes from being recorded in the database."""
return {
ATTR_SUPPORTED_COLOR_MODES,
ATTR_EFFECT_LIST,
ATTR_MIN_MIREDS,
ATTR_MAX_MIREDS,
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Mazda Connected Services",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mazda",
"requirements": ["pymazda==0.3.2"],
"requirements": ["pymazda==0.3.3"],
"codeowners": ["@bdr99"],
"quality_scale": "platinum",
"iot_class": "cloud_polling",
@@ -70,6 +70,7 @@ from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F40
from .const import ( # noqa: F401
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_ENTITY_PICTURE_LOCAL,
ATTR_GROUP_MEMBERS,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
@@ -936,7 +937,7 @@ class MediaPlayerEntity(Entity):
state_attr[attr] = value
if self.media_image_remotely_accessible:
state_attr["entity_picture_local"] = self.media_image_local
state_attr[ATTR_ENTITY_PICTURE_LOCAL] = self.media_image_local
return state_attr
@@ -32,6 +32,9 @@ def async_process_play_media_url(
"""Update a media URL with authentication if it points at Home Assistant."""
parsed = yarl.URL(media_content_id)
if parsed.scheme and parsed.scheme not in ("http", "https"):
return media_content_id
if parsed.is_absolute():
if not is_hass_url(hass, media_content_id):
return media_content_id
@@ -1,9 +1,12 @@
"""Provides the constants needed for component."""
from enum import IntEnum
# How long our auth signature on the content should be valid for
CONTENT_AUTH_EXPIRY_TIME = 3600 * 24
ATTR_APP_ID = "app_id"
ATTR_APP_NAME = "app_name"
ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local"
ATTR_GROUP_MEMBERS = "group_members"
ATTR_INPUT_SOURCE = "source"
ATTR_INPUT_SOURCE_LIST = "source_list"
@@ -89,6 +92,32 @@ REPEAT_MODE_OFF = "off"
REPEAT_MODE_ONE = "one"
REPEAT_MODES = [REPEAT_MODE_OFF, REPEAT_MODE_ALL, REPEAT_MODE_ONE]
class MediaPlayerEntityFeature(IntEnum):
"""Supported features of the media player entity."""
PAUSE = 1
SEEK = 2
VOLUME_SET = 4
VOLUME_MUTE = 8
PREVIOUS_TRACK = 16
NEXT_TRACK = 32
TURN_ON = 128
TURN_OFF = 256
PLAY_MEDIA = 512
VOLUME_STEP = 1024
SELECT_SOURCE = 2048
STOP = 4096
CLEAR_PLAYLIST = 8192
PLAY = 16384
SHUFFLE_SET = 32768
SELECT_SOUND_MODE = 65536
BROWSE_MEDIA = 131072
REPEAT_SET = 262144
GROUPING = 524288
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4
@@ -0,0 +1,26 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.core import HomeAssistant, callback
from . import (
ATTR_ENTITY_PICTURE_LOCAL,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_SOUND_MODE_LIST,
)
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static and token attributes from being recorded in the database."""
return {
ATTR_ENTITY_PICTURE_LOCAL,
ATTR_ENTITY_PICTURE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_POSITION,
ATTR_SOUND_MODE_LIST,
}
@@ -6,6 +6,7 @@ from collections.abc import Iterable
from typing import Any
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_STOP,
@@ -33,6 +34,7 @@ from .const import (
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
MediaPlayerEntityFeature,
)
# mypy: allow-untyped-defs
@@ -46,6 +48,8 @@ async def _async_reproduce_states(
reproduce_options: dict[str, Any] | None = None,
) -> None:
"""Reproduce component states."""
cur_state = hass.states.get(state.entity_id)
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
async def call_service(service: str, keys: Iterable) -> None:
"""Call service with set of attributes given."""
@@ -59,47 +63,75 @@ async def _async_reproduce_states(
)
if state.state == STATE_OFF:
await call_service(SERVICE_TURN_OFF, [])
if features & MediaPlayerEntityFeature.TURN_OFF:
await call_service(SERVICE_TURN_OFF, [])
# entities that are off have no other attributes to restore
return
if state.state in (
STATE_ON,
STATE_PLAYING,
STATE_IDLE,
STATE_PAUSED,
if (
state.state
in (
STATE_ON,
STATE_PLAYING,
STATE_IDLE,
STATE_PAUSED,
)
and features & MediaPlayerEntityFeature.TURN_ON
):
await call_service(SERVICE_TURN_ON, [])
if ATTR_MEDIA_VOLUME_LEVEL in state.attributes:
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
cur_state = hass.states.get(state.entity_id)
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
if ATTR_MEDIA_VOLUME_MUTED in state.attributes:
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
if ATTR_INPUT_SOURCE in state.attributes:
# First set source & sound mode to match the saved supported features
if (
ATTR_INPUT_SOURCE in state.attributes
and features & MediaPlayerEntityFeature.SELECT_SOURCE
):
await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE])
if ATTR_SOUND_MODE in state.attributes:
if (
ATTR_SOUND_MODE in state.attributes
and features & MediaPlayerEntityFeature.SELECT_SOUND_MODE
):
await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE])
if (
ATTR_MEDIA_VOLUME_LEVEL in state.attributes
and features & MediaPlayerEntityFeature.VOLUME_SET
):
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
if (
ATTR_MEDIA_VOLUME_MUTED in state.attributes
and features & MediaPlayerEntityFeature.VOLUME_MUTE
):
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
already_playing = False
if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and (
ATTR_MEDIA_CONTENT_ID in state.attributes
):
await call_service(
SERVICE_PLAY_MEDIA,
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
)
if features & MediaPlayerEntityFeature.PLAY_MEDIA:
await call_service(
SERVICE_PLAY_MEDIA,
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
)
already_playing = True
if state.state == STATE_PLAYING and not already_playing:
if (
not already_playing
and state.state == STATE_PLAYING
and features & MediaPlayerEntityFeature.PLAY
):
await call_service(SERVICE_MEDIA_PLAY, [])
elif state.state == STATE_IDLE:
await call_service(SERVICE_MEDIA_STOP, [])
if features & MediaPlayerEntityFeature.STOP:
await call_service(SERVICE_MEDIA_STOP, [])
elif state.state == STATE_PAUSED:
await call_service(SERVICE_MEDIA_PAUSE, [])
if features & MediaPlayerEntityFeature.PAUSE:
await call_service(SERVICE_MEDIA_PAUSE, [])
async def async_reproduce_states(
+9 -1
View File
@@ -21,6 +21,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.distance import convert as convert_distance
@@ -33,6 +34,7 @@ from .const import (
DOMAIN,
)
# Dedicated Home Assistant endpoint - do not change!
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
PLATFORMS = [Platform.WEATHER]
@@ -82,6 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unload_ok
class CannotConnect(HomeAssistantError):
"""Unable to connect to the web site."""
class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]):
"""Class to manage fetching Met data."""
@@ -173,7 +179,9 @@ class MetWeatherData:
async def fetch_data(self) -> MetWeatherData:
"""Fetch data from API - (current weather and forecast)."""
await self._weather_data.fetching_data()
resp = await self._weather_data.fetching_data()
if not resp:
raise CannotConnect()
self.current_weather_data = self._weather_data.get_current_weather()
time_zone = dt_util.DEFAULT_TIME_ZONE
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
@@ -10,6 +10,7 @@ from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
import voluptuous as vol
from homeassistant.components import camera
from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
@@ -181,7 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
p_id = face.store[g_id].get(service.data[ATTR_PERSON])
camera_entity = service.data[ATTR_CAMERA_ENTITY]
camera = hass.components.camera
try:
image = await camera.async_get_image(hass, camera_entity)
@@ -16,7 +16,14 @@ from homeassistant.helpers.schema_config_entry_flow import (
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
_STATISTIC_MEASURES = ["last", "max", "mean", "min", "median"]
_STATISTIC_MEASURES = [
{"value": "min", "label": "Minimum"},
{"value": "max", "label": "Maximum"},
{"value": "mean", "label": "Arithmetic mean"},
{"value": "median", "label": "Median"},
{"value": "last", "label": "Most recently updated"},
]
OPTIONS_SCHEMA = vol.Schema(
{
@@ -3,24 +3,30 @@
"config": {
"step": {
"user": {
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median.",
"title": "Add min / max / mean / median sensor",
"description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.",
"data": {
"entity_ids": "Input entities",
"name": "Name",
"round_digits": "Precision",
"type": "Statistic characteristic"
},
"data_description": {
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
}
}
}
},
"options": {
"step": {
"options": {
"description": "[%key:component::min_max::config::step::user::description%]",
"init": {
"data": {
"entity_ids": "[%key:component::min_max::config::step::user::data::entity_ids%]",
"round_digits": "[%key:component::min_max::config::step::user::data::round_digits%]",
"type": "[%key:component::min_max::config::step::user::data::type%]"
},
"data_description": {
"round_digits": "[%key:component::min_max::config::step::user::data_description::round_digits%]"
}
}
}
@@ -8,19 +8,25 @@
"round_digits": "Precision",
"type": "Statistic characteristic"
},
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median."
"data_description": {
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
},
"description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.",
"title": "Add min / max / mean / median sensor"
}
}
},
"options": {
"step": {
"options": {
"init": {
"data": {
"entity_ids": "Input entities",
"round_digits": "Precision",
"type": "Statistic characteristic"
},
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median."
"data_description": {
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
}
}
}
},
@@ -155,7 +155,7 @@ async def async_setup_entry(
platform.async_register_entity_service(
SERVICE_SET_ABSOLUTE_POSITION,
SET_ABSOLUTE_POSITION_SCHEMA,
SERVICE_SET_ABSOLUTE_POSITION,
"async_set_absolute_position",
)
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "mpd",
"name": "Music Player Daemon (MPD)",
"documentation": "https://www.home-assistant.io/integrations/mpd",
"requirements": ["python-mpd2==3.0.4"],
"requirements": ["python-mpd2==3.0.5"],
"codeowners": ["@fabaff"],
"iot_class": "local_polling",
"loggers": ["mpd"]
+1 -3
View File
@@ -463,7 +463,7 @@ class MpdDevice(MediaPlayerEntity):
if media_source.is_media_source_id(media_id):
media_type = MEDIA_TYPE_MUSIC
play_item = await media_source.async_resolve_media(self.hass, media_id)
media_id = play_item.url
media_id = async_process_play_media_url(self.hass, play_item.url)
if media_type == MEDIA_TYPE_PLAYLIST:
_LOGGER.debug("Playing playlist: %s", media_id)
@@ -476,8 +476,6 @@ class MpdDevice(MediaPlayerEntity):
await self._client.load(media_id)
await self._client.play()
else:
media_id = async_process_play_media_url(self.hass, media_id)
await self._client.clear()
self._currentplaylist = None
await self._client.add(media_id)
@@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.9.2"],
"requirements": ["pynetgear==0.9.4"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,
@@ -90,10 +90,12 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
await self._router.async_allow_block_device(self._mac, ALLOW)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
await self._router.async_allow_block_device(self._mac, BLOCK)
await self.coordinator.async_request_refresh()
@callback
def async_update_device(self) -> None:
+17 -4
View File
@@ -6,11 +6,16 @@ import logging
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
from homeassistant.loader import bind_hass
from . import util
from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
from .const import (
IPV4_BROADCAST_ADDR,
LOOPBACK_TARGET_IP,
MDNS_TARGET_IP,
PUBLIC_TARGET_IP,
)
from .models import Adapter
from .network import Network, async_get_network
@@ -26,7 +31,7 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
@bind_hass
async def async_get_source_ip(
hass: HomeAssistant, target_ip: str = PUBLIC_TARGET_IP
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
) -> str:
"""Get the source ip for a target ip."""
adapters = await async_get_adapters(hass)
@@ -35,7 +40,15 @@ async def async_get_source_ip(
if adapter["enabled"] and (ipv4s := adapter["ipv4"]):
all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
source_ip = util.async_get_source_ip(target_ip)
if target_ip is UNDEFINED:
source_ip = (
util.async_get_source_ip(PUBLIC_TARGET_IP)
or util.async_get_source_ip(MDNS_TARGET_IP)
or util.async_get_source_ip(LOOPBACK_TARGET_IP)
)
else:
source_ip = util.async_get_source_ip(target_ip)
if not all_ipv4s:
_LOGGER.warning(
"Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
@@ -17,6 +17,7 @@ ATTR_ADAPTERS: Final = "adapters"
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
LOOPBACK_TARGET_IP: Final = "127.0.0.1"
MDNS_TARGET_IP: Final = "224.0.0.251"
PUBLIC_TARGET_IP: Final = "8.8.8.8"
IPV4_BROADCAST_ADDR: Final = "255.255.255.255"
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "NINA",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nina",
"requirements": ["pynina==0.1.7"],
"requirements": ["pynina==0.1.8"],
"dependencies": [],
"codeowners": ["@DeerMaximum"],
"iot_class": "cloud_polling",
@@ -0,0 +1,17 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.core import HomeAssistant, callback
from . import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude static attributes from being recorded in the database."""
return {
ATTR_MIN,
ATTR_MAX,
ATTR_STEP,
ATTR_MODE,
}
+2 -1
View File
@@ -204,9 +204,10 @@ class ONVIFDevice:
if self._dt_diff_seconds > 5:
LOGGER.warning(
"The date/time on the device (UTC) is '%s', "
"The date/time on %s (UTC) is '%s', "
"which is different from the system '%s', "
"this could lead to authentication issues",
self.name,
cam_date_utc,
system_date,
)
@@ -2,7 +2,7 @@
"domain": "openhome",
"name": "Linn / OpenHome",
"documentation": "https://www.home-assistant.io/integrations/openhome",
"requirements": ["openhomedevice==2.0.1"],
"requirements": ["openhomedevice==2.0.2"],
"codeowners": ["@bazwilliams"],
"iot_class": "local_polling",
"loggers": ["async_upnp_client", "openhomedevice"]
@@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from pyoverkiz.enums.ui import UIWidget
@@ -55,15 +55,15 @@ class OverkizAlarmDescription(
"""Class to describe an Overkiz alarm control panel."""
alarm_disarm: str | None = None
alarm_disarm_args: OverkizStateType | list[OverkizStateType] = []
alarm_disarm_args: OverkizStateType | list[OverkizStateType] = None
alarm_arm_home: str | None = None
alarm_arm_home_args: OverkizStateType | list[OverkizStateType] = []
alarm_arm_home_args: OverkizStateType | list[OverkizStateType] = None
alarm_arm_night: str | None = None
alarm_arm_night_args: OverkizStateType | list[OverkizStateType] = []
alarm_arm_night_args: OverkizStateType | list[OverkizStateType] = None
alarm_arm_away: str | None = None
alarm_arm_away_args: OverkizStateType | list[OverkizStateType] = []
alarm_arm_away_args: OverkizStateType | list[OverkizStateType] = None
alarm_trigger: str | None = None
alarm_trigger_args: OverkizStateType | list[OverkizStateType] = []
alarm_trigger_args: OverkizStateType | list[OverkizStateType] = None
MAP_INTERNAL_STATUS_STATE: dict[str, str] = {
@@ -266,7 +266,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
assert self.entity_description.alarm_disarm
await self.executor.async_execute_command(
await self.async_execute_command(
self.entity_description.alarm_disarm,
self.entity_description.alarm_disarm_args,
)
@@ -274,7 +274,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
assert self.entity_description.alarm_arm_home
await self.executor.async_execute_command(
await self.async_execute_command(
self.entity_description.alarm_arm_home,
self.entity_description.alarm_arm_home_args,
)
@@ -282,7 +282,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
assert self.entity_description.alarm_arm_night
await self.executor.async_execute_command(
await self.async_execute_command(
self.entity_description.alarm_arm_night,
self.entity_description.alarm_arm_night_args,
)
@@ -290,7 +290,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
assert self.entity_description.alarm_arm_away
await self.executor.async_execute_command(
await self.async_execute_command(
self.entity_description.alarm_arm_away,
self.entity_description.alarm_arm_away_args,
)
@@ -298,7 +298,14 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command."""
assert self.entity_description.alarm_trigger
await self.executor.async_execute_command(
await self.async_execute_command(
self.entity_description.alarm_trigger,
self.entity_description.alarm_trigger_args,
)
async def async_execute_command(self, command_name: str, args: Any) -> None:
"""Execute device command in async context."""
if args:
await self.executor.async_execute_command(command_name, args)
else:
await self.executor.async_execute_command(command_name)
+1 -2
View File
@@ -39,8 +39,7 @@ def _select_option_open_closed_pedestrian(
OverkizCommandParam.CLOSED: OverkizCommand.CLOSE,
OverkizCommandParam.OPEN: OverkizCommand.OPEN,
OverkizCommandParam.PEDESTRIAN: OverkizCommand.SET_PEDESTRIAN_POSITION,
}[OverkizCommandParam(option)],
None,
}[OverkizCommandParam(option)]
)
@@ -6,7 +6,7 @@
"dependencies": ["webhook"],
"after_dependencies": ["cloud"],
"codeowners": ["@JohNan"],
"requirements": ["pyplaato==0.0.15"],
"requirements": ["pyplaato==0.0.16"],
"iot_class": "cloud_push",
"loggers": ["pyplaato"]
}
+2 -1
View File
@@ -159,7 +159,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_SERVER],
error,
)
return False
# Retry as setups behind a proxy can return transient 404 or 502 errors
raise ConfigEntryNotReady from error
_LOGGER.debug(
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
+3 -1
View File
@@ -173,7 +173,9 @@ def process_plex_payload(
media = plex_server.lookup_media(content_type, **search_query)
if supports_playqueues and (isinstance(media, list) or shuffle):
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
playqueue = plex_server.create_playqueue(
media, includeRelated=0, shuffle=shuffle
)
return PlexMediaSearchResult(playqueue, content)
return PlexMediaSearchResult(media, content)
@@ -2,7 +2,7 @@
"domain": "plugwise",
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["plugwise==0.17.2"],
"requirements": ["plugwise==0.17.3"],
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true,
@@ -162,6 +162,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Authentication failed", exc_info=err)
http_session.close()
raise ConfigEntryAuthFailed from err
except APIError as err:
http_session.close()
raise ConfigEntryNotReady from err
gateway_din = base_info.gateway_din
if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id):
@@ -223,13 +226,18 @@ def call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo:
def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData:
"""Process and update powerwall data."""
try:
backup_reserve = power_wall.get_backup_reserve_percentage()
except MissingAttributeError:
backup_reserve = None
return PowerwallData(
charge=power_wall.get_charge(),
site_master=power_wall.get_sitemaster(),
meters=power_wall.get_meters(),
grid_services_active=power_wall.is_grid_services_active(),
grid_status=power_wall.get_grid_status(),
backup_reserve=power_wall.get_backup_reserve_percentage(),
backup_reserve=backup_reserve,
)
+1 -1
View File
@@ -38,7 +38,7 @@ class PowerwallData:
meters: MetersAggregates
grid_services_active: bool
grid_status: GridStatus
backup_reserve: float
backup_reserve: float | None
class PowerwallRuntimeData(TypedDict):
+6 -2
View File
@@ -117,9 +117,11 @@ async def async_setup_entry(
data: PowerwallData = coordinator.data
entities: list[PowerWallEntity] = [
PowerWallChargeSensor(powerwall_data),
PowerWallBackupReserveSensor(powerwall_data),
]
if data.backup_reserve is not None:
entities.append(PowerWallBackupReserveSensor(powerwall_data))
for meter in data.meters.meters:
entities.append(PowerWallExportSensor(powerwall_data, meter))
entities.append(PowerWallImportSensor(powerwall_data, meter))
@@ -190,8 +192,10 @@ class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):
return f"{self.base_unique_id}_backup_reserve"
@property
def native_value(self) -> int:
def native_value(self) -> int | None:
"""Get the current value in percentage."""
if self.data.backup_reserve is None:
return None
return round(self.data.backup_reserve)
@@ -51,6 +51,7 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity):
self.contract = contract
self._auth = auth
self._attr_code_arm_required = False
self._attr_name = f"contract {self.contract}"
self._attr_unique_id = self.contract
self._attr_supported_features = SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME

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