Compare commits

...

139 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
199 changed files with 2524 additions and 1022 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
+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:
@@ -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,
+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
@@ -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"
}
}
+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.
@@ -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(
@@ -83,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"}}
),
}
)
@@ -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",
+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",
@@ -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,4 +1,6 @@
"""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
@@ -90,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
@@ -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",
+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"]
+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)
@@ -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
+45 -15
View File
@@ -12,7 +12,7 @@ import logging
import os
import re
from statistics import mean
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, overload
from sqlalchemy import bindparam, func
from sqlalchemy.exc import SQLAlchemyError, StatementError
@@ -125,9 +125,9 @@ STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery"
STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery"
# Convert pressure and temperature statistics from the native unit used for statistics
# to the units configured by the user
UNIT_CONVERSIONS = {
# Convert pressure, temperature and volume statistics from the normalized unit used for
# statistics to the unit configured by the user
STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = {
PRESSURE_PA: lambda x, units: pressure_util.convert(
x, PRESSURE_PA, units.pressure_unit
)
@@ -145,6 +145,17 @@ UNIT_CONVERSIONS = {
else None,
}
# Convert volume statistics from the display unit configured by the user
# to the normalized unit used for statistics
# This is used to support adjusting statistics in the display unit
DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[
str, Callable[[float, UnitSystem], float]
] = {
VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert(
x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS
),
}
_LOGGER = logging.getLogger(__name__)
@@ -721,7 +732,17 @@ def get_metadata(
)
@overload
def _configured_unit(unit: None, units: UnitSystem) -> None:
...
@overload
def _configured_unit(unit: str, units: UnitSystem) -> str:
...
def _configured_unit(unit: str | None, units: UnitSystem) -> str | None:
"""Return the pressure and temperature units configured by the user."""
if unit == PRESSURE_PA:
return units.pressure_unit
@@ -1163,7 +1184,7 @@ def _sorted_statistics_to_dict(
statistic_id = metadata[meta_id]["statistic_id"]
convert: Callable[[Any, Any], float | None]
if convert_units:
convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return]
convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return]
else:
convert = no_conversion
ent_results = result[meta_id]
@@ -1323,17 +1344,26 @@ def adjust_statistics(
if statistic_id not in metadata:
return True
tables: tuple[type[Statistics | StatisticsShortTerm], ...] = (
Statistics,
units = instance.hass.config.units
statistic_unit = metadata[statistic_id][1]["unit_of_measurement"]
display_unit = _configured_unit(statistic_unit, units)
convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type]
sum_adjustment = convert(sum_adjustment, units)
_adjust_sum_statistics(
session,
StatisticsShortTerm,
metadata[statistic_id][0],
start_time,
sum_adjustment,
)
_adjust_sum_statistics(
session,
Statistics,
metadata[statistic_id][0],
start_time.replace(minute=0),
sum_adjustment,
)
for table in tables:
_adjust_sum_statistics(
session,
table,
metadata[statistic_id][0],
start_time,
sum_adjustment,
)
return True
@@ -2,8 +2,8 @@
"domain": "remote_rpi_gpio",
"name": "remote_rpi_gpio",
"documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio",
"requirements": ["gpiozero==1.5.1"],
"requirements": ["gpiozero==1.6.2", "pigpio==1.78"],
"codeowners": [],
"iot_class": "local_push",
"loggers": ["gpiozero"]
"loggers": ["gpiozero", "pigpio"]
}
@@ -3,7 +3,7 @@
"name": "Renault",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/renault",
"requirements": ["renault-api==0.1.10"],
"requirements": ["renault-api==0.1.11"],
"codeowners": ["@epenet"],
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
+10 -6
View File
@@ -1,5 +1,6 @@
"""The roomba component."""
import asyncio
from functools import partial
import logging
import async_timeout
@@ -42,12 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
},
)
roomba = RoombaFactory.create_roomba(
address=config_entry.data[CONF_HOST],
blid=config_entry.data[CONF_BLID],
password=config_entry.data[CONF_PASSWORD],
continuous=config_entry.options[CONF_CONTINUOUS],
delay=config_entry.options[CONF_DELAY],
roomba = await hass.async_add_executor_job(
partial(
RoombaFactory.create_roomba,
address=config_entry.data[CONF_HOST],
blid=config_entry.data[CONF_BLID],
password=config_entry.data[CONF_PASSWORD],
continuous=config_entry.options[CONF_CONTINUOUS],
delay=config_entry.options[CONF_DELAY],
)
)
try:
+10 -6
View File
@@ -1,6 +1,7 @@
"""Config flow to configure roomba component."""
import asyncio
from functools import partial
from roombapy import RoombaFactory
from roombapy.discovery import RoombaDiscovery
@@ -41,12 +42,15 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
roomba = RoombaFactory.create_roomba(
address=data[CONF_HOST],
blid=data[CONF_BLID],
password=data[CONF_PASSWORD],
continuous=False,
delay=data[CONF_DELAY],
roomba = await hass.async_add_executor_job(
partial(
RoombaFactory.create_roomba,
address=data[CONF_HOST],
blid=data[CONF_BLID],
password=data[CONF_PASSWORD],
continuous=False,
delay=data[CONF_DELAY],
)
)
info = await async_connect_or_timeout(hass, roomba)
@@ -3,7 +3,7 @@
"name": "RTSPtoWebRTC",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc",
"requirements": ["rtsp-to-webrtc==0.5.0"],
"requirements": ["rtsp-to-webrtc==0.5.1"],
"dependencies": ["camera"],
"codeowners": ["@allenporter"],
"iot_class": "local_push",
@@ -363,9 +363,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not entry:
return None
entry_kw_args: dict = {}
if (
self.unique_id
and entry.unique_id is None
if self.unique_id and (
entry.unique_id is None
or (is_unique_match and self.unique_id != entry.unique_id)
):
entry_kw_args["unique_id"] = self.unique_id
@@ -469,6 +468,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self._async_set_unique_id_from_udn()
self._async_update_and_abort_for_matching_unique_id()
self._async_abort_if_host_already_in_progress()
if self._method == METHOD_LEGACY and discovery_info.ssdp_st in (
UPNP_SVC_RENDERING_CONTROL,
UPNP_SVC_MAIN_TV_AGENT,
):
# The UDN we use for the unique id cannot be determined
# from device_info for legacy devices
return self.async_abort(reason="not_supported")
self.context["title_placeholders"] = {"device": self._title}
return await self.async_step_confirm()
@@ -214,13 +214,17 @@ class SamsungTVDevice(MediaPlayerEntity):
)
if self._attr_state != STATE_ON:
if self._dmr_device and self._dmr_device.is_subscribed:
await self._dmr_device.async_unsubscribe_services()
return
startup_tasks: list[Coroutine[Any, Any, None]] = []
startup_tasks: list[Coroutine[Any, Any, Any]] = []
if not self._app_list_event.is_set():
startup_tasks.append(self._async_startup_app_list())
if self._dmr_device and not self._dmr_device.is_subscribed:
startup_tasks.append(self._async_resubscribe_dmr())
if not self._dmr_device and self._ssdp_rendering_control_location:
startup_tasks.append(self._async_startup_dmr())
@@ -273,9 +277,12 @@ class SamsungTVDevice(MediaPlayerEntity):
if self._dmr_device is None:
session = async_get_clientsession(self.hass)
upnp_requester = AiohttpSessionRequester(session)
upnp_factory = UpnpFactory(upnp_requester)
# Set non_strict to avoid invalid data sent by Samsung TV:
# Got invalid value for <UpnpStateVariable(PlaybackStorageMedium, string)>:
# NETWORK,NONE
upnp_factory = UpnpFactory(upnp_requester, non_strict=True)
upnp_device: UpnpDevice | None = None
with contextlib.suppress(UpnpConnectionError):
with contextlib.suppress(UpnpConnectionError, UpnpResponseError):
upnp_device = await upnp_factory.async_create_device(
self._ssdp_rendering_control_location
)
@@ -310,6 +317,11 @@ class SamsungTVDevice(MediaPlayerEntity):
LOGGER.debug("Error while subscribing during device connect: %r", err)
raise
async def _async_resubscribe_dmr(self) -> None:
assert self._dmr_device
with contextlib.suppress(UpnpConnectionError):
await self._dmr_device.async_subscribe_services(auto_resubscribe=True)
async def _async_shutdown_dmr(self) -> None:
"""Handle removal."""
if (dmr_device := self._dmr_device) is not None:
-3
View File
@@ -157,9 +157,6 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""If cover is closed."""
if not self.status["pos_control"]:
return None
return cast(bool, self.status["state"] == "closed")
@property
+3 -1
View File
@@ -265,7 +265,9 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
if device.config.get("switch:0"):
key = key.replace("input", "switch")
device_name = get_rpc_device_name(device)
entity_name: str | None = device.config[key].get("name", device_name)
entity_name: str | None = None
if key in device.config:
entity_name = device.config[key].get("name", device_name)
if entity_name is None:
return f"{device_name} {key.replace(':', '_')}"
+4 -3
View File
@@ -130,6 +130,7 @@ async def async_setup_entry(
class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
"""Representation of a SleepIQ number entity."""
entity_description: SleepIQNumberEntityDescription
_attr_icon = "mdi:bed"
def __init__(
@@ -140,7 +141,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
description: SleepIQNumberEntityDescription,
) -> None:
"""Initialize the number."""
self.description = description
self.entity_description = description
self.device = device
self._attr_name = description.get_name_fn(bed, device)
@@ -151,10 +152,10 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
@callback
def _async_update_attrs(self) -> None:
"""Update number attributes."""
self._attr_value = float(self.description.value_fn(self.device))
self._attr_value = float(self.entity_description.value_fn(self.device))
async def async_set_value(self, value: float) -> None:
"""Set the number value."""
await self.description.set_value_fn(self.device, int(value))
await self.entity_description.set_value_fn(self.device, int(value))
self._attr_value = value
self.async_write_ha_state()
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "SMA Solar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sma",
"requirements": ["pysma==0.6.10"],
"requirements": ["pysma==0.6.11"],
"codeowners": ["@kellerza", "@rklomp"],
"iot_class": "local_polling",
"loggers": ["pysma"]
@@ -28,10 +28,10 @@ create_zone:
description: Name of slaves entities to add to the new zone.
required: true
selector:
target:
entity:
integration: soundtouch
domain: media_player
entity:
multiple: true
integration: soundtouch
domain: media_player
add_zone_slave:
name: Add zone slave
@@ -50,10 +50,10 @@ add_zone_slave:
description: Name of slaves entities to add to the existing zone.
required: true
selector:
target:
entity:
integration: soundtouch
domain: media_player
entity:
multiple: true
integration: soundtouch
domain: media_player
remove_zone_slave:
name: Remove zone slave
@@ -72,7 +72,7 @@ remove_zone_slave:
description: Name of slaves entities to remove from the existing zone.
required: true
selector:
target:
entity:
integration: soundtouch
domain: media_player
entity:
multiple: true
integration: soundtouch
domain: media_player
@@ -47,7 +47,7 @@ from .util import fetch_image_url
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SCAN_INTERVAL = timedelta(minutes=1)
SUPPORT_SPOTIFY = (
SUPPORT_BROWSE_MEDIA
@@ -117,6 +117,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
_attr_icon = "mdi:spotify"
_attr_media_content_type = MEDIA_TYPE_MUSIC
_attr_media_image_remotely_accessible = False
_attr_entity_registry_enabled_default = False
def __init__(
self,
@@ -5,3 +5,4 @@ KNOWN_PLAYERS = "known_players"
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
DISCOVERY_TASK = "discovery_task"
DEFAULT_PORT = 9000
SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:")
@@ -63,7 +63,13 @@ from .browse_media import (
library_payload,
media_source_content_filter,
)
from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB
from .const import (
DISCOVERY_TASK,
DOMAIN,
KNOWN_PLAYERS,
PLAYER_DISCOVERY_UNSUB,
SQUEEZEBOX_SOURCE_STRINGS,
)
SERVICE_CALL_METHOD = "call_method"
SERVICE_CALL_QUERY = "call_query"
@@ -475,7 +481,9 @@ class SqueezeBoxEntity(MediaPlayerEntity):
media_id = play_item.url
if media_type in MEDIA_TYPE_MUSIC:
media_id = async_process_play_media_url(self.hass, media_id)
if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
# do not process special squeezebox "source" media ids
media_id = async_process_play_media_url(self.hass, media_id)
await self._player.async_load_url(media_id, cmd)
return
@@ -2,7 +2,7 @@
"domain": "stream",
"name": "Stream",
"documentation": "https://www.home-assistant.io/integrations/stream",
"requirements": ["PyTurboJPEG==1.6.6", "av==9.0.0"],
"requirements": ["PyTurboJPEG==1.6.6", "ha-av==9.1.1-3"],
"dependencies": ["http"],
"codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
"quality_scale": "internal",
+7 -3
View File
@@ -130,9 +130,12 @@ class Sun(Entity):
self._config_listener = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self.update_location
)
self._loaded_listener = self.hass.bus.async_listen(
EVENT_COMPONENT_LOADED, self.loading_complete
)
if DOMAIN in hass.config.components:
self.update_location()
else:
self._loaded_listener = self.hass.bus.async_listen(
EVENT_COMPONENT_LOADED, self.loading_complete
)
@callback
def loading_complete(self, event_: Event) -> None:
@@ -158,6 +161,7 @@ class Sun(Entity):
"""Remove the loaded listener."""
if self._loaded_listener:
self._loaded_listener()
self._loaded_listener = None
@callback
def remove_listeners(self):
@@ -1,9 +1,8 @@
{
"title": "Switch as X",
"title": "Change device type of a switch",
"config": {
"step": {
"user": {
"title": "Change switch device type",
"description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.",
"data": {
"entity_id": "Switch",
@@ -1,14 +1,14 @@
{
"config": {
"step": {
"init": {
"user": {
"data": {
"entity_id": "Switch entity",
"target_domain": "Type"
"entity_id": "Switch",
"target_domain": "New Type"
},
"title": "Make a switch a ..."
"description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden."
}
}
},
"title": "Switch as X"
"title": "Change device type of a switch"
}
+14 -2
View File
@@ -18,7 +18,10 @@ from homeassistant.util import Throttle
from .const import (
CONF_FALLBACK,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TADO_OPTIONS,
DATA,
DOMAIN,
INSIDE_TEMPERATURE_MEASUREMENT,
@@ -51,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT)
tadoconnector = TadoConnector(hass, username, password, fallback)
@@ -99,7 +102,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options)
if CONF_FALLBACK not in options:
options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
options[CONF_FALLBACK] = entry.data.get(
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
)
hass.config_entries.async_update_entry(entry, options=options)
if options[CONF_FALLBACK] not in CONST_OVERLAY_TADO_OPTIONS:
if options[CONF_FALLBACK]:
options[CONF_FALLBACK] = CONST_OVERLAY_TADO_MODE
else:
options[CONF_FALLBACK] = CONST_OVERLAY_MANUAL
hass.config_entries.async_update_entry(entry, options=options)
+11 -2
View File
@@ -11,7 +11,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_FALLBACK, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, UNIQUE_ID
from .const import (
CONF_FALLBACK,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_OPTIONS,
DOMAIN,
UNIQUE_ID,
)
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +132,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema = vol.Schema(
{
vol.Optional(
CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK)
CONF_FALLBACK,
default=self.config_entry.options.get(
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
),
): vol.In(CONST_OVERLAY_TADO_OPTIONS),
}
)

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