Compare commits

..

158 Commits

Author SHA1 Message Date
Paulus Schoutsen 5df747276f Merge pull request #61625 from home-assistant/rc 2021-12-12 15:39:42 -08:00
Paulus Schoutsen 77b06bc158 Bump aiohue to 3.0.3 (#61627) 2021-12-12 14:29:37 -08:00
J. Nick Koston db6d176658 Bump aiopvapi to 1.6.19 to fix async_timeout passing loop (#61618) 2021-12-12 14:29:36 -08:00
Paulus Schoutsen 973eb4f6d4 Bumped version to 2021.12.1 2021-12-12 14:15:15 -08:00
Marcel van der Veldt 14401aa840 Fix availability for 3th party Hue lights (#61603) 2021-12-12 14:14:13 -08:00
Ernst Klamer b82ddb77bc Fix for failing Solarlog integration in HA 2021.12 (#61602) 2021-12-12 14:14:13 -08:00
Allen Porter 22530f72f3 Only publish nest camera event messages once per thread and bump nest version (#61587) 2021-12-12 14:14:12 -08:00
Marcel van der Veldt a16bf358aa enable grouped light if enabled in previous integration (#61582) 2021-12-12 14:14:12 -08:00
Marcel van der Veldt 0924874d4b Fix Hue transition calculation (#61581) 2021-12-12 14:14:11 -08:00
Bram Kragten a3ff783bc1 Update frontend to 20211212.0 (#61577) 2021-12-12 14:14:10 -08:00
J. Nick Koston e7d06e3f6a Fix HomeKit covers with device class window and no tilt (#61566) 2021-12-12 14:14:10 -08:00
Allen Porter a2fc870266 Update logic for nest media source can_play for events (#61537) 2021-12-12 14:14:09 -08:00
jjlawren ffcb107716 Fix Sonos sub & surround switch state reporting (#61531)
* Fix sub/surround states, refactor volume param handling

* Lint
2021-12-12 14:14:08 -08:00
Franck Nijhof 604a2ac327 Merge pull request #61501 from home-assistant/rc 2021-12-11 19:06:29 +01:00
Franck Nijhof 1042f23a0a Bumped version to 2021.12.0 2021-12-11 18:15:19 +01:00
Bram Kragten 608ce2d5a0 Update frontend to 20211211.0 (#61499) 2021-12-11 18:14:53 +01:00
Marcel van der Veldt 2e989bdfcf Fix typo in Hue device triggers - use enum value (#61498) 2021-12-11 18:14:45 +01:00
Franck Nijhof f10bfc961d Bumped version to 2021.12.0b7 2021-12-11 13:36:48 +01:00
J. Nick Koston 1f57c8ed1a Fix missing color modes for Magic Home Ceiling Light CCT (0xE1) (#61478) 2021-12-11 13:36:23 +01:00
J. Nick Koston dd47f0b698 Fix exception in color_rgb_to_rgbww (#61466) 2021-12-11 13:36:20 +01:00
J. Nick Koston 08eabfd056 Fix non-threadsafe call to async_fire in telegram_bot (#61465)
Fixes https://github.com/home-assistant/core/issues/53255#issuecomment-888111478
2021-12-11 13:36:17 +01:00
Marcel van der Veldt 7b64eabde1 Small fix for device triggers and events on Hue integration (#61462) 2021-12-11 13:36:13 +01:00
David F. Mulcahey 46808b1fc1 Bump ZHA quirks to 0.0.65 (#61458) 2021-12-11 13:36:09 +01:00
Simone Chemelli 23cb75fe20 Interim fix (#61435) 2021-12-11 13:33:56 +01:00
Erik Montnemery d20496a1bc Correct rest sensor configured to generate timestamps (#61429) 2021-12-11 13:26:33 +01:00
Erik Montnemery 4496aeb327 Correct recorder.statistics.get_last_statistics (#61421) 2021-12-11 13:26:30 +01:00
Erik Montnemery 519ec18a04 Correct device class for Tasmota dewpoint sensor (#61420) 2021-12-11 13:26:26 +01:00
J. Nick Koston c14269d09d Fix older v1 dimmable flux_led bulbs not turning on (#61414) 2021-12-11 13:26:21 +01:00
J. Nick Koston 81b1b04210 Fix flux_led discovery with older models (#61413) 2021-12-11 13:26:17 +01:00
Aaron Bach 18768ad8a0 Bump simplisafe-python to 2021.12.1 (#61412) 2021-12-11 13:26:13 +01:00
J. Nick Koston d038db01ed Fix lookin set temperature when device is off (#61411) 2021-12-11 13:26:09 +01:00
Matthias Alphart c51c18781d Fix unique_id of S0 meters connected to Fronius inverters (#61408) 2021-12-11 13:26:06 +01:00
bsmappee e483c16d59 Remove energy entity again in Smappee local integration (#61373) 2021-12-11 13:26:02 +01:00
Yehuda Davis 87b50fff54 Fix Tuya cover open/close commands (#61369)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-12-11 13:25:58 +01:00
bsmappee da9fbde83a add missing unit of measurement in Smappee (#61365) 2021-12-11 13:25:55 +01:00
MattWestb 6785e32683 Add 2 new CN-Hysen TRVs (#61002)
Adding CN-Hysen "_TZE200_pvvbommb" and "_TZE200_4eeyebrt" TRVs
2021-12-11 13:25:51 +01:00
Paulus Schoutsen 7208cb49f1 Disable lupusec (#61142) 2021-12-09 15:05:41 -08:00
Paulus Schoutsen 5476b23d8b Bumped version to 2021.12.0b6 2021-12-09 14:42:14 -08:00
Aaron Bach 5d65db5168 Assign docs URL to a placeholder in SimpliSafe config flow (#61410) 2021-12-09 14:42:05 -08:00
Paulus Schoutsen 509ebbc743 Bump frontend to 20211209.0 (#61406) 2021-12-09 14:42:05 -08:00
Aaron Bach abe6f1ab5b Consolidate SimpliSafe config flow forms into one (#61402) 2021-12-09 14:42:04 -08:00
jjlawren ae26e60740 Fix Sonos radio handling during polling (#61401) 2021-12-09 14:42:03 -08:00
bigbadblunt f8f381afa3 Add default value for signal_repetitions in cover (#61393) 2021-12-09 14:42:02 -08:00
einarhauks 7cc2af2a46 Update tesla-wall-connector to v1.0.1 (#61392) 2021-12-09 14:42:02 -08:00
Franck Nijhof 29aab7ad7a Bumped version to 2021.12.0b5 2021-12-09 12:09:53 +01:00
Franck Nijhof 3b2b116c10 Upgrade tailscale to 0.1.4 (#61338) 2021-12-09 12:08:36 +01:00
Paulus Schoutsen 07438c07c9 Fix CO2signal error handling (#61311) 2021-12-09 12:08:33 +01:00
Paulus Schoutsen 0203228a11 Fix hue groups inheritance (#61308) 2021-12-09 12:08:29 +01:00
J. Nick Koston fe7521b503 Fix lookin failing to setup during firmware updates (#61305) 2021-12-09 12:08:26 +01:00
Yehuda Davis 24a6e90042 Fix regression in Tuya cover is_closed logic (#61303) 2021-12-09 12:08:23 +01:00
Paulus Schoutsen 7387640524 Fix rova timezone (#61302) 2021-12-09 12:08:20 +01:00
Stefan Agner d7708d58ba Introduce only_supervisor for @websocket_api.ws_require_user() (#61298) 2021-12-09 12:08:16 +01:00
Paulus Schoutsen 10a4037ed3 Rest fixes (#61296) 2021-12-09 12:08:13 +01:00
J. Nick Koston aefd675737 Restore rest integration ability to follow http redirects (#61293) 2021-12-09 12:08:10 +01:00
J. Nick Koston 9a4a09b2f2 Bump flux_led to 0.26.3 (#61287) 2021-12-09 12:08:06 +01:00
Paulus Schoutsen 793bdebc13 Use correct template parameter in Rest template rendering (#61269) 2021-12-09 12:08:01 +01:00
Paulus Schoutsen e66f0a68e7 Guard cannot connect during Tuya init (#61267) 2021-12-09 12:07:58 +01:00
Paulus Schoutsen 5c70ddb7cb Fix smartthings timestamp sensor (#61254) 2021-12-09 12:07:55 +01:00
Erik Montnemery 79501289f0 Correct state class for Tasmota sensors (#61236)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2021-12-09 12:07:51 +01:00
Allen Porter 7ee148c650 Display nest media events using local time (#61143) 2021-12-09 12:07:48 +01:00
Franck Nijhof 1ddb0d255a Fix date/datetime support for templates (#61088)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-09 12:07:45 +01:00
Stefan Agner 89eed9c31e Allow to lock SQLite database during backup (#60874)
* Allow to set CONF_DB_URL

This is useful for test which need a custom DB path.

* Introduce write_lock_db helper to lock SQLite database

* Introduce Websocket API which allows to lock database during backup

* Fix isort

* Avoid mutable default arguments

* Address pylint issues

* Avoid holding executor thread

* Set unlock event in case timeout occures

This makes sure the database is left unlocked even in case of a race
condition.

* Add more unit tests

* Address new pylint errors

* Lower timeout to speedup tests

* Introduce queue overflow test

* Unlock database if necessary

This makes sure that the test runs through in case locking actually
succeeds (and the test fails).

* Make DB_LOCK_TIMEOUT a global

There is no good reason for this to be an argument. The recorder needs
to pick a sensible value.

* Add Websocket Timeout test

* Test lock_database() return

* Update homeassistant/components/recorder/__init__.py

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

* Fix format

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-12-09 12:07:41 +01:00
Paulus Schoutsen 0cb0136b2f Bumped version to 2021.12.0b4 2021-12-08 11:02:14 -08:00
Paulus Schoutsen 36eca38be2 don't convert GTFS timestamp to UTC in timestamp sensor (#61221) 2021-12-08 11:02:05 -08:00
Paulus Schoutsen 0b470bb8fb Fix follow-up review comment for bbox (#61219) 2021-12-08 11:02:04 -08:00
Paulus Schoutsen 030ac3d762 Fix yandex_transport timestamp sensor (#61217) 2021-12-08 11:02:04 -08:00
Paulus Schoutsen b5b2c3cc0d Fix vallox timestamp sensor (#61216)
* Fix vallox timestamp sensor

* Change old state type
2021-12-08 11:02:03 -08:00
Paulus Schoutsen 7940aab4c5 Fix repetier timestamp sensors (#61214) 2021-12-08 11:02:02 -08:00
Paulus Schoutsen 2513347e27 Fix oasa_telematics timestamp sensor (#61213) 2021-12-08 11:02:01 -08:00
Paulus Schoutsen e6b784e4f2 Fix nextbus timestamp sensor (#61212) 2021-12-08 11:02:00 -08:00
Paulus Schoutsen d080c31583 Fix modern_forms timestmap sensors (#61211) 2021-12-08 11:01:59 -08:00
Paulus Schoutsen e68dcff3f3 Fix meteo_france timestamp sensor (#61210) 2021-12-08 11:01:58 -08:00
Paulus Schoutsen 66fa6dff93 Fix lyric timestamp sensor (#61209)
* Fix lyric timestamp sensor

* Update type
2021-12-08 11:01:57 -08:00
Paulus Schoutsen d533aba4f9 Fix litterrobot timestamp sensor (#61208)
* Fix litterrobot timestamp sensor

* Update type
2021-12-08 11:01:56 -08:00
Paulus Schoutsen 700eaf8794 Fix islamic prayer times timestamp sensor (#61207) 2021-12-08 11:01:56 -08:00
Paulus Schoutsen 7583d9a409 Fix hydrawise timestamp sensor (#61206) 2021-12-08 11:01:55 -08:00
Paulus Schoutsen dc3ece447b Fix hvv_departures timestamp sensor (#61205) 2021-12-08 11:01:54 -08:00
Paulus Schoutsen 2c0e406c1b Fix gtfs timestamp sensor (#61204) 2021-12-08 11:01:53 -08:00
Paulus Schoutsen 67c808bde9 Fix flipr timestamp sensor (#61203) 2021-12-08 11:01:52 -08:00
Paulus Schoutsen 2fa2a2e6d4 Fix bbox timestamp (#61202) 2021-12-08 11:01:52 -08:00
Paulus Schoutsen 8735395144 Fix Rova using strings as timestamp (#61201) 2021-12-08 11:01:51 -08:00
J. Nick Koston 428129cad7 Fix log spam from flux_led 0x08 devices when in music mode (#61196) 2021-12-08 11:01:50 -08:00
puddly 64c52aecef Bump ZHA dependency zigpy-znp from 0.6.3 to 0.6.4 (#61194) 2021-12-08 11:01:49 -08:00
J. Nick Koston 04a2e1fd7b Fix uncaught exception in bond config flow (#61184) 2021-12-08 11:01:49 -08:00
Robert Blomqvist bdc37e9353 Rephrase upgrade notification message to avoid installing Python 3.10 (#61181) 2021-12-08 11:01:48 -08:00
Jan Bouwhuis a581095bd0 Fix pvoutput template use and REST integer parsing (#61171)
* Fix pvoutput template use and REST integer parsing

* revert accepting templates as input
2021-12-08 11:01:47 -08:00
Erik Montnemery 707e501511 Skip duplicated data when calculating fossil energy consumption (#60599) 2021-12-08 11:01:46 -08:00
Paulus Schoutsen 9f1701f557 Bumped version to 2021.12.0b3 2021-12-07 12:54:28 -08:00
Charles Garwood 61545edd96 Remove loopenergy integration (#61175)
* Remove loopenergy integration

* Fix requirements_all.txt

* Fix requirements_test_all.txt
2021-12-07 12:54:22 -08:00
Allen Porter e09c85c591 Bump nest to 0.4.5 to fix media player event expiration (#61174) 2021-12-07 12:54:21 -08:00
einarhauks fecfbba442 Display energy in wh instead of kWh (#61169) 2021-12-07 12:54:21 -08:00
Aaron Bach 13ce6edc68 Bump py17track to 2021.12.2 (#61166) 2021-12-07 12:54:20 -08:00
Tobias Sauerwein 816b5af883 Fix Netatmo climate issue (#61154)
Signed-off-by: cgtobi <cgtobi@gmail.com>
2021-12-07 12:54:19 -08:00
Erik Montnemery 78ada630c0 Guard against missing states in Alexa state updates (#61152) 2021-12-07 12:54:18 -08:00
Marcel van der Veldt 4ad904f3b7 Change check for existence of options flow (#61147)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-07 12:54:16 -08:00
Fredrik Erlandsson fa447332c6 Fix point availability (#61144) 2021-12-07 12:54:15 -08:00
Erik Montnemery 8da3756602 Bump hatasmota to 0.3.1 (#61120) 2021-12-07 12:54:15 -08:00
G Johansson 01adc6a042 Improve code quality trafikverket_weatherstation (#61044)
* Code quality trafikverket_weatherstation

* Updates from review

* Fix extra attributes settings

* Fix for additional review comments
2021-12-07 12:54:14 -08:00
Paulus Schoutsen d105e9f99e Bumped version to 2021.12.0b2 2021-12-06 15:54:09 -08:00
Paulus Schoutsen 348079f069 Bump frontend to 20211206.0 (#61133) 2021-12-06 15:54:01 -08:00
Aaron Bach 86f5165e4c Deprecate entity_id parameter in Guardian service calls (#61129) 2021-12-06 15:54:00 -08:00
jjlawren b6d012222a Improve Sonos activity debug logging (#61122) 2021-12-06 15:53:59 -08:00
Aaron Bach 0532c22069 Bump simplisafe-python to 2021.12.0 (#61121) 2021-12-06 15:53:58 -08:00
Marcel van der Veldt d1672a1e9a Remove colon from default entity name in Hue integration (#61118) 2021-12-06 15:53:58 -08:00
Paulus Schoutsen 725e3046db Return native timestamps for home connect (#61116) 2021-12-06 15:53:57 -08:00
Paulus Schoutsen 325aa66b8c Bump aiohue to 3.0.2 (#61115) 2021-12-06 15:53:56 -08:00
Erik Montnemery 3ba07ce395 Fix CO2 calculation when data is missing (#61106) 2021-12-06 15:53:56 -08:00
Martin Hjelmare 21463121a7 Improve zwave_js add-on config flow description (#61099) 2021-12-06 15:53:55 -08:00
Marcel van der Veldt ef0f3f7ce9 Fix migration of entities of Hue integration (#61095)
* fix device name in log

* Fix Hue migration for all id versions

* fix tests

* typo

* change to bit more universal approach

* fix test again

* formatting
2021-12-06 15:53:54 -08:00
epenet cb371ef27c Prevent log flooding in frame helper (#61085)
Co-authored-by: epenet <epenet@users.noreply.github.com>
2021-12-06 15:53:54 -08:00
J. Nick Koston 878700e26f Provide a hint on which username to use for enphase_envoy (#61084) 2021-12-06 15:53:53 -08:00
J. Nick Koston e09245eb14 Fix missing unique id in enphase_envoy (#61083) 2021-12-06 15:53:52 -08:00
J. Nick Koston 20fb06484c Bump enphase_envoy to 0.20.1 (#61082) 2021-12-06 15:53:51 -08:00
Allen Porter f4a38c0190 Coalesce nest media source preview clips by session and bump google-nest-sdm (#61081) 2021-12-06 15:53:50 -08:00
Allen Porter fa33464217 Remove unnecessary explicit use of OrderedDict in nest media source (#61054)
Address follow up PR comments from #60073
2021-12-06 15:53:49 -08:00
Alexander Pitkin bd239bcbed Fix yandex transport for Belarus (#61080) 2021-12-06 15:52:00 -08:00
Aaron Bach d5f3e2a761 Deprecate system_id parameter in SimpliSafe service calls (#61076) 2021-12-06 15:51:59 -08:00
J. Nick Koston ec88a42948 Abort flux_led discovery if another device gets the ip (#61074)
- If the dhcp reservation expired for the device that
  was at the ip and a new flux_led device appears we
  would discover it because the unique_id did not match
2021-12-06 15:51:59 -08:00
Alexei Chetroi a3ede8f895 Add 3157100-E model to Centralite thermostat (#61073) 2021-12-06 15:51:58 -08:00
J. Nick Koston 23ebde58cd Bump flux_led to 0.25.17 to fix missing push messages on 0xA3 models (#61070) 2021-12-06 15:51:57 -08:00
Allen Porter 0c87885f41 Fix regression in nest event media player with multiple devices (#61064) 2021-12-06 15:51:56 -08:00
Aaron Bach c159790caf Fix mispelling in SimpliSafe service description (#61058) 2021-12-06 15:51:56 -08:00
Allen Porter 056575f491 Add debug logging for pip install command (#61057) 2021-12-06 15:51:55 -08:00
Jérôme W e4d9d0d83e Add media player volume control in fr-FR with Alexa (#60489)
* media player volume control in `fr-FR` with Alexa

* Apply suggestions from code review

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2021-12-06 15:51:54 -08:00
schreyack 34f728e5d2 Fix previous setting briefly appearing on newer flux_led devices when turning on (#60004)
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-12-06 15:51:54 -08:00
Allen Porter 377046bff5 Fetch media for events for rendering in the nest media player (#61056) 2021-12-05 09:52:28 -08:00
Marcel van der Veldt dd95b9b1e4 Disable options flow for Hue V2 bridges (#61045) 2021-12-05 09:52:28 -08:00
Marcel van der Veldt a976ed2c72 Add guard for empty mac address in Hue integration (#61037) 2021-12-05 09:52:27 -08:00
Marcel van der Veldt 90442d9e9e Fix Hue migration (#61030) 2021-12-05 09:52:26 -08:00
Marcel van der Veldt c67b250be2 Fix Hue config flow (#61028) 2021-12-05 09:52:26 -08:00
J. Nick Koston 974cc94f87 Update flux_led models database to fix turn on for newer models (#61005) 2021-12-05 09:52:25 -08:00
david reid 528d4bc6ce Catch ConnectionResetError (#60987) 2021-12-05 09:52:24 -08:00
Franck Nijhof 7a4f1c3147 Handle unknown/unavailable state for mobile_app (#60974) 2021-12-05 09:52:24 -08:00
Teemu R c70f833069 Use STATE_DOCKED for emptying the bin for xiaomi_miio.vacuum (#60513) 2021-12-05 09:52:23 -08:00
Paulus Schoutsen eaf53c10ed Bumped version to 2021.12.0b1 2021-12-05 09:44:11 -08:00
Allen Porter 576362bfe1 Bump nest to version 0.4.2 (#61036) 2021-12-05 09:43:52 -08:00
J. Nick Koston 4e957b1dbe Fix lutron caseta discovery with newer firmwares (#61029) 2021-12-05 09:43:51 -08:00
Allen Porter c4fe3d05f2 Improve nest media source event timestamp display (#61027)
Drop subsecond text from the nest media source event timestamp display, using a common date/time
template string.
2021-12-05 09:43:51 -08:00
Marvin Wichmann f81055dc09 Add missing local_ip to KNX config flow and options flow (#61018)
* Add missing local_ip to KNX config flow and options flow

* Update strings
2021-12-05 09:43:50 -08:00
Erik Montnemery 70814130c3 Fix translations for binary_sensor tampered device triggers (#60996) 2021-12-05 09:43:49 -08:00
Franck Nijhof 0e70121a6f Fix typo in state_characteristic warning (#60990) 2021-12-05 09:43:49 -08:00
Franck Nijhof 62a60f1cf6 Fix str for device registry entry_type warnings caused by core (#60989) 2021-12-05 09:43:48 -08:00
Franck Nijhof 6a1dce852e Fix DSMR Reader providing strings as timestamps (#60988) 2021-12-05 09:43:47 -08:00
Franck Nijhof af1ad0e6f8 Only report deprecated device_state_attributes once (#60980) 2021-12-05 09:43:47 -08:00
Franck Nijhof dd2e250c66 Fix Xiaomi Miio providing strings as timestamps (#60979) 2021-12-05 09:43:46 -08:00
Martin Hjelmare 18f36b9c0b Revert metoffice weather daytime (#60978) 2021-12-05 09:43:45 -08:00
J. Nick Koston 2ba7f9c584 Fix flood lights not turning on/off with flux_led (#60973) 2021-12-05 09:43:45 -08:00
J. Nick Koston 5a3dd71bde Fix dimmable effects for flux_led model 0x33 v9+ (#60972) 2021-12-05 09:43:44 -08:00
Franck Nijhof 823a4578d7 Upgrade netdata to 1.0.1 (#60971) 2021-12-05 09:43:43 -08:00
Franck Nijhof b5bfa728e9 Upgrade luftdaten to 0.7.1 (#60970) 2021-12-05 09:43:43 -08:00
J. Nick Koston 11b343a513 Fix yeelight name changing to ip address if discovery fails (#60967) 2021-12-05 09:43:42 -08:00
Paulus Schoutsen fe46b2664a Handle invalid device registry entry type (#60966)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2021-12-05 09:43:41 -08:00
Paulus Schoutsen 53e2ebc688 Correctly type the SSDP callback function (#60964) 2021-12-05 09:43:41 -08:00
Paulus Schoutsen 4023d55229 Fix statistics registering at start callback (#60963) 2021-12-05 09:43:40 -08:00
Aaron Bach 823e46ea26 Ensure that inactive RainMachine switch that is toggled on is toggled back off (#60959) 2021-12-05 09:43:39 -08:00
Aaron Bach 0b9efc2a06 Add missing SimpliSafe service information (#60958) 2021-12-05 09:43:38 -08:00
Raman Gupta 6af9471710 Fix nzbget datetime return value (#60953) 2021-12-05 09:43:38 -08:00
rikroe f78e59842d Fix BMW Connected Drive (#60938)
* Bump bimmer_connected to 0.8.5

* Always update HA states after service execution

* Fix BMW device tracker & vehicle_finder service

* Add charging_end_time sensor

* Fix pylint & pytest

* Remove unneeded DEFAULT_OPTION

* Revert adding charging_end_time & state_attributes

* Don't delete option data for CONF_USE_LOCATION

* Remove stale string

Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2021-12-05 09:43:37 -08:00
212 changed files with 3682 additions and 1089 deletions
-1
View File
@@ -597,7 +597,6 @@ omit =
homeassistant/components/lookin/models.py
homeassistant/components/lookin/sensor.py
homeassistant/components/lookin/climate.py
homeassistant/components/loopenergy/sensor.py
homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/__init__.py
homeassistant/components/luftdaten/sensor.py
-1
View File
@@ -293,7 +293,6 @@ homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/lookin/* @ANMalko
homeassistant/components/loopenergy/* @pavoni
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @mzdrale
homeassistant/components/luftdaten/* @fabaff
+1 -2
View File
@@ -252,8 +252,7 @@ async def async_from_config_dict(
f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will "
f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. "
"Please upgrade Python to "
f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or "
"higher."
f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])}."
)
_LOGGER.warning(msg)
hass.components.persistent_notification.async_create(
@@ -695,6 +695,7 @@ class AlexaSpeaker(AlexaCapability):
"en-US",
"es-ES",
"es-MX",
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
"it-IT",
"ja-JP",
}
@@ -752,6 +753,7 @@ class AlexaStepSpeaker(AlexaCapability):
"en-IN",
"en-US",
"es-ES",
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
"it-IT",
}
@@ -182,12 +182,13 @@ async def async_send_add_or_update_message(hass, config, entity_ids):
endpoints = []
for entity_id in entity_ids:
domain = entity_id.split(".", 1)[0]
if domain not in ENTITY_ADAPTERS:
if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS:
continue
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id))
if (state := hass.states.get(entity_id)) is None:
continue
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state)
endpoints.append(alexa_entity.serialize_discovery())
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}}
+1 -2
View File
@@ -131,10 +131,9 @@ class BboxUptimeSensor(SensorEntity):
def update(self):
"""Get the latest data from Bbox and update the state."""
self.bbox_data.update()
uptime = utcnow() - timedelta(
self._attr_native_value = utcnow() - timedelta(
seconds=self.bbox_data.router_infos["device"]["uptime"]
)
self._attr_native_value = uptime.replace(microsecond=0).isoformat()
class BboxSensor(SensorEntity):
@@ -90,8 +90,8 @@
"no_smoke": "{entity_name} stopped detecting smoke",
"sound": "{entity_name} started detecting sound",
"no_sound": "{entity_name} stopped detecting sound",
"is_tampered": "{entity_name} started detecting tampering",
"is_not_tampered": "{entity_name} stopped detecting tampering",
"tampered": "{entity_name} started detecting tampering",
"not_tampered": "{entity_name} stopped detecting tampering",
"update": "{entity_name} got an update available",
"no_update": "{entity_name} became up-to-date",
"vibration": "{entity_name} started detecting vibration",
@@ -35,7 +35,6 @@ from .const import (
CONF_ACCOUNT,
CONF_ALLOWED_REGIONS,
CONF_READ_ONLY,
CONF_USE_LOCATION,
DATA_ENTRIES,
DATA_HASS_CONFIG,
)
@@ -65,7 +64,6 @@ SERVICE_SCHEMA = vol.Schema(
DEFAULT_OPTIONS = {
CONF_READ_ONLY: False,
CONF_USE_LOCATION: False,
}
PLATFORMS = [
@@ -215,13 +213,10 @@ def setup_account(
password: str = entry.data[CONF_PASSWORD]
region: str = entry.data[CONF_REGION]
read_only: bool = entry.options[CONF_READ_ONLY]
use_location: bool = entry.options[CONF_USE_LOCATION]
_LOGGER.debug("Adding new account %s", name)
pos = (
(hass.config.latitude, hass.config.longitude) if use_location else (None, None)
)
pos = (hass.config.latitude, hass.config.longitude)
cd_account = BMWConnectedDriveAccount(
username, password, region, name, read_only, *pos
)
@@ -258,6 +253,13 @@ def setup_account(
function_call = getattr(vehicle.remote_services, function_name)
function_call()
if call.service in [
"find_vehicle",
"activate_air_conditioning",
"deactivate_air_conditioning",
]:
cd_account.update()
if not read_only:
# register the remote services
for service in _SERVICE_MAP:
@@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from . import DOMAIN
from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION
from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY
DATA_SCHEMA = vol.Schema(
{
@@ -115,10 +115,6 @@ class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow):
CONF_READ_ONLY,
default=self.config_entry.options.get(CONF_READ_ONLY, False),
): bool,
vol.Optional(
CONF_USE_LOCATION,
default=self.config_entry.options.get(CONF_USE_LOCATION, False),
): bool,
}
),
)
@@ -35,7 +35,7 @@ async def async_setup_entry(
for vehicle in account.account.vehicles:
entities.append(BMWDeviceTracker(account, vehicle))
if not vehicle.status.is_vehicle_tracking_enabled:
if not vehicle.is_vehicle_tracking_enabled:
_LOGGER.info(
"Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown",
vehicle.name,
@@ -83,6 +83,6 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
self._attr_extra_state_attributes = self._attrs
self._location = (
self._vehicle.status.gps_position
if self._vehicle.status.is_vehicle_tracking_enabled
if self._vehicle.is_vehicle_tracking_enabled
else None
)
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.8.2"],
"requirements": ["bimmer_connected==0.8.5"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling"
@@ -21,8 +21,7 @@
"step": {
"account_options": {
"data": {
"read_only": "Read-only (only sensors and notify, no execution of services, no lock)",
"use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)"
"read_only": "Read-only (only sensors and notify, no execution of services, no lock)"
}
}
}
+4 -1
View File
@@ -87,7 +87,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return
self._discovered[CONF_ACCESS_TOKEN] = token
_, hub_name = await _validate_input(self.hass, self._discovered)
try:
_, hub_name = await _validate_input(self.hass, self._discovered)
except InputValidationError:
return
self._discovered[CONF_NAME] = hub_name
async def async_step_zeroconf(
@@ -134,9 +134,6 @@ def get_data(hass: HomeAssistant, config: dict) -> CO2SignalResponse:
_LOGGER.exception("Unexpected exception")
raise UnknownError from err
except Exception as err:
_LOGGER.exception("Unexpected exception")
raise UnknownError from err
else:
if "error" in data:
@@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_data
from . import APIRatelimitExceeded, CO2Error, InvalidAuth, get_data
from .const import CONF_COUNTRY_CODE, DOMAIN
from .util import get_extra_name
@@ -172,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth"
except APIRatelimitExceeded:
errors["base"] = "api_ratelimit"
except UnknownError:
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
else:
return self.async_create_entry(
@@ -366,13 +366,11 @@ async def ignore_config_flow(hass, connection, msg):
def entry_json(entry: config_entries.ConfigEntry) -> dict:
"""Return JSON value of a config entry."""
handler = config_entries.HANDLERS.get(entry.domain)
supports_options = (
# Guard in case handler is no longer registered (custom component etc)
handler is not None
# pylint: disable=comparison-with-callable
and handler.async_get_options_flow
!= config_entries.ConfigFlow.async_get_options_flow
# work out if handler has support for options flow
supports_options = handler is not None and handler.async_supports_options_flow(
entry
)
return {
"entry_id": entry.entry_id,
"domain": entry.domain,
@@ -10,7 +10,10 @@ from homeassistant.components.websocket_api.decorators import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry
from homeassistant.helpers.entity_registry import (
RegistryEntryDisabler,
async_get_registry,
)
async def async_setup(hass):
@@ -75,7 +78,12 @@ async def websocket_get_entity(hass, connection, msg):
vol.Optional("name"): vol.Any(str, None),
vol.Optional("new_entity_id"): str,
# We only allow setting disabled_by user via API.
vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None),
vol.Optional("disabled_by"): vol.Any(
None,
vol.All(
vol.Coerce(RegistryEntryDisabler), RegistryEntryDisabler.USER.value
),
),
}
)
async def websocket_update_entity(hass, connection, msg):
@@ -24,6 +24,7 @@ from homeassistant.const import (
POWER_KILO_WATT,
VOLUME_CUBIC_METERS,
)
from homeassistant.util import dt as dt_util
PRICE_EUR_KWH: Final = f"EUR/{ENERGY_KILO_WATT_HOUR}"
PRICE_EUR_M3: Final = f"EUR/{VOLUME_CUBIC_METERS}"
@@ -202,6 +203,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
name="Telegram timestamp",
entity_registry_enabled_default=False,
device_class=DEVICE_CLASS_TIMESTAMP,
state=dt_util.parse_datetime,
),
DSMRReaderSensorEntityDescription(
key="dsmr/consumption/gas/delivered",
@@ -222,6 +224,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
name="Gas meter read",
entity_registry_enabled_default=False,
device_class=DEVICE_CLASS_TIMESTAMP,
state=dt_util.parse_datetime,
),
DSMRReaderSensorEntityDescription(
key="dsmr/day-consumption/electricity1",
@@ -274,14 +274,16 @@ async def ws_get_fossil_energy_consumption(
) -> dict[datetime, float]:
"""Combine multiple statistics, returns a dict indexed by start time."""
result: defaultdict[datetime, float] = defaultdict(float)
seen: defaultdict[datetime, set[str]] = defaultdict(set)
for statistics_id, stat in stats.items():
if statistics_id not in statistic_ids:
continue
for period in stat:
if period["sum"] is None:
if period["sum"] is None or statistics_id in seen[period["start"]]:
continue
result[period["start"]] += period["sum"]
seen[period["start"]].add(statistics_id)
return {key: result[key] for key in sorted(result)}
@@ -303,6 +305,8 @@ async def ws_get_fossil_energy_consumption(
"""Reduce hourly deltas to daily or monthly deltas."""
result: list[dict[str, Any]] = []
deltas: list[float] = []
if not stat_list:
return result
prev_stat: dict[str, Any] = stat_list[0]
# Loop over the hourly deltas + a fake entry to end the period
@@ -75,6 +75,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
envoy_reader.get_inverters = False
await coordinator.async_config_entry_first_refresh()
if not entry.unique_id:
try:
serial = await envoy_reader.get_full_serial_number()
except httpx.HTTPError:
pass
else:
hass.config_entries.async_update_entry(entry, unique_id=serial)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
COORDINATOR: coordinator,
NAME: name,
@@ -1,6 +1,7 @@
"""Config flow for Enphase Envoy integration."""
from __future__ import annotations
import contextlib
import logging
from typing import Any
@@ -31,7 +32,7 @@ ENVOY = "Envoy"
CONF_SERIAL = "serial"
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader:
"""Validate the user input allows us to connect."""
envoy_reader = EnvoyReader(
data[CONF_HOST],
@@ -48,6 +49,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
except (RuntimeError, httpx.HTTPError) as err:
raise CannotConnect from err
return envoy_reader
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Enphase Envoy."""
@@ -59,7 +62,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.ip_address = None
self.name = None
self.username = None
self.serial = None
self._reauth_entry = None
@callback
@@ -104,8 +106,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
self.serial = discovery_info.properties["serialnum"]
await self.async_set_unique_id(self.serial)
serial = discovery_info.properties["serialnum"]
await self.async_set_unique_id(serial)
self.ip_address = discovery_info.host
self._abort_if_unique_id_configured({CONF_HOST: self.ip_address})
for entry in self._async_current_entries(include_ignore=False):
@@ -114,9 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
and CONF_HOST in entry.data
and entry.data[CONF_HOST] == self.ip_address
):
title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY
title = f"{ENVOY} {serial}" if entry.title == ENVOY else ENVOY
self.hass.config_entries.async_update_entry(
entry, title=title, unique_id=self.serial
entry, title=title, unique_id=serial
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
@@ -132,6 +134,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
return await self.async_step_user()
def _async_envoy_name(self) -> str:
"""Return the name of the envoy."""
if self.name:
return self.name
if self.unique_id:
return f"{ENVOY} {self.unique_id}"
return ENVOY
async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool:
"""Set the unique id by fetching it from the envoy."""
serial = None
with contextlib.suppress(httpx.HTTPError):
serial = await envoy_reader.get_full_serial_number()
if serial:
await self.async_set_unique_id(serial)
return True
return False
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -145,7 +165,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
):
return self.async_abort(reason="already_configured")
try:
await validate_input(self.hass, user_input)
envoy_reader = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
@@ -155,21 +175,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
else:
data = user_input.copy()
if self.serial:
data[CONF_NAME] = f"{ENVOY} {self.serial}"
else:
data[CONF_NAME] = self.name or ENVOY
data[CONF_NAME] = self._async_envoy_name()
if self._reauth_entry:
self.hass.config_entries.async_update_entry(
self._reauth_entry,
data=data,
)
return self.async_abort(reason="reauth_successful")
if not self.unique_id and await self._async_set_unique_id_from_envoy(
envoy_reader
):
data[CONF_NAME] = self._async_envoy_name()
if self.unique_id:
self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]})
return self.async_create_entry(title=data[CONF_NAME], data=data)
if self.serial:
if self.unique_id:
self.context["title_placeholders"] = {
CONF_SERIAL: self.serial,
CONF_SERIAL: self.unique_id,
CONF_HOST: self.ip_address,
}
return self.async_show_form(
@@ -3,7 +3,7 @@
"name": "Enphase Envoy",
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"requirements": [
"envoy_reader==0.20.0"
"envoy_reader==0.20.1"
],
"codeowners": [
"@gtdiehl"
@@ -15,4 +15,4 @@
}
],
"iot_class": "local_polling"
}
}
@@ -3,6 +3,7 @@
"flow_title": "{serial} ({host})",
"step": {
"user": {
"description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
@@ -16,7 +16,8 @@
"host": "Host",
"password": "Password",
"username": "Username"
}
},
"description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password."
}
}
}
+1 -6
View File
@@ -1,8 +1,6 @@
"""Sensor platform for the Flipr's pool_sensor."""
from __future__ import annotations
from datetime import datetime
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
@@ -60,7 +58,4 @@ class FliprSensor(FliprEntity, SensorEntity):
@property
def native_value(self):
"""State of the sensor."""
state = self.coordinator.data[self.entity_description.key]
if isinstance(state, datetime):
return state.isoformat()
return state
return self.coordinator.data[self.entity_description.key]
@@ -115,8 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == host and not entry.unique_id:
async_update_entry_from_discovery(self.hass, entry, device)
if entry.data[CONF_HOST] == host:
if not entry.unique_id:
async_update_entry_from_discovery(self.hass, entry, device)
return self.async_abort(reason="already_configured")
self.context[CONF_HOST] = host
for progress in self._async_in_progress():
@@ -237,7 +238,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FluxLEDDiscovery:
"""Try to connect."""
self._async_abort_entries_match({CONF_HOST: host})
if device := await async_discover_device(self.hass, host):
if (device := await async_discover_device(self.hass, host)) and device[
ATTR_MODEL_DESCRIPTION
]:
# Older models do not return enough information
# to build the model description via UDP so we have
# to fallback to making a tcp connection to avoid
# identifying the device as the chip model number
# AKA `HF-LPB100-ZJ200`
return device
bulb = async_wifi_bulb_for_host(host)
try:
+5 -4
View File
@@ -280,10 +280,11 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
async def _async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified or all lights on."""
if not self.is_on:
await self._device.async_turn_on()
if not kwargs:
return
if self._device.requires_turn_on or not kwargs:
if not self.is_on:
await self._device.async_turn_on()
if not kwargs:
return
if MODE_ATTRS.intersection(kwargs):
await self._async_set_mode(**kwargs)
@@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.25.10"],
"requirements": ["flux_led==0.26.7"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",
+9 -2
View File
@@ -784,15 +784,22 @@ class MeterSensor(_FroniusSensorEntity):
self._entity_id_prefix = f"meter_{solar_net_id}"
super().__init__(coordinator, key, solar_net_id)
meter_data = self._device_data()
# S0 meters connected directly to inverters respond "n.a." as serial number
# `model` contains the inverter id: "S0 Meter at inverter 1"
if (meter_uid := meter_data["serial"]["value"]) == "n.a.":
meter_uid = (
f"{coordinator.solar_net.solar_net_device_id}:"
f'{meter_data["model"]["value"]}'
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, meter_data["serial"]["value"])},
identifiers={(DOMAIN, meter_uid)},
manufacturer=meter_data["manufacturer"]["value"],
model=meter_data["model"]["value"],
name=meter_data["model"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
)
self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}'
self._attr_unique_id = f"{meter_uid}-{key}"
class OhmpilotSensor(_FroniusSensorEntity):
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211203.0"
"home-assistant-frontend==20211212.0"
],
"dependencies": [
"api",
+3 -5
View File
@@ -544,7 +544,7 @@ class GTFSDepartureSensor(SensorEntity):
self._available = False
self._icon = ICON
self._name = ""
self._state: str | None = None
self._state: datetime.datetime | None = None
self._attributes: dict[str, Any] = {}
self._agency = None
@@ -563,7 +563,7 @@ class GTFSDepartureSensor(SensorEntity):
return self._name
@property
def native_value(self) -> str | None:
def native_value(self) -> datetime.datetime | None:
"""Return the state of the sensor."""
return self._state
@@ -619,9 +619,7 @@ class GTFSDepartureSensor(SensorEntity):
if not self._departure:
self._state = None
else:
self._state = dt_util.as_utc(
self._departure["departure_time"]
).isoformat()
self._state = self._departure["departure_time"]
# Fetch trip and route details once, unless updated
if not self._departure:
+57 -18
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from typing import cast
from typing import TYPE_CHECKING, cast
from aioguardian import Client
from aioguardian.errors import GuardianError
@@ -11,6 +11,8 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_DEVICE_ID,
CONF_FILENAME,
CONF_IP_ADDRESS,
@@ -18,7 +20,11 @@ from homeassistant.const import (
CONF_URL,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import (
@@ -63,20 +69,41 @@ SERVICES = (
SERVICE_NAME_UPGRADE_FIRMWARE,
)
SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Required(CONF_UID): cv.string,
}
SERVICE_BASE_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
}
),
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
)
SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_URL): cv.url,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_FILENAME): cv.string,
},
SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(CONF_UID): cv.string,
}
),
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
)
SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_URL): cv.url,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_FILENAME): cv.string,
},
),
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
)
@@ -86,6 +113,14 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"]
@callback
def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str:
"""Get the entry ID related to a service call (by device ID)."""
if ATTR_ENTITY_ID in call.data:
entity_registry = er.async_get(hass)
entity_registry_entry = entity_registry.async_get(call.data[ATTR_ENTITY_ID])
if TYPE_CHECKING:
assert entity_registry_entry
assert entity_registry_entry.config_entry_id
return entity_registry_entry.config_entry_id
device_id = call.data[CONF_DEVICE_ID]
device_registry = dr.async_get(hass)
@@ -221,15 +256,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
for service_name, schema, method in (
(SERVICE_NAME_DISABLE_AP, None, async_disable_ap),
(SERVICE_NAME_ENABLE_AP, None, async_enable_ap),
(SERVICE_NAME_DISABLE_AP, SERVICE_BASE_SCHEMA, async_disable_ap),
(SERVICE_NAME_ENABLE_AP, SERVICE_BASE_SCHEMA, async_enable_ap),
(
SERVICE_NAME_PAIR_SENSOR,
SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA,
async_pair_sensor,
),
(SERVICE_NAME_REBOOT, None, async_reboot),
(SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, None, async_reset_valve_diagnostics),
(SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot),
(
SERVICE_NAME_RESET_VALVE_DIAGNOSTICS,
SERVICE_BASE_SCHEMA,
async_reset_valve_diagnostics,
),
(
SERVICE_NAME_UNPAIR_SENSOR,
SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA,
+3 -2
View File
@@ -20,6 +20,7 @@ from homeassistant.const import (
ATTR_MANUFACTURER,
ATTR_NAME,
EVENT_CORE_CONFIG_UPDATE,
HASSIO_USER_NAME,
SERVICE_HOMEASSISTANT_RESTART,
SERVICE_HOMEASSISTANT_STOP,
)
@@ -439,11 +440,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
# Migrate old name
if user.name == "Hass.io":
await hass.auth.async_update_user(user, name="Supervisor")
await hass.auth.async_update_user(user, name=HASSIO_USER_NAME)
if refresh_token is None:
user = await hass.auth.async_create_system_user(
"Supervisor", group_ids=[GROUP_ID_ADMIN]
HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN]
)
refresh_token = await hass.auth.async_create_refresh_token(user)
data["hassio_user"] = user.id
@@ -255,3 +255,5 @@ async def _websocket_forward(ws_from, ws_to):
await ws_to.close(code=ws_to.close_code, message=msg.extra)
except RuntimeError:
_LOGGER.debug("Ingress Websocket runtime error")
except ConnectionResetError:
_LOGGER.debug("Ingress Websocket Connection Reset")
@@ -63,16 +63,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
elif (
self._state is not None
and self._sign == 1
and dt_util.parse_datetime(self._state) < dt_util.utcnow()
and self._state < dt_util.utcnow()
):
# if the date is supposed to be in the future but we're
# already past it, set state to None.
self._state = None
else:
seconds = self._sign * float(status[self._key][ATTR_VALUE])
self._state = (
dt_util.utcnow() + timedelta(seconds=seconds)
).isoformat()
self._state = dt_util.utcnow() + timedelta(seconds=seconds)
else:
self._state = status[self._key].get(ATTR_VALUE)
if self._key == BSH_OPERATION_STATE:
@@ -249,14 +249,17 @@ class OpeningDeviceBase(HomeAccessory):
def async_update_state(self, new_state):
"""Update cover position and tilt after state changed."""
# update tilt
if not self._supports_tilt:
return
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if isinstance(current_tilt, (float, int)):
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
if not isinstance(current_tilt, (float, int)):
return
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
self.char_target_tilt.set_value(current_tilt)
class OpeningDevice(OpeningDeviceBase, HomeAccessory):
+26 -20
View File
@@ -50,6 +50,14 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return HueOptionsFlowHandler(config_entry)
@classmethod
@callback
def async_supports_options_flow(
cls, config_entry: config_entries.ConfigEntry
) -> bool:
"""Return options flow support for this handler."""
return config_entry.data.get(CONF_API_VERSION, 1) == 1
def __init__(self) -> None:
"""Initialize the Hue flow."""
self.bridge: DiscoveredHueBridge | None = None
@@ -216,16 +224,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not url.hostname:
return self.async_abort(reason="not_hue_bridge")
bridge = await self._get_bridge(
# abort if we already have exactly this bridge id/host
# reload the integration if the host got updated
bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
await self.async_set_unique_id(bridge_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: url.hostname}, reload_on_update=True
)
self.bridge = await self._get_bridge(
url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
)
await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: bridge.host}, reload_on_update=False
)
self.bridge = bridge
return await self.async_step_link()
async def async_step_zeroconf(
@@ -236,17 +245,18 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
This flow is triggered by the Zeroconf component. It will check if the
host is already configured and delegate to the import step if not.
"""
bridge = await self._get_bridge(
discovery_info.host,
discovery_info.properties["bridgeid"],
)
await self.async_set_unique_id(bridge.id)
# abort if we already have exactly this bridge id/host
# reload the integration if the host got updated
bridge_id = normalize_bridge_id(discovery_info.properties["bridgeid"])
await self.async_set_unique_id(bridge_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: bridge.host}, reload_on_update=False
updates={CONF_HOST: discovery_info.host}, reload_on_update=True
)
self.bridge = bridge
# we need to query the other capabilities too
self.bridge = await self._get_bridge(
discovery_info.host, discovery_info.properties["bridgeid"]
)
return await self.async_step_link()
async def async_step_homekit(
@@ -290,10 +300,6 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
if self.config_entry.data.get(CONF_API_VERSION, 1) > 1:
# Options for Hue are only applicable to V1 bridges.
return self.async_show_form(step_id="init")
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.1"],
"requirements": ["aiohue==3.0.3"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",
+84 -56
View File
@@ -4,6 +4,7 @@ import logging
from aiohue import HueBridgeV2
from aiohue.discovery import is_v2_bridge
from aiohue.v2.models.device import DeviceArchetypes
from aiohue.v2.models.resource import ResourceTypes
from homeassistant import core
@@ -18,7 +19,10 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import async_get as async_get_device_registry
from homeassistant.helpers.device_registry import (
async_entries_for_config_entry as devices_for_config_entries,
async_get as async_get_device_registry,
)
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry as entities_for_config_entry,
async_entries_for_device,
@@ -82,6 +86,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
dev_reg = async_get_device_registry(hass)
ent_reg = async_get_entity_registry(hass)
LOGGER.info("Start of migration of devices and entities to support API schema 2")
# Create mapping of mac address to HA device id's.
# Identifier in dev reg should be mac-address,
# but in some cases it has a postfix like `-0b` or `-01`.
dev_ids = {}
for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id):
for domain, mac in hass_dev.identifiers:
if domain != DOMAIN:
continue
normalized_mac = mac.split("-")[0]
dev_ids[normalized_mac] = hass_dev.id
# initialize bridge connection just for the migration
async with HueBridgeV2(host, api_key, websession) as api:
@@ -92,81 +108,93 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE,
}
# handle entities attached to device
# migrate entities attached to a device
for hue_dev in api.devices:
zigbee = api.devices.get_zigbee_connectivity(hue_dev.id)
if not zigbee:
# not a zigbee device
if not zigbee or not zigbee.mac_address:
# not a zigbee device or invalid mac
continue
mac = zigbee.mac_address
# get/update existing device by V1 identifier (mac address)
# the device will now have both the old and the new identifier
identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, mac)}
hass_dev = dev_reg.async_get_or_create(
config_entry_id=entry.entry_id, identifiers=identifiers
)
LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id)
# loop through al entities for device and find match
for ent in async_entries_for_device(ent_reg, hass_dev.id, True):
# migrate light
if ent.entity_id.startswith("light"):
# should always return one lightid here
new_unique_id = next(iter(hue_dev.lights))
if ent.unique_id == new_unique_id:
continue # just in case
LOGGER.info(
"Migrating %s from unique id %s to %s",
ent.entity_id,
ent.unique_id,
new_unique_id,
)
ent_reg.async_update_entity(
ent.entity_id, new_unique_id=new_unique_id
)
continue
# migrate sensors
matched_dev_class = sensor_class_mapping.get(
ent.original_device_class or "unknown"
# get existing device by V1 identifier (mac address)
if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2:
hass_dev_id = dev_ids.get(api.config.bridge_id.upper())
else:
hass_dev_id = dev_ids.get(zigbee.mac_address)
if hass_dev_id is None:
# can be safely ignored, this device does not exist in current config
LOGGER.debug(
"Ignoring device %s (%s) as it does not (yet) exist in the device registry",
hue_dev.metadata.name,
hue_dev.id,
)
if matched_dev_class is None:
continue
dev_reg.async_update_device(
hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)}
)
LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id)
# loop through all entities for device and find match
for ent in async_entries_for_device(ent_reg, hass_dev_id, True):
if ent.entity_id.startswith("light"):
# migrate light
# should always return one lightid here
new_unique_id = next(iter(hue_dev.lights), None)
else:
# migrate sensors
matched_dev_class = sensor_class_mapping.get(
ent.original_device_class or "unknown"
)
new_unique_id = next(
(
sensor.id
for sensor in api.devices.get_sensors(hue_dev.id)
if sensor.type == matched_dev_class
),
None,
)
if new_unique_id is None:
# this may happen if we're looking at orphaned or unsupported entity
LOGGER.warning(
"Skip migration of %s because it no longer exists on the bridge",
ent.entity_id,
)
continue
for sensor in api.devices.get_sensors(hue_dev.id):
if sensor.type != matched_dev_class:
continue
new_unique_id = sensor.id
if ent.unique_id == new_unique_id:
break # just in case
try:
ent_reg.async_update_entity(
ent.entity_id, new_unique_id=new_unique_id
)
except ValueError:
# assume edge case where the entity was already migrated in a previous run
# which got aborted somehow and we do not want
# to crash the entire integration init
LOGGER.warning(
"Skip migration of %s because it already exists",
ent.entity_id,
)
else:
LOGGER.info(
"Migrating %s from unique id %s to %s",
"Migrated entity %s from unique id %s to %s",
ent.entity_id,
ent.unique_id,
new_unique_id,
)
try:
ent_reg.async_update_entity(
ent.entity_id, new_unique_id=sensor.id
)
except ValueError:
# assume edge case where the entity was already migrated in a previous run
# which got aborted somehow and we do not want
# to crash the entire integration init
LOGGER.warning(
"Skip migration of %s because it already exists",
ent.entity_id,
)
break
# migrate entities that are not connected to a device (groups)
for ent in entities_for_config_entry(ent_reg, entry.entry_id):
if ent.device_id is not None:
continue
v1_id = f"/groups/{ent.unique_id}"
hue_group = api.groups.room.get_by_v1_id(v1_id)
if "-" in ent.unique_id:
# handle case where unique id is v2-id of group/zone
hue_group = api.groups.get(ent.unique_id)
else:
# handle case where the unique id is just the v1 id
v1_id = f"/groups/{ent.unique_id}"
hue_group = api.groups.room.get_by_v1_id(
v1_id
) or api.groups.zone.get_by_v1_id(v1_id)
if hue_group is None or hue_group.grouped_light is None:
# this may happen if we're looking at some orphaned entity
LOGGER.warning(
+2 -2
View File
@@ -96,8 +96,8 @@ class HueSceneEntity(HueBaseEntity, SceneEntity):
"""Activate Hue scene."""
transition = kwargs.get("transition")
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
dynamic = kwargs.get("dynamic", self.is_dynamic)
await self.bridge.async_request_call(
self.controller.recall,
+2 -1
View File
@@ -49,7 +49,8 @@ async def async_setup_devices(bridge: "HueBridge"):
params[ATTR_IDENTIFIERS].add((DOMAIN, api.config.bridge_id))
else:
params[ATTR_VIA_DEVICE] = (DOMAIN, api.config.bridge_device.id)
if zigbee := dev_controller.get_zigbee_connectivity(hue_device.id):
zigbee = dev_controller.get_zigbee_connectivity(hue_device.id)
if zigbee and zigbee.mac_address:
params[ATTR_CONNECTIONS] = {
(device_registry.CONNECTION_NETWORK_MAC, zigbee.mac_address)
}
@@ -40,6 +40,19 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
}
)
DEFAULT_BUTTON_EVENT_TYPES = (
# all except `DOUBLE_SHORT_RELEASE`
ButtonEvent.INITIAL_PRESS,
ButtonEvent.REPEAT,
ButtonEvent.SHORT_RELEASE,
ButtonEvent.LONG_RELEASE,
)
DEVICE_SPECIFIC_EVENT_TYPES = {
# device specific overrides of specific supported button events
"Hue tap switch": (ButtonEvent.INITIAL_PRESS,),
}
async def async_validate_trigger_config(
bridge: "HueBridge",
@@ -84,18 +97,21 @@ async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry):
hue_dev_id = get_hue_device_id(device_entry)
# extract triggers from all button resources of this Hue device
triggers = []
model_id = api.devices[hue_dev_id].product_data.product_name
for resource in api.devices.get_sensors(hue_dev_id):
if resource.type != ResourceTypes.BUTTON:
continue
for event_type in (x.value for x in ButtonEvent if x != ButtonEvent.UNKNOWN):
for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get(
model_id, DEFAULT_BUTTON_EVENT_TYPES
):
triggers.append(
{
CONF_DEVICE_ID: device_entry.id,
CONF_DOMAIN: DOMAIN,
CONF_PLATFORM: "device",
CONF_TYPE: event_type,
CONF_TYPE: event_type.value,
CONF_SUBTYPE: resource.metadata.control_id,
CONF_UNIQUE_ID: device_entry.id,
CONF_UNIQUE_ID: resource.id,
}
)
return triggers
+4 -1
View File
@@ -64,7 +64,7 @@ class HueBaseEntity(Entity):
type_title = RESOURCE_TYPE_NAMES.get(
self.resource.type, self.resource.type.value.replace("_", " ").title()
)
return f"{dev_name}: {type_title}"
return f"{dev_name} {type_title}"
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
@@ -103,6 +103,9 @@ class HueBaseEntity(Entity):
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
# the zigbee connectivity sensor itself should be always available
return True
if self.device.product_data.manufacturer_name != "Signify Netherlands B.V.":
# availability status for non-philips brand lights is unreliable
return True
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
# all device-attached entities get availability from the zigbee connectivity
return zigbee.status == ConnectivityServiceStatus.CONNECTED
+12 -7
View File
@@ -7,7 +7,6 @@ from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from homeassistant.components.group.light import LightGroup
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -18,13 +17,14 @@ from homeassistant.components.light import (
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
SUPPORT_TRANSITION,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
from .entity import HueBaseEntity
ALLOWED_ERRORS = [
@@ -73,11 +73,10 @@ async def async_setup_entry(
)
class GroupedHueLight(HueBaseEntity, LightGroup):
class GroupedHueLight(HueBaseEntity, LightEntity):
"""Representation of a Grouped Hue light."""
# Entities for Hue groups are disabled by default
_attr_entity_registry_enabled_default = False
_attr_icon = "mdi:lightbulb-group"
def __init__(
self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone
@@ -91,6 +90,12 @@ class GroupedHueLight(HueBaseEntity, LightGroup):
self.api: HueBridgeV2 = bridge.api
self._attr_supported_features |= SUPPORT_TRANSITION
# Entities for Hue groups are disabled by default
# unless they were enabled in old version (legacy option)
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
CONF_ALLOW_HUE_GROUPS, False
)
self._update_values()
async def async_added_to_hass(self) -> None:
@@ -145,8 +150,8 @@ class GroupedHueLight(HueBaseEntity, LightGroup):
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
+4 -4
View File
@@ -158,8 +158,8 @@ class HueLight(HueBaseEntity, LightEntity):
# Hue uses a range of [0, 100] to control brightness.
brightness = float((brightness / 255) * 100)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
await self.bridge.async_request_call(
self.controller.set_state,
@@ -176,8 +176,8 @@ class HueLight(HueBaseEntity, LightEntity):
"""Turn the light off."""
transition = kwargs.get(ATTR_TRANSITION)
if transition is not None:
# hue transition duration is in steps of 100 ms
transition = int(transition * 100)
# hue transition duration is in milliseconds
transition = int(transition * 1000)
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
@@ -2,7 +2,7 @@
"domain": "hunterdouglas_powerview",
"name": "Hunter Douglas PowerView",
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
"requirements": ["aiopvapi==1.6.14"],
"requirements": ["aiopvapi==1.6.19"],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {
@@ -116,7 +116,7 @@ class HVVDepartureSensor(SensorEntity):
departure_time
+ timedelta(minutes=departure["timeOffset"])
+ timedelta(seconds=delay)
).isoformat()
)
self.attr.update(
{
+1 -1
View File
@@ -83,4 +83,4 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
_LOGGER.debug("New cycle time: %s", next_cycle)
self._attr_native_value = dt.utc_from_timestamp(
dt.as_timestamp(dt.now()) + next_cycle
).isoformat()
)
@@ -45,10 +45,8 @@ class IslamicPrayerTimeSensor(SensorEntity):
@property
def native_value(self):
"""Return the state of the sensor."""
return (
self.client.prayer_times_info.get(self.sensor_type)
.astimezone(dt_util.UTC)
.isoformat()
return self.client.prayer_times_info.get(self.sensor_type).astimezone(
dt_util.UTC
)
async def async_added_to_hass(self):
+1
View File
@@ -390,6 +390,7 @@ class KNXModule:
connection_type=ConnectionType.TUNNELING,
gateway_ip=self.config[CONF_HOST],
gateway_port=self.config[CONF_PORT],
local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP),
route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False),
auto_reconnect=True,
)
@@ -121,6 +121,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
ConnectionSchema.CONF_KNX_ROUTE_BACK: user_input[
ConnectionSchema.CONF_KNX_ROUTE_BACK
],
ConnectionSchema.CONF_KNX_LOCAL_IP: user_input.get(
ConnectionSchema.CONF_KNX_LOCAL_IP
),
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING,
},
)
@@ -134,6 +137,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK, default=False
): vol.Coerce(bool),
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
}
return self.async_show_form(
@@ -243,6 +247,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
**DEFAULT_ENTRY_DATA,
CONF_HOST: config[CONF_KNX_TUNNELING][CONF_HOST],
CONF_PORT: config[CONF_KNX_TUNNELING][CONF_PORT],
ConnectionSchema.CONF_KNX_LOCAL_IP: config[CONF_KNX_TUNNELING].get(
ConnectionSchema.CONF_KNX_LOCAL_IP
),
ConnectionSchema.CONF_KNX_ROUTE_BACK: config[CONF_KNX_TUNNELING][
ConnectionSchema.CONF_KNX_ROUTE_BACK
],
@@ -299,6 +306,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
): cv.port,
vol.Optional(ConnectionSchema.CONF_KNX_LOCAL_IP): str,
vol.Required(
ConnectionSchema.CONF_KNX_ROUTE_BACK,
default=self.current_config.get(
+4 -2
View File
@@ -19,7 +19,8 @@
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"individual_address": "Individual address for the connection",
"route_back": "Route Back / NAT Mode"
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
}
},
"routing": {
@@ -55,7 +56,8 @@
"data": {
"port": "[%key:common::config_flow::data::port%]",
"host": "[%key:common::config_flow::data::host%]",
"route_back": "Route Back / NAT Mode"
"route_back": "Route Back / NAT Mode",
"local_ip": "Local IP (leave empty if unsure)"
}
}
}
@@ -12,6 +12,7 @@
"data": {
"host": "Host",
"individual_address": "Individual address for the connection",
"local_ip": "Local IP (leave empty if unsure)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
},
@@ -54,6 +55,7 @@
"tunnel": {
"data": {
"host": "Host",
"local_ip": "Local IP (leave empty if unsure)",
"port": "Port",
"route_back": "Route Back / NAT Mode"
}
@@ -1,9 +1,11 @@
"""Support for Litter-Robot sensors."""
from __future__ import annotations
from datetime import datetime
from pylitterbot.robot import Robot
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import SensorEntity, StateType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from homeassistant.core import HomeAssistant
@@ -36,7 +38,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity):
self.sensor_attribute = sensor_attribute
@property
def native_value(self) -> str:
def native_value(self) -> StateType | datetime:
"""Return the state."""
return getattr(self.robot, self.sensor_attribute)
@@ -59,10 +61,10 @@ class LitterRobotSleepTimeSensor(LitterRobotPropertySensor):
"""Litter-Robot sleep time sensor."""
@property
def native_value(self) -> str | None:
def native_value(self) -> StateType | datetime:
"""Return the state."""
if self.robot.sleep_mode_enabled:
return super().native_value.isoformat()
return super().native_value
return None
@property
+2 -1
View File
@@ -1,6 +1,7 @@
"""The lookin integration."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
@@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
lookin_device = await lookin_protocol.get_info()
devices = await lookin_protocol.get_devices()
except aiohttp.ClientError as ex:
except (asyncio.TimeoutError, aiohttp.ClientError) as ex:
raise ConfigEntryNotReady from ex
meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
@@ -10,6 +10,7 @@ from aiolookin import Climate, MeteoSensor, SensorID
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
ATTR_HVAC_MODE,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
@@ -151,6 +152,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return
self._climate.temp_celsius = int(temperature)
lookin_index = LOOKIN_HVAC_MODE_IDX_TO_HASS
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
self._climate.hvac_mode = HASS_TO_LOOKIN_HVAC_MODE[hvac_mode]
elif self._climate.hvac_mode == lookin_index.index(HVAC_MODE_OFF):
#
# If the device is off, and the user didn't specify an HVAC mode
# (which is the default when using the HA UI), the device won't turn
# on without having an HVAC mode passed.
#
# We picked the hvac mode based on the current temp if its available
# since only some units support auto, but most support either heat
# or cool otherwise we set auto since we don't have a way to make
# an educated guess.
#
meteo_data: MeteoSensor = self._meteo_coordinator.data
current_temp = meteo_data.temperature
if not current_temp:
self._climate.hvac_mode = lookin_index.index(HVAC_MODE_AUTO)
elif current_temp >= self._climate.temp_celsius:
self._climate.hvac_mode = lookin_index.index(HVAC_MODE_COOL)
else:
self._climate.hvac_mode = lookin_index.index(HVAC_MODE_HEAT)
await self._async_update_conditioner()
async def async_set_fan_mode(self, fan_mode: str) -> None:
@@ -1 +0,0 @@
"""The loopenergy component."""
@@ -1,8 +0,0 @@
{
"domain": "loopenergy",
"name": "Loop Energy",
"documentation": "https://www.home-assistant.io/integrations/loopenergy",
"requirements": ["pyloopenergy==0.2.1"],
"codeowners": ["@pavoni"],
"iot_class": "cloud_push"
}
@@ -1,149 +0,0 @@
"""Support for Loop Energy sensors."""
import logging
import pyloopenergy
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_ELEC = "electricity"
CONF_GAS = "gas"
CONF_ELEC_SERIAL = "electricity_serial"
CONF_ELEC_SECRET = "electricity_secret"
CONF_GAS_SERIAL = "gas_serial"
CONF_GAS_SECRET = "gas_secret"
CONF_GAS_CALORIFIC = "gas_calorific"
CONF_GAS_TYPE = "gas_type"
DEFAULT_CALORIFIC = 39.11
DEFAULT_UNIT = "kW"
ELEC_SCHEMA = vol.Schema(
{
vol.Required(CONF_ELEC_SERIAL): cv.string,
vol.Required(CONF_ELEC_SECRET): cv.string,
}
)
GAS_TYPE_SCHEMA = vol.In([CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL])
GAS_SCHEMA = vol.Schema(
{
vol.Required(CONF_GAS_SERIAL): cv.string,
vol.Required(CONF_GAS_SECRET): cv.string,
vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA,
vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): vol.Coerce(float),
}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_ELEC): ELEC_SCHEMA, vol.Optional(CONF_GAS): GAS_SCHEMA}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Loop Energy sensors."""
elec_config = config.get(CONF_ELEC)
gas_config = config.get(CONF_GAS, {})
controller = pyloopenergy.LoopEnergy(
elec_config.get(CONF_ELEC_SERIAL),
elec_config.get(CONF_ELEC_SECRET),
gas_config.get(CONF_GAS_SERIAL),
gas_config.get(CONF_GAS_SECRET),
gas_config.get(CONF_GAS_TYPE),
gas_config.get(CONF_GAS_CALORIFIC),
)
def stop_loopenergy(event):
"""Shutdown loopenergy thread on exit."""
_LOGGER.info("Shutting down loopenergy")
controller.terminate()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_loopenergy)
sensors = [LoopEnergyElec(controller)]
if gas_config.get(CONF_GAS_SERIAL):
sensors.append(LoopEnergyGas(controller))
add_entities(sensors)
class LoopEnergySensor(SensorEntity):
"""Implementation of an Loop Energy base sensor."""
def __init__(self, controller):
"""Initialize the sensor."""
self._state = None
self._unit_of_measurement = DEFAULT_UNIT
self._controller = controller
self._name = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def _callback(self):
self.schedule_update_ha_state(True)
class LoopEnergyElec(LoopEnergySensor):
"""Implementation of an Loop Energy Electricity sensor."""
def __init__(self, controller):
"""Initialize the sensor."""
super().__init__(controller)
self._name = "Power Usage"
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._controller.subscribe_elecricity(self._callback)
def update(self):
"""Get the cached Loop energy reading."""
self._state = round(self._controller.electricity_useage, 2)
class LoopEnergyGas(LoopEnergySensor):
"""Implementation of an Loop Energy Gas sensor."""
def __init__(self, controller):
"""Initialize the sensor."""
super().__init__(controller)
self._name = "Gas Usage"
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._controller.subscribe_gas(self._callback)
def update(self):
"""Get the cached Loop gas reading."""
self._state = round(self._controller.gas_useage, 2)
@@ -24,7 +24,6 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
@@ -175,11 +174,9 @@ async def async_setup_entry(hass, config_entry):
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
return False
session = async_get_clientsession(hass)
try:
luftdaten = LuftDatenData(
Luftdaten(config_entry.data[CONF_SENSOR_ID], hass.loop, session),
Luftdaten(config_entry.data[CONF_SENSOR_ID]),
config_entry.data.get(CONF_SENSORS, {}).get(
CONF_MONITORED_CONDITIONS, SENSOR_KEYS
),
@@ -13,7 +13,6 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP,
)
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN
@@ -69,8 +68,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if sensor_id in configured_sensors(self.hass):
return self._show_form({CONF_SENSOR_ID: "already_configured"})
session = aiohttp_client.async_get_clientsession(self.hass)
luftdaten = Luftdaten(user_input[CONF_SENSOR_ID], self.hass.loop, session)
luftdaten = Luftdaten(user_input[CONF_SENSOR_ID])
try:
await luftdaten.get_data()
valid = await luftdaten.validate_sensor()
@@ -3,7 +3,7 @@
"name": "Luftdaten",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/luftdaten",
"requirements": ["luftdaten==0.6.5"],
"requirements": ["luftdaten==0.7.1"],
"codeowners": ["@fabaff"],
"quality_scale": "gold",
"iot_class": "cloud_polling"
@@ -1,4 +1,5 @@
"""Support for Lupusec Home Security system."""
# pylint: disable=import-error
import logging
import lupupy
@@ -1,4 +1,5 @@
"""Support for Lupusec Security System binary sensors."""
# pylint: disable=import-error
from datetime import timedelta
import lupupy.constants as CONST
@@ -1,4 +1,5 @@
{
"disabled": "Library has incompatible requirements.",
"domain": "lupusec",
"name": "Lupus Electronics LUPUSEC",
"documentation": "https://www.home-assistant.io/integrations/lupusec",
@@ -1,4 +1,5 @@
"""Support for Lupusec Security System switches."""
# pylint: disable=import-error
from datetime import timedelta
import lupupy.constants as CONST
@@ -68,7 +68,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
hostname = discovery_info.hostname
if hostname is None or not hostname.startswith("lutron-"):
if hostname is None or not hostname.lower().startswith("lutron-"):
return self.async_abort(reason="not_lutron_device")
self.lutron_id = hostname.split("-")[1].replace(".local.", "")
+2 -2
View File
@@ -47,7 +47,7 @@ LYRIC_SETPOINT_STATUS_NAMES = {
class LyricSensorEntityDescription(SensorEntityDescription):
"""Class describing Honeywell Lyric sensor entities."""
value: Callable[[LyricDevice], StateType] = round
value: Callable[[LyricDevice], StateType | datetime] = round
def get_datetime_from_future_time(time: str) -> datetime:
@@ -133,7 +133,7 @@ async def async_setup_entry(
device_class=DEVICE_CLASS_TIMESTAMP,
value=lambda device: get_datetime_from_future_time(
device.changeableValues.nextPeriodTime
).isoformat(),
),
),
location,
device,
@@ -142,11 +142,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
(cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1),
None,
)
return (
dt_util.utc_from_timestamp(next_rain["dt"]).isoformat()
if next_rain
else None
)
return dt_util.utc_from_timestamp(next_rain["dt"]) if next_rain else None
@property
def extra_state_attributes(self):
@@ -24,8 +24,6 @@ DOMAIN = "metoffice"
DEFAULT_NAME = "Met Office"
ATTRIBUTION = "Data provided by the Met Office"
ATTR_FORECAST_DAYTIME = "daytime"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
METOFFICE_COORDINATES = "metoffice_coordinates"
@@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import get_device_info
from .const import (
ATTR_FORECAST_DAYTIME,
ATTRIBUTION,
CONDITION_CLASSES,
DEFAULT_NAME,
@@ -47,7 +46,7 @@ async def async_setup_entry(
)
def _build_forecast_data(timestep, use_3hourly):
def _build_forecast_data(timestep):
data = {}
data[ATTR_FORECAST_TIME] = timestep.date.isoformat()
if timestep.weather:
@@ -60,9 +59,6 @@ def _build_forecast_data(timestep, use_3hourly):
data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value
if timestep.wind_speed:
data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value
if not use_3hourly:
# if it's close to noon, mark as Day, otherwise as Night
data[ATTR_FORECAST_DAYTIME] = abs(timestep.date.hour - 12) < 6
return data
@@ -86,7 +82,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity):
)
self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}"
self._attr_unique_id = hass_data[METOFFICE_COORDINATES]
self._use_3hourly = use_3hourly
if not use_3hourly:
self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}"
@@ -160,7 +155,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity):
if self.coordinator.data.forecast is None:
return None
return [
_build_forecast_data(timestep, self._use_3hourly)
_build_forecast_data(timestep)
for timestep in self.coordinator.data.forecast
]
+12 -1
View File
@@ -1,6 +1,12 @@
"""A entity class for mobile_app."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID
from homeassistant.const import (
ATTR_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_WEBHOOK_ID,
STATE_UNAVAILABLE,
)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -101,6 +107,11 @@ class MobileAppEntity(RestoreEntity):
"""Return device registry information for this entity."""
return device_info(self._registration)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE
@callback
def _handle_update(self, data):
"""Handle async event updates."""
@@ -8,6 +8,7 @@ from homeassistant.const import (
CONF_WEBHOOK_ID,
DEVICE_CLASS_DATE,
DEVICE_CLASS_TIMESTAMP,
STATE_UNKNOWN,
)
from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
@@ -88,9 +89,11 @@ class MobileAppSensor(MobileAppEntity, SensorEntity):
@property
def native_value(self):
"""Return the state of the sensor."""
if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN):
return None
if (
(state := self._config[ATTR_SENSOR_STATE]) is not None
and self.device_class
self.device_class
in (
DEVICE_CLASS_DATE,
DEVICE_CLASS_TIMESTAMP,
@@ -73,7 +73,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor):
self._attr_device_class = DEVICE_CLASS_TIMESTAMP
@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
sleep_time: datetime = dt_util.utc_from_timestamp(
self.coordinator.data.state.light_sleep_timer
@@ -83,7 +83,7 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor):
or (sleep_time - dt_util.utcnow()).total_seconds() < 0
):
return None
return sleep_time.isoformat()
return sleep_time
class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor):
@@ -103,7 +103,7 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor):
self._attr_device_class = DEVICE_CLASS_TIMESTAMP
@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
sleep_time: datetime = dt_util.utc_from_timestamp(
self.coordinator.data.state.fan_sleep_timer
@@ -115,4 +115,4 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor):
):
return None
return sleep_time.isoformat()
return sleep_time
+9 -1
View File
@@ -86,6 +86,11 @@ PLATFORMS = ["sensor", "camera", "climate"]
WEB_AUTH_DOMAIN = DOMAIN
INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed"
# Fetch media for events with an in memory cache. The largest media items
# are mp4 clips at ~90kb each, so this totals a few MB per camera.
# Note: Media for events can only be published within 30 seconds of the event
EVENT_MEDIA_CACHE_SIZE = 64
class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
"""OAuth implementation using OAuth for web applications."""
@@ -192,7 +197,7 @@ class SignalUpdateCallback:
"device_id": device_entry.id,
"type": event_type,
"timestamp": event_message.timestamp,
"nest_event_id": image_event.event_id,
"nest_event_id": image_event.event_session_id,
}
self._hass.bus.async_fire(NEST_EVENT, message)
@@ -206,6 +211,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
subscriber = await api.new_subscriber(hass, entry)
if not subscriber:
return False
# Keep media for last N events in memory
subscriber.cache_policy.event_cache_size = EVENT_MEDIA_CACHE_SIZE
subscriber.cache_policy.fetch = True
callback = SignalUpdateCallback(hass)
subscriber.set_update_callback(callback.async_handle_event)
+1 -1
View File
@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.0"],
"requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.6"],
"codeowners": ["@allenporter"],
"quality_scale": "platinum",
"dhcp": [
+12 -11
View File
@@ -18,14 +18,13 @@ https://developers.google.com/nest/device-access/api/camera#handle_camera_events
from __future__ import annotations
from collections import OrderedDict
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait
from google_nest_sdm.device import Device
from google_nest_sdm.event import ImageEventBase
from google_nest_sdm.event import EventImageType, ImageEventBase
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
@@ -46,7 +45,8 @@ from homeassistant.components.nest.const import DATA_SUBSCRIBER, DOMAIN
from homeassistant.components.nest.device_info import NestDeviceInfo
from homeassistant.components.nest.events import MEDIA_SOURCE_EVENT_TITLE_MAP
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -137,7 +137,7 @@ class NestMediaSource(MediaSource):
raise Unresolvable(
"Unable to find device with identifier: %s" % item.identifier
)
events = _get_events(device)
events = await _get_events(device)
if media_id.event_id not in events:
raise Unresolvable(
"Unable to find event with identifier: %s" % item.identifier
@@ -180,16 +180,16 @@ class NestMediaSource(MediaSource):
# Browse a specific device and return child events
browse_device = _browse_device(media_id, device)
browse_device.children = []
events = _get_events(device)
events = await _get_events(device)
for child_event in events.values():
event_id = MediaId(media_id.device_id, child_event.event_id)
event_id = MediaId(media_id.device_id, child_event.event_session_id)
browse_device.children.append(
_browse_event(event_id, device, child_event)
)
return browse_device
# Browse a specific event
events = _get_events(device)
events = await _get_events(device)
if not (event := events.get(media_id.event_id)):
raise BrowseError(
"Unable to find event with identiifer: %s" % item.identifier
@@ -201,9 +201,10 @@ class NestMediaSource(MediaSource):
return await get_media_source_devices(self.hass)
def _get_events(device: Device) -> Mapping[str, ImageEventBase]:
async def _get_events(device: Device) -> Mapping[str, ImageEventBase]:
"""Return relevant events for the specified device."""
return OrderedDict({e.event_id: e for e in device.event_media_manager.events})
events = await device.event_media_manager.async_events()
return {e.event_session_id: e for e in events}
def _browse_root() -> BrowseMediaSource:
@@ -250,9 +251,9 @@ def _browse_event(
media_content_type=MEDIA_TYPE_IMAGE,
title=CLIP_TITLE_FORMAT.format(
event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"),
event_time=dt_util.as_local(event.timestamp),
event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT),
),
can_play=True,
can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW),
can_expand=False,
thumbnail=None,
children=[],
+8 -3
View File
@@ -135,9 +135,14 @@ async def async_setup_entry(
entities = []
for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
try:
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
except KeyError:
continue
climate_state = data_handler.data[signal_name]
climate_topology.register_handler(home_id, climate_state.process_topology)
@@ -194,7 +194,11 @@ class NetatmoDataHandler:
self._auth, **kwargs
)
await self.async_fetch_data(data_class_entry)
try:
await self.async_fetch_data(data_class_entry)
except KeyError:
self.data_classes.pop(data_class_entry)
raise
self._queue.append(self.data_classes[data_class_entry])
_LOGGER.debug("Data class %s added", data_class_entry)
@@ -48,6 +48,14 @@ async def async_setup_entry(
entities = []
for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
try:
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
except KeyError:
continue
await data_handler.register_data_class(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
)
@@ -2,7 +2,7 @@
"domain": "netdata",
"name": "Netdata",
"documentation": "https://www.home-assistant.io/integrations/netdata",
"requirements": ["netdata==0.2.0"],
"requirements": ["netdata==1.0.1"],
"codeowners": ["@fabaff"],
"iot_class": "local_polling"
}
+1 -3
View File
@@ -16,7 +16,6 @@ from homeassistant.const import (
PERCENTAGE,
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@@ -61,8 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
port = config.get(CONF_PORT)
resources = config.get(CONF_RESOURCES)
session = async_get_clientsession(hass)
netdata = NetdataData(Netdata(host, hass.loop, session, port=port))
netdata = NetdataData(Netdata(host, port=port))
await netdata.async_update()
if netdata.api.metrics is None:
+1 -3
View File
@@ -218,6 +218,4 @@ class NextBusDepartureSensor(SensorEntity):
)
latest_prediction = maybe_first(predictions)
self._state = utc_from_timestamp(
int(latest_prediction["epochTime"]) / 1000
).isoformat()
self._state = utc_from_timestamp(int(latest_prediction["epochTime"]) / 1000)
+1 -1
View File
@@ -127,6 +127,6 @@ class NZBGetSensor(NZBGetEntity, SensorEntity):
if "UpTimeSec" in sensor_type and value > 0:
uptime = utcnow() - timedelta(seconds=value)
return uptime.replace(microsecond=0).isoformat()
return uptime.replace(microsecond=0)
return value
@@ -120,7 +120,7 @@ class OASATelematicsSensor(SensorEntity):
self._name_data = self.data.name_data
next_arrival_data = self._times[0]
if ATTR_NEXT_ARRIVAL in next_arrival_data:
self._state = next_arrival_data[ATTR_NEXT_ARRIVAL].isoformat()
self._state = next_arrival_data[ATTR_NEXT_ARRIVAL]
class OASATelematicsData:
+1 -1
View File
@@ -185,7 +185,7 @@ class MinutPointClient:
async def _sync(self):
"""Update local list of devices."""
if not await self._client.update() and self._is_available:
if not await self._client.update():
self._is_available = False
_LOGGER.warning("Device is unavailable")
async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)
+1 -1
View File
@@ -27,7 +27,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
_ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp"
_ENDPOINT = "https://pvoutput.org/service/r2/getstatus.jsp"
ATTR_ENERGY_GENERATION = "energy_generation"
ATTR_POWER_GENERATION = "power_generation"
@@ -277,6 +277,8 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
if not self.coordinator.data[self.entity_description.uid]["active"]:
self._attr_is_on = False
self.async_write_ha_state()
raise HomeAssistantError(
f"Cannot turn on an inactive program/zone: {self.name}"
)
+91 -9
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable
import concurrent.futures
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import queue
@@ -76,6 +77,7 @@ from .util import (
session_scope,
setup_connection_for_dialect,
validate_or_move_away_sqlite_database,
write_lock_db,
)
_LOGGER = logging.getLogger(__name__)
@@ -123,6 +125,9 @@ KEEPALIVE_TIME = 30
# States and Events objects
EXPIRE_AFTER_COMMITS = 120
DB_LOCK_TIMEOUT = 30
DB_LOCK_QUEUE_CHECK_TIMEOUT = 1
CONF_AUTO_PURGE = "auto_purge"
CONF_DB_URL = "db_url"
CONF_DB_MAX_RETRIES = "db_max_retries"
@@ -370,6 +375,15 @@ class WaitTask:
"""An object to insert into the recorder queue to tell it set the _queue_watch event."""
@dataclass
class DatabaseLockTask:
"""An object to insert into the recorder queue to prevent writes to the database."""
database_locked: asyncio.Event
database_unlock: threading.Event
queue_overflow: bool
class Recorder(threading.Thread):
"""A threaded recorder class."""
@@ -419,6 +433,7 @@ class Recorder(threading.Thread):
self.migration_in_progress = False
self._queue_watcher = None
self._db_supports_row_number = True
self._database_lock_task: DatabaseLockTask | None = None
self.enabled = True
@@ -687,6 +702,8 @@ class Recorder(threading.Thread):
def _process_one_event_or_recover(self, event):
"""Process an event, reconnect, or recover a malformed database."""
try:
if self._process_one_task(event):
return
self._process_one_event(event)
return
except exc.DatabaseError as err:
@@ -788,34 +805,63 @@ class Recorder(threading.Thread):
# Schedule a new statistics task if this one didn't finish
self.queue.put(ExternalStatisticsTask(metadata, stats))
def _process_one_event(self, event):
def _lock_database(self, task: DatabaseLockTask):
@callback
def _async_set_database_locked(task: DatabaseLockTask):
task.database_locked.set()
with write_lock_db(self):
# Notify that lock is being held, wait until database can be used again.
self.hass.add_job(_async_set_database_locked, task)
while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT):
if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9:
_LOGGER.warning(
"Database queue backlog reached more than 90% of maximum queue "
"length while waiting for backup to finish; recorder will now "
"resume writing to database. The backup can not be trusted and "
"must be restarted"
)
task.queue_overflow = True
break
_LOGGER.info(
"Database queue backlog reached %d entries during backup",
self.queue.qsize(),
)
def _process_one_task(self, event) -> bool:
"""Process one event."""
if isinstance(event, PurgeTask):
self._run_purge(event.purge_before, event.repack, event.apply_filter)
return
return True
if isinstance(event, PurgeEntitiesTask):
self._run_purge_entities(event.entity_filter)
return
return True
if isinstance(event, PerodicCleanupTask):
perodic_db_cleanups(self)
return
return True
if isinstance(event, StatisticsTask):
self._run_statistics(event.start)
return
return True
if isinstance(event, ClearStatisticsTask):
statistics.clear_statistics(self, event.statistic_ids)
return
return True
if isinstance(event, UpdateStatisticsMetadataTask):
statistics.update_statistics_metadata(
self, event.statistic_id, event.unit_of_measurement
)
return
return True
if isinstance(event, ExternalStatisticsTask):
self._run_external_statistics(event.metadata, event.statistics)
return
return True
if isinstance(event, WaitTask):
self._queue_watch.set()
return
return True
if isinstance(event, DatabaseLockTask):
self._lock_database(event)
return True
return False
def _process_one_event(self, event):
if event.event_type == EVENT_TIME_CHANGED:
self._keepalive_count += 1
if self._keepalive_count >= KEEPALIVE_TIME:
@@ -982,6 +1028,42 @@ class Recorder(threading.Thread):
self.queue.put(WaitTask())
self._queue_watch.wait()
async def lock_database(self) -> bool:
"""Lock database so it can be backed up safely."""
if self._database_lock_task:
_LOGGER.warning("Database already locked")
return False
database_locked = asyncio.Event()
task = DatabaseLockTask(database_locked, threading.Event(), False)
self.queue.put(task)
try:
await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT)
except asyncio.TimeoutError as err:
task.database_unlock.set()
raise TimeoutError(
f"Could not lock database within {DB_LOCK_TIMEOUT} seconds."
) from err
self._database_lock_task = task
return True
@callback
def unlock_database(self) -> bool:
"""Unlock database.
Returns true if database lock has been held throughout the process.
"""
if not self._database_lock_task:
_LOGGER.warning("Database currently not locked")
return False
self._database_lock_task.database_unlock.set()
success = not self._database_lock_task.queue_overflow
self._database_lock_task = None
return success
def _setup_connection(self):
"""Ensure database is ready to fly."""
kwargs = {}
@@ -834,8 +834,12 @@ def statistics_during_period(
return _reduce_statistics_per_month(result)
def get_last_statistics(
hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool
def _get_last_statistics(
hass: HomeAssistant,
number_of_stats: int,
statistic_id: str,
convert_units: bool,
table: type[Statistics | StatisticsShortTerm],
) -> dict[str, list[dict]]:
"""Return the last number_of_stats statistics for a given statistic_id."""
statistic_ids = [statistic_id]
@@ -845,16 +849,19 @@ def get_last_statistics(
if not metadata:
return {}
baked_query = hass.data[STATISTICS_SHORT_TERM_BAKERY](
lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM)
)
if table == StatisticsShortTerm:
bakery = STATISTICS_SHORT_TERM_BAKERY
base_query = QUERY_STATISTICS_SHORT_TERM
else:
bakery = STATISTICS_BAKERY
base_query = QUERY_STATISTICS
baked_query = hass.data[bakery](lambda session: session.query(*base_query))
baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id"))
metadata_id = metadata[statistic_id][0]
baked_query += lambda q: q.order_by(
StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()
)
baked_query += lambda q: q.order_by(table.metadata_id, table.start.desc())
baked_query += lambda q: q.limit(bindparam("number_of_stats"))
@@ -874,11 +881,29 @@ def get_last_statistics(
statistic_ids,
metadata,
convert_units,
StatisticsShortTerm,
table,
None,
)
def get_last_statistics(
hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool
) -> dict[str, list[dict]]:
"""Return the last number_of_stats statistics for a statistic_id."""
return _get_last_statistics(
hass, number_of_stats, statistic_id, convert_units, Statistics
)
def get_last_short_term_statistics(
hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool
) -> dict[str, list[dict]]:
"""Return the last number_of_stats short term statistics for a statistic_id."""
return _get_last_statistics(
hass, number_of_stats, statistic_id, convert_units, StatisticsShortTerm
)
def _statistics_at_time(
session: scoped_session,
metadata_ids: set[int],
+19
View File
@@ -457,6 +457,25 @@ def perodic_db_cleanups(instance: Recorder):
connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE);"))
@contextmanager
def write_lock_db(instance: Recorder):
"""Lock database for writes."""
if instance.engine.dialect.name == "sqlite":
with instance.engine.connect() as connection:
# Execute sqlite to create a wal checkpoint
# This is optional but makes sure the backup is going to be minimal
connection.execute(text("PRAGMA wal_checkpoint(TRUNCATE)"))
# Create write lock
_LOGGER.debug("Lock database")
connection.execute(text("BEGIN IMMEDIATE;"))
try:
yield
finally:
_LOGGER.debug("Unlock database")
connection.execute(text("END;"))
def async_migration_in_progress(hass: HomeAssistant) -> bool:
"""Determine is a migration is in progress.
@@ -1,6 +1,7 @@
"""The Energy websocket API."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import voluptuous as vol
@@ -15,6 +16,8 @@ from .util import async_migration_in_progress
if TYPE_CHECKING:
from . import Recorder
_LOGGER: logging.Logger = logging.getLogger(__package__)
@callback
def async_setup(hass: HomeAssistant) -> None:
@@ -23,6 +26,8 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_clear_statistics)
websocket_api.async_register_command(hass, ws_update_statistics_metadata)
websocket_api.async_register_command(hass, ws_info)
websocket_api.async_register_command(hass, ws_backup_start)
websocket_api.async_register_command(hass, ws_backup_end)
@websocket_api.websocket_command(
@@ -106,3 +111,38 @@ def ws_info(
"thread_running": thread_alive,
}
connection.send_result(msg["id"], recorder_info)
@websocket_api.ws_require_user(only_supervisor=True)
@websocket_api.websocket_command({vol.Required("type"): "backup/start"})
@websocket_api.async_response
async def ws_backup_start(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Backup start notification."""
_LOGGER.info("Backup start notification, locking database for writes")
instance: Recorder = hass.data[DATA_INSTANCE]
try:
await instance.lock_database()
except TimeoutError as err:
connection.send_error(msg["id"], "timeout_error", str(err))
return
connection.send_result(msg["id"])
@websocket_api.ws_require_user(only_supervisor=True)
@websocket_api.websocket_command({vol.Required("type"): "backup/end"})
@websocket_api.async_response
async def ws_backup_end(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Backup end notification."""
instance: Recorder = hass.data[DATA_INSTANCE]
_LOGGER.info("Backup end notification, releasing write lock")
if not instance.unlock_database():
connection.send_error(
msg["id"], "database_unlock_failed", "Failed to unlock database."
)
connection.send_result(msg["id"])
+2 -2
View File
@@ -160,7 +160,7 @@ class RepetierJobEndSensor(RepetierSensor):
print_time = data["print_time"]
from_start = data["from_start"]
time_end = start + round(print_time, 0)
self._state = datetime.utcfromtimestamp(time_end).isoformat()
self._state = datetime.utcfromtimestamp(time_end)
remaining = print_time - from_start
remaining_secs = int(round(remaining, 0))
_LOGGER.debug(
@@ -182,7 +182,7 @@ class RepetierJobStartSensor(RepetierSensor):
job_name = data["job_name"]
start = data["start"]
from_start = data["from_start"]
self._state = datetime.utcfromtimestamp(start).isoformat()
self._state = datetime.utcfromtimestamp(start)
elapsed_secs = int(round(from_start, 0))
_LOGGER.debug(
"Job %s elapsed %s",
+3 -3
View File
@@ -25,7 +25,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery
from homeassistant.helpers import discovery, template
from homeassistant.helpers.entity_component import (
DEFAULT_SCAN_INTERVAL,
EntityComponent,
@@ -37,7 +37,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX
from .data import RestData
from .schema import CONFIG_SCHEMA # noqa: F401
from .utils import inject_hass_in_templates_list
_LOGGER = logging.getLogger(__name__)
@@ -161,7 +160,8 @@ def create_rest_data_from_config(hass, config):
resource_template.hass = hass
resource = resource_template.async_render(parse_result=False)
inject_hass_in_templates_list(hass, [headers, params])
template.attach(hass, headers)
template.attach(hass, params)
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
+4 -3
View File
@@ -3,7 +3,7 @@ import logging
import httpx
from homeassistant.components.rest.utils import render_templates
from homeassistant.helpers import template
from homeassistant.helpers.httpx_client import get_async_client
DEFAULT_TIMEOUT = 10
@@ -52,8 +52,8 @@ class RestData:
self._hass, verify_ssl=self._verify_ssl
)
rendered_headers = render_templates(self._headers)
rendered_params = render_templates(self._params)
rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params)
_LOGGER.debug("Updating from %s", self._resource)
try:
@@ -65,6 +65,7 @@ class RestData:
auth=self._auth,
data=self._request_data,
timeout=self._timeout,
follow_redirects=True,
)
self.data = response.text
self.headers = response.headers
+12 -1
View File
@@ -11,8 +11,10 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
)
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
@@ -186,4 +188,13 @@ class RestSensor(RestEntity, SensorEntity):
value, None
)
self._state = value
if value is None or self.device_class not in (
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
):
self._state = value
return
self._state = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
+8 -8
View File
@@ -24,10 +24,8 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .utils import inject_hass_in_templates_list, render_templates
_LOGGER = logging.getLogger(__name__)
CONF_BODY_OFF = "body_off"
@@ -92,7 +90,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
body_on.hass = hass
if body_off is not None:
body_off.hass = hass
inject_hass_in_templates_list(hass, [headers, params])
template.attach(hass, headers)
template.attach(hass, params)
timeout = config.get(CONF_TIMEOUT)
try:
@@ -207,8 +207,8 @@ class RestSwitch(SwitchEntity):
"""Send a state update to the device."""
websession = async_get_clientsession(self.hass, self._verify_ssl)
rendered_headers = render_templates(self._headers)
rendered_params = render_templates(self._params)
rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params)
async with async_timeout.timeout(self._timeout):
req = await getattr(websession, self._method)(
@@ -233,8 +233,8 @@ class RestSwitch(SwitchEntity):
"""Get the latest data from REST API and update the state."""
websession = async_get_clientsession(hass, self._verify_ssl)
rendered_headers = render_templates(self._headers)
rendered_params = render_templates(self._params)
rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params)
async with async_timeout.timeout(self._timeout):
req = await websession.get(
-27
View File
@@ -1,27 +0,0 @@
"""Reusable utilities for the Rest component."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
def inject_hass_in_templates_list(
hass: HomeAssistant, tpl_dict_list: list[dict[str, Template] | None]
):
"""Inject hass in a list of dict of templates."""
for tpl_dict in tpl_dict_list:
if tpl_dict is not None:
for tpl in tpl_dict.values():
tpl.hass = hass
def render_templates(tpl_dict: dict[str, Template] | None):
"""Render a dict of templates."""
if tpl_dict is None:
return None
rendered_items = {}
for item_name, template_header in tpl_dict.items():
if (value := template_header.async_render()) is not None:
rendered_items[item_name] = value
return rendered_items
+1 -1
View File
@@ -65,7 +65,7 @@ async def async_setup_entry(
entity = RfxtrxCover(
event.device,
device_id,
signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS],
signal_repetitions=entity_info.get(CONF_SIGNAL_REPETITIONS, 1),
venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE),
)
entities.append(entity)
+6 -3
View File
@@ -20,6 +20,7 @@ from homeassistant.const import (
)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.util.dt import get_time_zone, now
# Config for rova requests.
CONF_ZIP_CODE = "zip_code"
@@ -116,7 +117,7 @@ class RovaSensor(SensorEntity):
self.data_service.update()
pickup_date = self.data_service.data.get(self.entity_description.key)
if pickup_date is not None:
self._attr_native_value = pickup_date.isoformat()
self._attr_native_value = pickup_date
class RovaData:
@@ -140,10 +141,12 @@ class RovaData:
self.data = {}
for item in items:
date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S")
date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace(
tzinfo=get_time_zone("Europe/Amsterdam")
)
code = item["GarbageTypeCode"].lower()
if code not in self.data and date > datetime.now():
if code not in self.data and date > now():
self.data[code] = date
_LOGGER.debug("Updated Rova calendar: %s", self.data)

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