From e08934ee4c81e5346241357828ddba3cedd1c3ed Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 30 Sep 2020 21:19:41 +0200 Subject: [PATCH 001/831] Support adding Spotify share links to the Sonos queue (#40802) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index daded59cadf..0bb15d2949e 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.33"], + "requirements": ["pysonos==0.0.34"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index 6b1087502c2..7c8643f00c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1668,7 +1668,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.33 +pysonos==0.0.34 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 751fb61f179..21e54d27754 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -806,7 +806,7 @@ pysmartthings==0.7.3 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.33 +pysonos==0.0.34 # homeassistant.components.spc pyspcwebgw==0.4.0 From c0c38fa6d47fd959026196ecfedc3ef800a4f368 Mon Sep 17 00:00:00 2001 From: Khole Date: Wed, 30 Sep 2020 23:02:42 +0100 Subject: [PATCH 002/831] Update Pyhiveapi Library Version (#40804) * Update Pyhiveapi Library Version This fixs an issue caused by a change in authentication method by hive * Update Library Version --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 060a1a0a200..f8fb9bc8c2a 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -2,6 +2,6 @@ "domain": "hive", "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.2.20.1"], + "requirements": ["pyhiveapi==0.2.20.2"], "codeowners": ["@Rendili", "@KJonline"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c8643f00c0..c782e7ad37b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1401,7 +1401,7 @@ pyheos==0.6.0 pyhik==0.2.7 # homeassistant.components.hive -pyhiveapi==0.2.20.1 +pyhiveapi==0.2.20.2 # homeassistant.components.homematic pyhomematic==0.1.68 From 7d39ecffa05f7b70bd8085c8a1cc055d64f70d64 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 1 Oct 2020 00:04:01 +0000 Subject: [PATCH 003/831] [ci skip] Translation update --- .../alarmdecoder/translations/el.json | 32 ++++++++++++++++++- .../components/canary/translations/el.json | 20 ++++++++++++ .../components/deconz/translations/es.json | 3 +- .../homekit_controller/translations/el.json | 10 +++++- .../homematicip_cloud/translations/el.json | 7 ++++ .../nightscout/translations/el.json | 1 + .../openweathermap/translations/el.json | 22 +++++++++++++ .../progettihwsw/translations/el.json | 19 +++++++++++ .../components/remote/translations/el.json | 15 +++++++++ .../components/velbus/translations/ca.json | 3 ++ .../components/velbus/translations/en.json | 3 ++ .../components/velbus/translations/no.json | 3 ++ .../components/velbus/translations/ru.json | 3 ++ .../zoneminder/translations/el.json | 12 ++++++- 14 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/homematicip_cloud/translations/el.json create mode 100644 homeassistant/components/progettihwsw/translations/el.json diff --git a/homeassistant/components/alarmdecoder/translations/el.json b/homeassistant/components/alarmdecoder/translations/el.json index ad488dd17ff..2a585b0db95 100644 --- a/homeassistant/components/alarmdecoder/translations/el.json +++ b/homeassistant/components/alarmdecoder/translations/el.json @@ -11,14 +11,43 @@ "data": { "device_baudrate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 Baud \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "device_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - } + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf AlarmDecoder" } } }, "options": { + "error": { + "int": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b5\u03b4\u03af\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2.", + "loop_rfid": "\u039f \u03b2\u03c1\u03cc\u03c7\u03bf\u03c2 RF \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c7\u03c9\u03c1\u03af\u03c2 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF." + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bd\u03c5\u03c7\u03c4\u03b5\u03c1\u03b9\u03bd\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "auto_bypass": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c4\u03bf\u03bd \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc" + } + }, + "init": { + "data": { + "edit_select": "\u0395\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1" + }, + "description": "\u03a4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5;", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" + }, "zone_details": { "data": { + "zone_loop": "\u0392\u03c1\u03cc\u03c7\u03bf\u03c2 RF", + "zone_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03b6\u03ce\u03bd\u03b7\u03c2", + "zone_relayaddr": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad", + "zone_relaychan": "\u039a\u03b1\u03bd\u03ac\u03bb\u03b9 \u03c1\u03b5\u03bb\u03ad", + "zone_rfid": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc RF", "zone_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2" }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" @@ -27,6 +56,7 @@ "data": { "zone_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2" }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03b6\u03ce\u03bd\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5, \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5.", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder" } } diff --git a/homeassistant/components/canary/translations/el.json b/homeassistant/components/canary/translations/el.json index 2cf87db92a3..f1900330469 100644 --- a/homeassistant/components/canary/translations/el.json +++ b/homeassistant/components/canary/translations/el.json @@ -5,6 +5,26 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u03a4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03b2\u03b9\u03b2\u03ac\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03bf ffmpeg \u03b3\u03b9\u03b1 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2", + "timeout": "\u0391\u03af\u03c4\u03b7\u03bc\u03b1 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 877623188bb..016d82da813 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -93,7 +93,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "Permitir sensores deCONZ CLIP", - "allow_deconz_groups": "Permitir grupos de luz deCONZ" + "allow_deconz_groups": "Permitir grupos de luz deCONZ", + "allow_new_devices": "Permitir a\u00f1adir autom\u00e1ticamente nuevos dispositivos" }, "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ", "title": "Opciones deCONZ" diff --git a/homeassistant/components/homekit_controller/translations/el.json b/homeassistant/components/homekit_controller/translations/el.json index 55f468c0fe8..50e23ec5e98 100644 --- a/homeassistant/components/homekit_controller/translations/el.json +++ b/homeassistant/components/homekit_controller/translations/el.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "invalid_properties": "\u0391\u03bd\u03b1\u03ba\u03bf\u03b9\u03bd\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + } + }, "device_automation": { "trigger_subtype": { "button1": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 1", @@ -10,9 +15,12 @@ "button6": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 6", "button7": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 7", "button8": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 8", - "button9": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 9" + "button9": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af 9", + "doorbell": "\u039a\u03bf\u03c5\u03b4\u03bf\u03cd\u03bd\u03b9" }, "trigger_type": { + "double_press": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b4\u03cd\u03bf \u03c6\u03bf\u03c1\u03ad\u03c2 \u03c4\u03bf \" {subtype} \"", + "long_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03b9 \u03ba\u03c1\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \" {subtype} \"", "single_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \" {subtype} \"" } } diff --git a/homeassistant/components/homematicip_cloud/translations/el.json b/homeassistant/components/homematicip_cloud/translations/el.json new file mode 100644 index 00000000000..843d590e7e0 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf PIN, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json index 2a4ee09725b..42762bfddce 100644 --- a/homeassistant/components/nightscout/translations/el.json +++ b/homeassistant/components/nightscout/translations/el.json @@ -5,6 +5,7 @@ "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, + "description": "- \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 nightcout. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n - \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03b5\u03c4\u03b1\u03b9 (auth_default_roles! = readable).", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Nightscout." } } diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index dd34c67ce42..e09bc05a875 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -1,7 +1,29 @@ { "config": { + "abort": { + "already_configured": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenWeatherMap \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + }, + "error": { + "auth": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc.", + "connection": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf OWM API" + }, "step": { "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API OpenWeatherMap", + "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { "data": { "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1" } diff --git a/homeassistant/components/progettihwsw/translations/el.json b/homeassistant/components/progettihwsw/translations/el.json new file mode 100644 index 00000000000..dba162a00ba --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "relay_modes": { + "data": { + "relay_6": "\u03a1\u03b5\u03bb\u03ad 6", + "relay_7": "\u03a1\u03b5\u03bb\u03ad 7", + "relay_8": "\u03a1\u03b5\u03bb\u03ad 8", + "relay_9": "\u03a1\u03b5\u03bb\u03ad 9" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c1\u03b5\u03bb\u03ad" + }, + "user": { + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1" + } + } + }, + "title": "\u0391\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 ProgettiHWSW" +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/el.json b/homeassistant/components/remote/translations/el.json index 79860300b96..0ef87433fce 100644 --- a/homeassistant/components/remote/translations/el.json +++ b/homeassistant/components/remote/translations/el.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", + "turn_off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}", + "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "is_on": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf" + }, + "trigger_type": { + "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", + "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" + } + }, "state": { "_": { "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", diff --git a/homeassistant/components/velbus/translations/ca.json b/homeassistant/components/velbus/translations/ca.json index 783bb6d7f3b..b6680ace1d6 100644 --- a/homeassistant/components/velbus/translations/ca.json +++ b/homeassistant/components/velbus/translations/ca.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "port_exists": "El port ja est\u00e0 configurat" }, "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "connection_failed": "Ha fallat la connexi\u00f3 Velbus", "port_exists": "El port ja est\u00e0 configurat" }, diff --git a/homeassistant/components/velbus/translations/en.json b/homeassistant/components/velbus/translations/en.json index ab455442891..f0b6d16c7be 100644 --- a/homeassistant/components/velbus/translations/en.json +++ b/homeassistant/components/velbus/translations/en.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "port_exists": "This port is already configured" }, "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", "connection_failed": "The velbus connection failed", "port_exists": "This port is already configured" }, diff --git a/homeassistant/components/velbus/translations/no.json b/homeassistant/components/velbus/translations/no.json index 0cc2f475820..99732e6dd9a 100644 --- a/homeassistant/components/velbus/translations/no.json +++ b/homeassistant/components/velbus/translations/no.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "port_exists": "Denne porten er allerede konfigurert" }, "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes.", "connection_failed": "Velbus-tilkoblingen mislyktes", "port_exists": "Denne porten er allerede konfigurert" }, diff --git a/homeassistant/components/velbus/translations/ru.json b/homeassistant/components/velbus/translations/ru.json index e88f6209eee..7a80b18f03e 100644 --- a/homeassistant/components/velbus/translations/ru.json +++ b/homeassistant/components/velbus/translations/ru.json @@ -1,9 +1,12 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." }, "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 Velbus.", "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." }, diff --git a/homeassistant/components/zoneminder/translations/el.json b/homeassistant/components/zoneminder/translations/el.json index 8a63dab388f..81511935386 100644 --- a/homeassistant/components/zoneminder/translations/el.json +++ b/homeassistant/components/zoneminder/translations/el.json @@ -7,13 +7,23 @@ "create_entry": { "default": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 ZoneMinder." }, + "error": { + "auth_fail": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1.", + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder." + }, "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03b8\u03cd\u03c1\u03b1 (\u03c0\u03c1\u03ce\u03b7\u03bd 10.10.0.4:8010)", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS", + "path_zms": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae ZMS", + "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 SSL \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03bf ZoneMinder", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL" - } + }, + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder." } } } From 9fa23f6479e9cf8aed3fa6d2980ddc98dae06e7b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 30 Sep 2020 20:53:00 -0400 Subject: [PATCH 004/831] Bump up zha dependencies (#40914) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index aa8a797d125..e76aaf7e0df 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.45", "zigpy-cc==0.5.2", "zigpy-deconz==0.10.0", - "zigpy==0.24.3", + "zigpy==0.25.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", "zigpy-znp==0.2.0" diff --git a/requirements_all.txt b/requirements_all.txt index c782e7ad37b..91f229521f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.2.0 # homeassistant.components.zha -zigpy==0.24.3 +zigpy==0.25.0 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21e54d27754..2dd73c48c06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1089,7 +1089,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.2.0 # homeassistant.components.zha -zigpy==0.24.3 +zigpy==0.25.0 # homeassistant.components.zoneminder zm-py==0.4.0 From 44e5ec58b0bfebc5ecc170a198f07980ab5e5e93 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Sep 2020 19:20:10 -0600 Subject: [PATCH 005/831] Bump simplisafe-python to 9.4.1 (#40819) * Bump simplisafe-python to 9.4.0 * One more bump --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f78762b17c8..613187ef744 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.3.3"], + "requirements": ["simplisafe-python==9.4.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 91f229521f8..39d606181e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,7 +2000,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.3.3 +simplisafe-python==9.4.1 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dd73c48c06..f28ba1c4b49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.3.3 +simplisafe-python==9.4.1 # homeassistant.components.sleepiq sleepyq==0.7 From c5041b41c836f20f5fccddb64a235de50a39055d Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 1 Oct 2020 08:55:57 +0200 Subject: [PATCH 006/831] Implement config and option flow for rfxtrx integration (#39117) * Create option flow for Rfxtrx integration (#37982) * Implement config flow for rfxtrx integration (#39299) * Add config flow * Add strings * Add first series of tests * Add tests * Adjust tests according review comments * Adjust strings * Add executor for testing connection * Change ports to dict * Fix pylint issue * Adjust tests * Migrate config entry for rfxtrx integration (#39528) * Add rfxtrx device connection validation when importing (#39582) * Implement import connection validation * Fix binary sensor tests * Move rfxtrx data * Fix cover tests * Fix test init * Fix light tests * Fix sensor tests * Fix switch tests * Refactor rfxtrx test data * Fix strings * Fix check * Rework device string in test code * Add option to delete multiple rfxtrx devices (#39625) * Opt to remove multiple devices * Fix devices key * Add tests (phase 1) * Add tests (phase 2) * Tweak remove devices test * Implement device migration function in rfxtrx option flow (#39694) * Prompt option to replace device * Revert unwanted changes * Add replace device function * WIP replace entities * Remove device/entities and update config entry * Fix styling * Add info * Add test * Fix strings * Refactor building migration map * Allow migration for all device types * Add test to migrate control device * Fixup some names * Fixup entry names in test code * Bump pyRFXtrx to 0.26 and deprecate debug config key (#40679) * Create option flow for Rfxtrx integration (#37982) * Implement config flow for rfxtrx integration (#39299) * Add config flow * Add strings * Add first series of tests * Add tests * Adjust tests according review comments * Adjust strings * Add executor for testing connection * Change ports to dict * Fix pylint issue * Adjust tests * Migrate config entry for rfxtrx integration (#39528) * Add rfxtrx device connection validation when importing (#39582) * Implement import connection validation * Fix binary sensor tests * Move rfxtrx data * Fix cover tests * Fix test init * Fix light tests * Fix sensor tests * Fix switch tests * Refactor rfxtrx test data * Fix strings * Fix check * Rework device string in test code * Add option to delete multiple rfxtrx devices (#39625) * Opt to remove multiple devices * Fix devices key * Add tests (phase 1) * Add tests (phase 2) * Tweak remove devices test * Implement device migration function in rfxtrx option flow (#39694) * Prompt option to replace device * Revert unwanted changes * Add replace device function * WIP replace entities * Remove device/entities and update config entry * Fix styling * Add info * Add test * Fix strings * Refactor building migration map * Allow migration for all device types * Add test to migrate control device * Fixup some names * Fixup entry names in test code * Bump version number * Remove debug key from connect * Remove debug option from config flow * Remove debug from tests * Fix event test * Add cv.deprecated * Fix test * Fix config schema * Add timeout on connection * Rework config schema * Fix schema...again * Prevent creation of duplicate device in rfxtrx option flow (#40656) --- CODEOWNERS | 2 +- homeassistant/components/rfxtrx/__init__.py | 38 +- .../components/rfxtrx/binary_sensor.py | 22 +- .../components/rfxtrx/config_flow.py | 583 ++++++++- homeassistant/components/rfxtrx/const.py | 9 + homeassistant/components/rfxtrx/cover.py | 8 +- homeassistant/components/rfxtrx/light.py | 14 +- homeassistant/components/rfxtrx/manifest.json | 8 +- homeassistant/components/rfxtrx/strings.json | 77 +- homeassistant/components/rfxtrx/switch.py | 18 +- .../components/rfxtrx/translations/en.json | 73 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/conftest.py | 26 +- tests/components/rfxtrx/test_binary_sensor.py | 166 ++- tests/components/rfxtrx/test_config_flow.py | 1131 ++++++++++++++++- tests/components/rfxtrx/test_cover.py | 73 +- tests/components/rfxtrx/test_init.py | 38 +- tests/components/rfxtrx/test_light.py | 69 +- tests/components/rfxtrx/test_sensor.py | 121 +- tests/components/rfxtrx/test_switch.py | 84 +- 22 files changed, 2202 insertions(+), 363 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ede90722253..549f4193c09 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -355,7 +355,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/repetier/* @MTrab -homeassistant/components/rfxtrx/* @danielhiversen @elupus +homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/ring/* @balloob homeassistant/components/risco/* @OnFreund homeassistant/components/rmvtransport/* @cgtobi diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index eb13800f748..c12a6380f20 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -5,6 +5,7 @@ from collections import OrderedDict import logging import RFXtrx as rfxtrxmod +import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -33,11 +34,19 @@ from homeassistant.const import ( VOLT, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_EVENT, + CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, + CONF_DEBUG, + CONF_FIRE_EVENT, + CONF_OFF_DELAY, + CONF_REMOVE_DEVICE, + CONF_SIGNAL_REPETITIONS, DEVICE_PACKET_TYPE_LIGHTING4, EVENT_RFXTRX_EVENT, SERVICE_SEND, @@ -47,12 +56,6 @@ DOMAIN = "rfxtrx" DEFAULT_SIGNAL_REPETITIONS = 1 -CONF_FIRE_EVENT = "fire_event" -CONF_DATA_BITS = "data_bits" -CONF_AUTOMATIC_ADD = "automatic_add" -CONF_SIGNAL_REPETITIONS = "signal_repetitions" -CONF_DEBUG = "debug" -CONF_OFF_DELAY = "off_delay" SIGNAL_EVENT = f"{DOMAIN}_event" DATA_TYPES = OrderedDict( @@ -126,10 +129,10 @@ DEVICE_DATA_SCHEMA = vol.Schema( BASE_SCHEMA = vol.Schema( { - vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DEBUG): cv.boolean, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, vol.Optional(CONF_DEVICES, default={}): {cv.string: _ensure_device}, - } + }, ) DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string}) @@ -139,7 +142,8 @@ PORT_SCHEMA = BASE_SCHEMA.extend( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA + {DOMAIN: vol.All(cv.deprecated(CONF_DEBUG), vol.Any(DEVICE_SCHEMA, PORT_SCHEMA))}, + extra=vol.ALLOW_EXTRA, ) DOMAINS = ["switch", "sensor", "light", "binary_sensor", "cover"] @@ -154,7 +158,6 @@ async def async_setup(hass, config): CONF_HOST: config[DOMAIN].get(CONF_HOST), CONF_PORT: config[DOMAIN].get(CONF_PORT), CONF_DEVICE: config[DOMAIN].get(CONF_DEVICE), - CONF_DEBUG: config[DOMAIN].get(CONF_DEBUG), CONF_AUTOMATIC_ADD: config[DOMAIN].get(CONF_AUTOMATIC_ADD), CONF_DEVICES: config[DOMAIN][CONF_DEVICES], } @@ -223,11 +226,10 @@ def _create_rfx(config): rfx = rfxtrxmod.Connect( (config[CONF_HOST], config[CONF_PORT]), None, - debug=config[CONF_DEBUG], transport_protocol=rfxtrxmod.PyNetworkTransport, ) else: - rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None, debug=config[CONF_DEBUG]) + rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None) return rfx @@ -251,7 +253,11 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): config = entry.data # Initialize library - rfx_object = await hass.async_add_executor_job(_create_rfx, config) + try: + async with async_timeout.timeout(5): + rfx_object = await hass.async_add_executor_job(_create_rfx, config) + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady from err # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) @@ -444,6 +450,12 @@ class RfxtrxEntity(RestoreEntity): ) ) + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", self.async_remove + ) + ) + @property def should_poll(self): """No polling needed for a RFXtrx switch.""" diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 21f3e0b74b3..7fe89e747bc 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -61,6 +61,18 @@ DEVICE_TYPE_DEVICE_CLASS = { } +def supported(event): + """Return whether an event supports binary_sensor.""" + if isinstance(event, rfxtrxmod.ControlEvent): + return True + if isinstance(event, rfxtrxmod.SensorEvent): + return event.values.get("Sensor Status") in [ + *SENSOR_STATUS_ON, + *SENSOR_STATUS_OFF, + ] + return False + + async def async_setup_entry( hass, config_entry, @@ -74,16 +86,6 @@ async def async_setup_entry( discovery_info = config_entry.data - def supported(event): - if isinstance(event, rfxtrxmod.ControlEvent): - return True - if isinstance(event, rfxtrxmod.SensorEvent): - return event.values.get("Sensor Status") in [ - *SENSOR_STATUS_ON, - *SENSOR_STATUS_OFF, - ] - return False - for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) if event is None: diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 596f1d0b5e9..db7ca49691a 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -1,12 +1,404 @@ """Config flow for RFXCOM RFXtrx integration.""" +import copy import logging +import os -from homeassistant import config_entries +import RFXtrx as rfxtrxmod +import serial +import serial.tools.list_ports +import voluptuous as vol -from . import DOMAIN +from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_DEVICE, + CONF_DEVICE_ID, + CONF_DEVICES, + CONF_HOST, + CONF_PORT, + CONF_TYPE, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry as async_get_entity_registry, +) + +from . import DOMAIN, get_device_id, get_rfx_object +from .binary_sensor import supported as binary_supported +from .const import ( + CONF_AUTOMATIC_ADD, + CONF_DATA_BITS, + CONF_FIRE_EVENT, + CONF_OFF_DELAY, + CONF_REMOVE_DEVICE, + CONF_REPLACE_DEVICE, + CONF_SIGNAL_REPETITIONS, + DEVICE_PACKET_TYPE_LIGHTING4, +) +from .cover import supported as cover_supported +from .light import supported as light_supported +from .switch import supported as switch_supported _LOGGER = logging.getLogger(__name__) +CONF_EVENT_CODE = "event_code" +CONF_MANUAL_PATH = "Enter Manually" + + +def none_or_int(value, base): + """Check if strin is one otherwise convert to int.""" + if value is None: + return None + return int(value, base) + + +class OptionsFlow(config_entries.OptionsFlow): + """Handle Rfxtrx options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize rfxtrx options flow.""" + self._config_entry = config_entry + self._global_options = None + self._selected_device = None + self._selected_device_entry_id = None + self._selected_device_event_code = None + self._selected_device_object = None + self._device_entries = None + self._device_registry = None + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_prompt_options() + + async def async_step_prompt_options(self, user_input=None): + """Prompt for options.""" + errors = {} + + if user_input is not None: + self._global_options = { + CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD], + } + if CONF_DEVICE in user_input: + entry_id = user_input[CONF_DEVICE] + device_data = self._get_device_data(entry_id) + self._selected_device_entry_id = entry_id + event_code = device_data[CONF_EVENT_CODE] + self._selected_device_event_code = event_code + self._selected_device = self._config_entry.data[CONF_DEVICES][ + event_code + ] + self._selected_device_object = get_rfx_object(event_code) + return await self.async_step_set_device_options() + if CONF_REMOVE_DEVICE in user_input: + remove_devices = user_input[CONF_REMOVE_DEVICE] + devices = {} + for entry_id in remove_devices: + device_data = self._get_device_data(entry_id) + + event_code = device_data[CONF_EVENT_CODE] + device_id = device_data[CONF_DEVICE_ID] + self.hass.helpers.dispatcher.async_dispatcher_send( + f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{device_id}" + ) + self._device_registry.async_remove_device(entry_id) + devices[event_code] = None + + self.update_config_data( + global_options=self._global_options, devices=devices + ) + + return self.async_create_entry(title="", data={}) + if CONF_EVENT_CODE in user_input: + self._selected_device_event_code = user_input[CONF_EVENT_CODE] + self._selected_device = {} + selected_device_object = get_rfx_object( + self._selected_device_event_code + ) + if selected_device_object is None: + errors[CONF_EVENT_CODE] = "invalid_event_code" + elif not self._can_add_device(selected_device_object): + errors[CONF_EVENT_CODE] = "already_configured_device" + else: + self._selected_device_object = selected_device_object + return await self.async_step_set_device_options() + + if not errors: + self.update_config_data(global_options=self._global_options) + + return self.async_create_entry(title="", data={}) + + device_registry = await async_get_device_registry(self.hass) + device_entries = async_entries_for_config_entry( + device_registry, self._config_entry.entry_id + ) + self._device_registry = device_registry + self._device_entries = device_entries + + devices = { + entry.id: entry.name_by_user if entry.name_by_user else entry.name + for entry in device_entries + } + + options = { + vol.Optional( + CONF_AUTOMATIC_ADD, + default=self._config_entry.data[CONF_AUTOMATIC_ADD], + ): bool, + vol.Optional(CONF_EVENT_CODE): str, + vol.Optional(CONF_DEVICE): vol.In(devices), + vol.Optional(CONF_REMOVE_DEVICE): cv.multi_select(devices), + } + + return self.async_show_form( + step_id="prompt_options", data_schema=vol.Schema(options), errors=errors + ) + + async def async_step_set_device_options(self, user_input=None): + """Manage device options.""" + errors = {} + + if user_input is not None: + device_id = get_device_id( + self._selected_device_object.device, + data_bits=user_input.get(CONF_DATA_BITS), + ) + + if CONF_REPLACE_DEVICE in user_input: + await self._async_replace_device(user_input[CONF_REPLACE_DEVICE]) + + devices = {self._selected_device_event_code: None} + self.update_config_data( + global_options=self._global_options, devices=devices + ) + + return self.async_create_entry(title="", data={}) + + try: + command_on = none_or_int(user_input.get(CONF_COMMAND_ON), 16) + except ValueError: + errors[CONF_COMMAND_ON] = "invalid_input_2262_on" + + try: + command_off = none_or_int(user_input.get(CONF_COMMAND_OFF), 16) + except ValueError: + errors[CONF_COMMAND_OFF] = "invalid_input_2262_off" + + try: + off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10) + except ValueError: + errors[CONF_OFF_DELAY] = "invalid_input_off_delay" + + if not errors: + devices = {} + device = { + CONF_DEVICE_ID: device_id, + CONF_FIRE_EVENT: user_input.get(CONF_FIRE_EVENT, False), + CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1), + } + + devices[self._selected_device_event_code] = device + + if off_delay: + device[CONF_OFF_DELAY] = off_delay + if user_input.get(CONF_DATA_BITS): + device[CONF_DATA_BITS] = user_input[CONF_DATA_BITS] + if command_on: + device[CONF_COMMAND_ON] = command_on + if command_off: + device[CONF_COMMAND_OFF] = command_off + + self.update_config_data( + global_options=self._global_options, devices=devices + ) + + return self.async_create_entry(title="", data={}) + + device_data = self._selected_device + + data_schema = { + vol.Optional( + CONF_FIRE_EVENT, default=device_data.get(CONF_FIRE_EVENT, False) + ): bool, + } + + if binary_supported(self._selected_device_object): + if device_data.get(CONF_OFF_DELAY): + off_delay_schema = { + vol.Optional( + CONF_OFF_DELAY, + description={"suggested_value": device_data[CONF_OFF_DELAY]}, + ): str, + } + else: + off_delay_schema = { + vol.Optional(CONF_OFF_DELAY): str, + } + data_schema.update(off_delay_schema) + + if ( + binary_supported(self._selected_device_object) + or cover_supported(self._selected_device_object) + or light_supported(self._selected_device_object) + or switch_supported(self._selected_device_object) + ): + data_schema.update( + { + vol.Optional( + CONF_SIGNAL_REPETITIONS, + default=device_data.get(CONF_SIGNAL_REPETITIONS, 1), + ): int, + } + ) + + if ( + self._selected_device_object.device.packettype + == DEVICE_PACKET_TYPE_LIGHTING4 + ): + data_schema.update( + { + vol.Optional( + CONF_DATA_BITS, default=device_data.get(CONF_DATA_BITS, 0) + ): int, + vol.Optional( + CONF_COMMAND_ON, + default=hex(device_data.get(CONF_COMMAND_ON, 0)), + ): str, + vol.Optional( + CONF_COMMAND_OFF, + default=hex(device_data.get(CONF_COMMAND_OFF, 0)), + ): str, + } + ) + + devices = { + entry.id: entry.name_by_user if entry.name_by_user else entry.name + for entry in self._device_entries + if self._can_replace_device(entry.id) + } + + if devices: + data_schema.update( + { + vol.Optional(CONF_REPLACE_DEVICE): vol.In(devices), + } + ) + + return self.async_show_form( + step_id="set_device_options", + data_schema=vol.Schema(data_schema), + errors=errors, + ) + + async def _async_replace_device(self, replace_device): + """Migrate properties of a device into another.""" + device_registry = self._device_registry + old_device = self._selected_device_entry_id + old_entry = device_registry.async_get(old_device) + device_registry.async_update_device( + replace_device, + area_id=old_entry.area_id, + name_by_user=old_entry.name_by_user, + ) + + old_device_data = self._get_device_data(old_device) + new_device_data = self._get_device_data(replace_device) + + old_device_id = "_".join(x for x in old_device_data[CONF_DEVICE_ID]) + new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID]) + + entity_registry = await async_get_entity_registry(self.hass) + entity_entries = async_entries_for_device(entity_registry, old_device) + entity_migration_map = {} + for entry in entity_entries: + unique_id = entry.unique_id + new_unique_id = unique_id.replace(old_device_id, new_device_id) + + new_entity_id = entity_registry.async_get_entity_id( + entry.domain, entry.platform, new_unique_id + ) + + if new_entity_id is not None: + entity_migration_map[new_entity_id] = entry + + for entry in entity_migration_map.values(): + entity_registry.async_remove(entry.entity_id) + + for entity_id, entry in entity_migration_map.items(): + entity_registry.async_update_entity( + entity_id, + new_entity_id=entry.entity_id, + name=entry.name, + icon=entry.icon, + ) + + device_registry.async_remove_device(old_device) + + def _can_add_device(self, new_rfx_obj): + """Check if device does not already exist.""" + new_device_id = get_device_id(new_rfx_obj.device) + for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): + rfx_obj = get_rfx_object(packet_id) + device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS)) + if new_device_id == device_id: + return False + + return True + + def _can_replace_device(self, entry_id): + """Check if device can be replaced with selected device.""" + device_data = self._get_device_data(entry_id) + event_code = device_data[CONF_EVENT_CODE] + rfx_obj = get_rfx_object(event_code) + if ( + rfx_obj.device.packettype == self._selected_device_object.device.packettype + and rfx_obj.device.subtype == self._selected_device_object.device.subtype + and self._selected_device_event_code != event_code + ): + return True + + return False + + def _get_device_data(self, entry_id): + """Get event code based on device identifier.""" + event_code = None + device_id = None + entry = self._device_registry.async_get(entry_id) + device_id = next(iter(entry.identifiers))[1:] + for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): + if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id: + event_code = packet_id + break + + data = {CONF_EVENT_CODE: event_code, CONF_DEVICE_ID: device_id} + + return data + + @callback + def update_config_data(self, global_options=None, devices=None): + """Update data in ConfigEntry.""" + entry_data = self._config_entry.data.copy() + entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES]) + if global_options: + entry_data.update(global_options) + if devices: + for event_code, options in devices.items(): + if options is None: + entry_data[CONF_DEVICES].pop(event_code) + else: + entry_data[CONF_DEVICES][event_code] = options + self.hass.config_entries.async_update_entry(self._config_entry, data=entry_data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._config_entry.entry_id) + ) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for RFXCOM RFXtrx.""" @@ -14,11 +406,190 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + async def async_step_user(self, user_input=None): + """Step when user initializes a integration.""" + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + errors = {} + if user_input is not None: + user_selection = user_input[CONF_TYPE] + if user_selection == "Serial": + return await self.async_step_setup_serial() + + return await self.async_step_setup_network() + + list_of_types = ["Serial", "Network"] + + schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_setup_network(self, user_input=None): + """Step when setting up network configuration.""" + errors = {} + + if user_input is not None: + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + try: + data = await self.async_validate_rfx(host=host, port=port) + except CannotConnect: + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry(title="RFXTRX", data=data) + + schema = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_PORT): int} + ) + return self.async_show_form( + step_id="setup_network", + data_schema=schema, + errors=errors, + ) + + async def async_step_setup_serial(self, user_input=None): + """Step when setting up serial configuration.""" + errors = {} + + if user_input is not None: + user_selection = user_input[CONF_DEVICE] + if user_selection == CONF_MANUAL_PATH: + return await self.async_step_setup_serial_manual_path() + + dev_path = await self.hass.async_add_executor_job( + get_serial_by_id, user_selection + ) + + try: + data = await self.async_validate_rfx(device=dev_path) + except CannotConnect: + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry(title="RFXTRX", data=data) + + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + list_of_ports = {} + for port in ports: + list_of_ports[ + port.device + ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( + f" - {port.manufacturer}" if port.manufacturer else "" + ) + list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH + + schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(list_of_ports)}) + return self.async_show_form( + step_id="setup_serial", + data_schema=schema, + errors=errors, + ) + + async def async_step_setup_serial_manual_path(self, user_input=None): + """Select path manually.""" + errors = {} + + if user_input is not None: + device = user_input[CONF_DEVICE] + try: + data = await self.async_validate_rfx(device=device) + except CannotConnect: + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry(title="RFXTRX", data=data) + + schema = vol.Schema({vol.Required(CONF_DEVICE): str}) + return self.async_show_form( + step_id="setup_serial_manual_path", + data_schema=schema, + errors=errors, + ) + async def async_step_import(self, import_config=None): """Handle the initial step.""" entry = await self.async_set_unique_id(DOMAIN) - if entry and import_config.items() != entry.data.items(): - self.hass.config_entries.async_update_entry(entry, data=import_config) - return self.async_abort(reason="already_configured") - self._abort_if_unique_id_configured() + if entry: + if CONF_DEVICES not in entry.data: + # In version 0.113, devices key was not written to config entry. Update the entry with import data + self._abort_if_unique_id_configured(import_config) + else: + self._abort_if_unique_id_configured() + + host = import_config[CONF_HOST] + port = import_config[CONF_PORT] + device = import_config[CONF_DEVICE] + + try: + if host is not None: + await self.async_validate_rfx(host=host, port=port) + else: + await self.async_validate_rfx(device=device) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + return self.async_create_entry(title="RFXTRX", data=import_config) + + async def async_validate_rfx(self, host=None, port=None, device=None): + """Create data for rfxtrx entry.""" + success = await self.hass.async_add_executor_job( + _test_transport, host, port, device + ) + if not success: + raise CannotConnect + + data = { + CONF_HOST: host, + CONF_PORT: port, + CONF_DEVICE: device, + CONF_AUTOMATIC_ADD: False, + CONF_DEVICES: {}, + } + return data + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlow(config_entry) + + +def _test_transport(host, port, device): + """Construct a rfx object based on config.""" + if port is not None: + try: + conn = rfxtrxmod.PyNetworkTransport((host, port)) + except OSError: + return False + + conn.close() + else: + try: + conn = rfxtrxmod.PySerialTransport(device) + except serial.serialutil.SerialException: + return False + + if conn.serial is None: + return False + + conn.close() + + return True + + +def get_serial_by_id(dev_path: str) -> str: + """Return a /dev/serial/by-id match for given device if available.""" + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return dev_path + + for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): + if os.path.realpath(path) == dev_path: + return path + return dev_path + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index c0436bfcf60..404d344cc71 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -1,5 +1,14 @@ """Constants for RFXtrx integration.""" +CONF_FIRE_EVENT = "fire_event" +CONF_DATA_BITS = "data_bits" +CONF_AUTOMATIC_ADD = "automatic_add" +CONF_SIGNAL_REPETITIONS = "signal_repetitions" +CONF_DEBUG = "debug" +CONF_OFF_DELAY = "off_delay" + +CONF_REMOVE_DEVICE = "remove_device" +CONF_REPLACE_DEVICE = "replace_device" COMMAND_ON_LIST = [ "On", diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index fc6ab6cbf15..86950308f55 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -20,6 +20,11 @@ from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST _LOGGER = logging.getLogger(__name__) +def supported(event): + """Return whether an event supports cover.""" + return event.device.known_to_be_rollershutter + + async def async_setup_entry( hass, config_entry, @@ -29,9 +34,6 @@ async def async_setup_entry( discovery_info = config_entry.data device_ids = set() - def supported(event): - return event.device.known_to_be_rollershutter - entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 791cc158693..33ee5ea4748 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -28,6 +28,14 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS +def supported(event): + """Return whether an event supports light.""" + return ( + isinstance(event.device, rfxtrxmod.LightingDevice) + and event.device.known_to_be_dimmable + ) + + async def async_setup_entry( hass, config_entry, @@ -37,12 +45,6 @@ async def async_setup_entry( discovery_info = config_entry.data device_ids = set() - def supported(event): - return ( - isinstance(event.device, rfxtrxmod.LightingDevice) - and event.device.known_to_be_dimmable - ) - # Add switch from config file entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 44b53ed0dac..e62fc5c3c83 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.25"], - "codeowners": ["@danielhiversen", "@elupus"], - "config_flow": false -} \ No newline at end of file + "requirements": ["pyRFXtrx==0.26"], + "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], + "config_flow": true +} diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index e19265dec32..9e976999157 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -1,9 +1,74 @@ { - "config": { - "step": {}, - "error": {}, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } + "title": "Rfxtrx", + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "type": "Connection type" + }, + "title": "Select connection type" + }, + "setup_network": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "title": "Select connection address" + }, + "setup_serial": { + "data": { + "device": "Select device" + }, + "title": "Device" + }, + "setup_serial_manual_path": { + "data": { + "device": "[%key:common::config_flow::data::usb_path%]" + }, + "title": "Path" + } } + }, + "options": { + "step": { + "prompt_options": { + "data": { + "debug": "Enable debugging", + "automatic_add": "Enable automatic add", + "event_code": "Enter event code to add", + "device": "Select device to configure", + "remove_device": "Select device to delete" + }, + "title": "Rfxtrx Options" + }, + "set_device_options": { + "data": { + "fire_event": "Enable device event", + "off_delay": "Off delay", + "off_delay_enabled": "Enable off delay", + "data_bit": "Number of data bits", + "command_on": "Data bits value for command on", + "command_off": "Data bits value for command off", + "signal_repetitions": "Number of signal repetitions", + "replace_device": "Select device to replace" + }, + "title": "Configure device options" + } + }, + "error": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_event_code": "Invalid event code", + "invalid_input_2262_on": "Invalid input for command on", + "invalid_input_2262_off": "Invalid input for command off", + "invalid_input_off_delay": "Invalid input for off delay", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } } diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index bce5222b778..53069210794 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -25,6 +25,16 @@ DATA_SWITCH = f"{DOMAIN}_switch" _LOGGER = logging.getLogger(__name__) +def supported(event): + """Return whether an event supports switch.""" + return ( + isinstance(event.device, rfxtrxmod.LightingDevice) + and not event.device.known_to_be_dimmable + and not event.device.known_to_be_rollershutter + or isinstance(event.device, rfxtrxmod.RfyDevice) + ) + + async def async_setup_entry( hass, config_entry, @@ -34,14 +44,6 @@ async def async_setup_entry( discovery_info = config_entry.data device_ids = set() - def supported(event): - return ( - isinstance(event.device, rfxtrxmod.LightingDevice) - and not event.device.known_to_be_dimmable - and not event.device.known_to_be_rollershutter - or isinstance(event.device, rfxtrxmod.RfyDevice) - ) - # Add switch from config file entities = [] for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 1344d2f6988..ebb7c77f303 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -1,7 +1,74 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Already configured. Only a single configuration possible.", + "cannot_connect": "Failed to connect" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "type": "Connection type" + }, + "title": "Select connection type" + }, + "setup_network": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Select connection address" + }, + "setup_serial": { + "data": { + "device": "Select device" + }, + "title": "Device" + }, + "setup_serial_manual_path": { + "data": { + "device": "Select path" + }, + "title": "Path" + } } - } -} \ No newline at end of file + }, + "options": { + "step": { + "prompt_options": { + "data": { + "debug": "Enable debugging", + "automatic_add": "Enable automatic add", + "event_code": "Enter event code to add", + "device": "Select device to configure", + "remove_device": "Select device to delete" + }, + "title": "Rfxtrx Options" + }, + "set_device_options": { + "data": { + "fire_event": "Enable device event", + "off_delay": "Off delay", + "off_delay_enabled": "Enable off delay", + "data_bit": "Number of data bits", + "command_on": "Data bits value for command on", + "command_off": "Data bits value for command off", + "signal_repetitions": "Number of signal repetitions", + "replace_device": "Select device to replace" + }, + "title": "Configure device options" + } + }, + "error": { + "already_configured_device": "Device is already configured", + "invalid_event_code": "Invalid event code", + "invalid_input_2262_on": "Invalid input for command on", + "invalid_input_2262_off": "Invalid input for command off", + "invalid_input_off_delay": "Invalid input for off delay", + "unknown": "Unexpected error" + } + }, + "title": "Rfxtrx" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bfd3c340e6d..0347b8c82d6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -150,6 +150,7 @@ FLOWS = [ "pvpc_hourly_pricing", "rachio", "rainmachine", + "rfxtrx", "ring", "risco", "roku", diff --git a/requirements_all.txt b/requirements_all.txt index 39d606181e3..0ad24c0ae88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1211,7 +1211,7 @@ pyHS100==0.3.5.1 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.25 +pyRFXtrx==0.26 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f28ba1c4b49..86b1faf8965 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -592,7 +592,7 @@ pyHS100==0.3.5.1 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.25 +pyRFXtrx==0.26 # homeassistant.components.tibber pyTibber==0.15.3 diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 1eb39f00691..82c4bd7aacd 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -4,11 +4,23 @@ from datetime import timedelta import pytest from homeassistant.components import rfxtrx -from homeassistant.setup import async_setup_component +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.util.dt import utcnow from tests.async_mock import patch -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed + + +def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None): + """Create rfxtrx config entry data.""" + return { + "device": device, + "host": None, + "port": None, + "automatic_add": automatic_add, + "debug": False, + "devices": devices, + } @pytest.fixture(autouse=True, name="rfxtrx") @@ -37,12 +49,12 @@ async def rfxtrx_fixture(hass): @pytest.fixture(name="rfxtrx_automatic") async def rfxtrx_automatic_fixture(hass, rfxtrx): """Fixture that starts up with automatic additions.""" + entry_data = create_rfx_test_cfg(automatic_add=True, devices={}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "automatic_add": True}}, - ) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() yield rfxtrx diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index ee757192aaf..a52b390395a 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -1,11 +1,12 @@ """The tests for the Rfxtrx sensor platform.""" import pytest +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import ATTR_EVENT from homeassistant.core import State -from homeassistant.setup import async_setup_component -from tests.common import mock_restore_cache +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.rfxtrx.conftest import create_rfx_test_cfg EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670" EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770" @@ -21,11 +22,12 @@ EVENT_AC_118CDEA_2_ON = "0b1100100118cdea02010f70" async def test_one(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}}, - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("binary_sensor.ac_213c7f2_48") @@ -36,22 +38,20 @@ async def test_one(hass, rfxtrx): async def test_one_pt2262(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0913000022670e013970": { - "data_bits": 4, - "command_on": 0xE, - "command_off": 0x7, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, } - }, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -71,19 +71,14 @@ async def test_one_pt2262(hass, rfxtrx): async def test_pt2262_unconfigured(hass, rfxtrx): """Test with discovery for PT2262.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0913000022670e013970": {}, - "09130000226707013970": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={"0913000022670e013970": {}, "09130000226707013970": {}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -109,11 +104,12 @@ async def test_state_restore(hass, rfxtrx, state, event): mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})]) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f230010f71": {}}}}, - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state @@ -121,20 +117,18 @@ async def test_state_restore(hass, rfxtrx, state, event): async def test_several(hass, rfxtrx): """Test with 3.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1100cd0213c7f230010f71": {}, - "0b1100100118cdea02010f70": {}, - "0b1100101118cdea02010f70": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0b1100cd0213c7f230010f71": {}, + "0b1100100118cdea02010f70": {}, + "0b1100101118cdea02010f70": {}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("binary_sensor.ac_213c7f2_48") @@ -181,16 +175,12 @@ async def test_off_delay_restore(hass, rfxtrx): ], ) - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": {EVENT_AC_118CDEA_2_ON: {"off_delay": 5}}, - } - }, - ) + entry_data = create_rfx_test_cfg(devices={EVENT_AC_118CDEA_2_ON: {"off_delay": 5}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -201,16 +191,14 @@ async def test_off_delay_restore(hass, rfxtrx): async def test_off_delay(hass, rfxtrx, timestep): """Test with discovery.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": {"0b1100100118cdea02010f70": {"off_delay": 5}}, - } - }, + entry_data = create_rfx_test_cfg( + devices={"0b1100100118cdea02010f70": {"off_delay": 5}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -295,27 +283,25 @@ async def test_light(hass, rfxtrx_automatic): async def test_pt2262_duplicate_id(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0913000022670e013970": { - "data_bits": 4, - "command_on": 0xE, - "command_off": 0x7, - }, - "09130000226707013970": { - "data_bits": 4, - "command_on": 0xE, - "command_off": 0x7, - }, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + }, + "09130000226707013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + }, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 53f3b317d53..6ba045d60a6 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -1,19 +1,306 @@ """Test the Tado config flow.""" -from homeassistant import config_entries, setup -from homeassistant.components.rfxtrx import DOMAIN +import os +import serial.tools.list_ports + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.rfxtrx import DOMAIN, config_flow +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) + +from tests.async_mock import MagicMock, patch, sentinel from tests.common import MockConfigEntry -async def test_import(hass): +def serial_connect(self): + """Mock a serial connection.""" + self.serial = True + + +def serial_connect_fail(self): + """Mock a failed serial connection.""" + self.serial = None + + +def com_port(): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo() + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/dev/ttyUSB1234" + port.description = "Some serial port" + + return port + + +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect", + return_value=None, +) +async def test_setup_network(connect_mock, hass): + """Test we can setup network.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Network"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_network" + assert result["errors"] == {} + + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "10.10.0.1", "port": 1234} + ) + + assert result["type"] == "create_entry" + assert result["title"] == "RFXTRX" + assert result["data"] == { + "host": "10.10.0.1", + "port": 1234, + "device": None, + "automatic_add": False, + "devices": {}, + } + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect", + serial_connect, +) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close", + return_value=None, +) +async def test_setup_serial(com_mock, connect_mock, hass): + """Test we can setup serial.""" + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + + assert result["type"] == "create_entry" + assert result["title"] == "RFXTRX" + assert result["data"] == { + "host": None, + "port": None, + "device": port.device, + "automatic_add": False, + "devices": {}, + } + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect", + serial_connect, +) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close", + return_value=None, +) +async def test_setup_serial_manual(com_mock, connect_mock, hass): + """Test we can setup serial with manual entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == "create_entry" + assert result["title"] == "RFXTRX" + assert result["data"] == { + "host": None, + "port": None, + "device": "/dev/ttyUSB0", + "automatic_add": False, + "devices": {}, + } + + +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect", + side_effect=OSError, +) +async def test_setup_network_fail(connect_mock, hass): + """Test we can setup network.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Network"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_network" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "10.10.0.1", "port": 1234} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_network" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect", + side_effect=serial.serialutil.SerialException, +) +async def test_setup_serial_fail(com_mock, connect_mock, hass): + """Test setup serial failed connection.""" + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect", + serial_connect_fail, +) +async def test_setup_serial_manual_fail(com_mock, hass): + """Test setup serial failed connection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.connect", + serial_connect, +) +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PySerialTransport.close", + return_value=None, +) +async def test_import_serial(connect_mock, hass): """Test we can import.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": None, "port": None, "device": "/dev/tty123", "debug": False}, - ) + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": None, "port": None, "device": "/dev/tty123", "debug": False}, + ) assert result["type"] == "create_entry" assert result["title"] == "RFXTRX" @@ -25,10 +312,87 @@ async def test_import(hass): } +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect", + return_value=None, +) +async def test_import_network(connect_mock, hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": "localhost", "port": 1234, "device": None, "debug": False}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "RFXTRX" + assert result["data"] == { + "host": "localhost", + "port": 1234, + "device": None, + "debug": False, + } + + +@patch( + "homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport.connect", + side_effect=OSError, +) +async def test_import_network_connection_fail(connect_mock, hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": "localhost", "port": 1234, "device": None, "debug": False}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + async def test_import_update(hass): """Test we can import.""" await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "debug": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "debug": True, + "devices": {}, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_migrate(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={"host": None, "port": None, "device": "/dev/tty123", "debug": False}, @@ -36,12 +400,751 @@ async def test_import_update(hass): ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": None, "port": None, "device": "/dev/tty123", "debug": True}, - ) + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "debug": True, + "automatic_add": True, + "devices": {}, + }, + ) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data["debug"] + + assert entry.data["devices"] == {} + + +async def test_options_global(hass): + """Test if we can set global options.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"automatic_add": True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["automatic_add"] + + +async def test_options_add_device(hass): + """Test we can add a device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + # Try with invalid event code + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"automatic_add": True, "event_code": "1234"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + assert result["errors"] + assert result["errors"]["event_code"] == "invalid_event_code" + + # Try with valid event code + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "0b1100cd0213c7f230010f71", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"fire_event": True, "signal_repetitions": 5} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["automatic_add"] + + assert entry.data["devices"]["0b1100cd0213c7f230010f71"] + assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"] + assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5 + assert "delay_off" not in entry.data["devices"]["0b1100cd0213c7f230010f71"] + + state = hass.states.get("binary_sensor.ac_213c7f2_48") + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "AC 213c7f2:48" + + +async def test_options_add_duplicate_device(hass): + """Test we can add a device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "debug": False, + "automatic_add": False, + "devices": {"0b1100cd0213c7f230010f71": {"signal_repetitions": 1}}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "0b1100cd0213c7f230010f71", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + assert result["errors"] + assert result["errors"]["event_code"] == "already_configured_device" + + +async def test_options_add_remove_device(hass): + """Test we can add a device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "0b1100cd0213c7f230010f71", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"fire_event": True, "signal_repetitions": 5, "off_delay": "4"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["automatic_add"] + + assert entry.data["devices"]["0b1100cd0213c7f230010f71"] + assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["fire_event"] + assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5 + assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["off_delay"] == 4 + + state = hass.states.get("binary_sensor.ac_213c7f2_48") + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "AC 213c7f2:48" + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].id + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "remove_device": [device_entries[0].id], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert not entry.data["automatic_add"] + + assert "0b1100cd0213c7f230010f71" not in entry.data["devices"] + + state = hass.states.get("binary_sensor.ac_213c7f2_48") + assert not state + + +async def test_options_replace_sensor_device(hass): + """Test we can replace a sensor device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": { + "0a520101f00400e22d0189": {"device_id": ["52", "1", "f0:04"]}, + "0a520105230400c3260279": {"device_id": ["52", "1", "23:04"]}, + }, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status" + ) + assert state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature" + ) + assert state + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + old_device = next( + ( + elem.id + for elem in device_entries + if next(iter(elem.identifiers))[1:] == ("52", "1", "f0:04") + ), + None, + ) + new_device = next( + ( + elem.id + for elem in device_entries + if next(iter(elem.identifiers))[1:] == ("52", "1", "23:04") + ), + None, + ) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "device": old_device, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "replace_device": new_device, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + entity_registry = await async_get_entity_registry(hass) + + entry = entity_registry.async_get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + ) + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity" + ) + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_humidity_status" + ) + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + ) + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_temperature" + ) + assert entry + assert entry.device_id == new_device + + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + ) + assert not state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + ) + assert not state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity" + ) + assert not state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_humidity_status" + ) + assert not state + state = hass.states.get( + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_temperature" + ) + assert not state + + +async def test_options_replace_control_device(hass): + """Test we can replace a control device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": { + "0b1100100118cdea02010f70": { + "device_id": ["11", "0", "118cdea:2"], + "signal_repetitions": 1, + }, + "0b1100101118cdea02010f70": { + "device_id": ["11", "0", "1118cdea:2"], + "signal_repetitions": 1, + }, + }, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.ac_118cdea_2") + assert state + state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric") + assert state + state = hass.states.get("switch.ac_118cdea_2") + assert state + state = hass.states.get("binary_sensor.ac_1118cdea_2") + assert state + state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + assert state + state = hass.states.get("switch.ac_1118cdea_2") + assert state + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + old_device = next( + ( + elem.id + for elem in device_entries + if next(iter(elem.identifiers))[1:] == ("11", "0", "118cdea:2") + ), + None, + ) + new_device = next( + ( + elem.id + for elem in device_entries + if next(iter(elem.identifiers))[1:] == ("11", "0", "1118cdea:2") + ), + None, + ) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "device": old_device, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "replace_device": new_device, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + entity_registry = await async_get_entity_registry(hass) + + entry = entity_registry.async_get("binary_sensor.ac_118cdea_2") + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric") + assert entry + assert entry.device_id == new_device + entry = entity_registry.async_get("switch.ac_118cdea_2") + assert entry + assert entry.device_id == new_device + + state = hass.states.get("binary_sensor.ac_1118cdea_2") + assert not state + state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + assert not state + state = hass.states.get("switch.ac_1118cdea_2") + assert not state + + +async def test_options_remove_multiple_devices(hass): + """Test we can add a device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": { + "0b1100cd0213c7f230010f71": {"device_id": ["11", "0", "213c7f2:48"]}, + "0b1100100118cdea02010f70": {"device_id": ["11", "0", "118cdea:2"]}, + "0b1100101118cdea02010f70": {"device_id": ["11", "0", "1118cdea:2"]}, + }, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.ac_213c7f2_48") + assert state + state = hass.states.get("binary_sensor.ac_118cdea_2") + assert state + state = hass.states.get("binary_sensor.ac_1118cdea_2") + assert state + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + assert len(device_entries) == 3 + + def match_device_id(entry): + device_id = next(iter(entry.identifiers))[1:] + if device_id == ("11", "0", "213c7f2:48"): + return True + if device_id == ("11", "0", "118cdea:2"): + return True + return False + + remove_devices = [elem.id for elem in device_entries if match_device_id(elem)] + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "remove_device": remove_devices, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.ac_213c7f2_48") + assert not state + state = hass.states.get("binary_sensor.ac_118cdea_2") + assert not state + state = hass.states.get("binary_sensor.ac_1118cdea_2") + assert state + + +async def test_options_add_and_configure_device(hass): + """Test we can add a device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "0913000022670e013970", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "signal_repetitions": 5, + "data_bits": 4, + "off_delay": "abcdef", + "command_on": "xyz", + "command_off": "xyz", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + assert result["errors"] + assert result["errors"]["off_delay"] == "invalid_input_off_delay" + assert result["errors"]["command_on"] == "invalid_input_2262_on" + assert result["errors"]["command_off"] == "invalid_input_2262_off" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "signal_repetitions": 5, + "data_bits": 4, + "command_on": "0xE", + "command_off": "0x7", + "off_delay": "9", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["automatic_add"] + + assert entry.data["devices"]["0913000022670e013970"] + assert not entry.data["devices"]["0913000022670e013970"]["fire_event"] + assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 + assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9 + + state = hass.states.get("binary_sensor.pt2262_22670e") + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].id + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "device": device_entries[0].id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": True, + "signal_repetitions": 5, + "data_bits": 4, + "command_on": "0xE", + "command_off": "0x7", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["devices"]["0913000022670e013970"] + assert entry.data["devices"]["0913000022670e013970"]["fire_event"] + assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 + assert "delay_off" not in entry.data["devices"]["0913000022670e013970"] + + +def test_get_serial_by_id_no_dir(): + """Test serial by id conversion if there's no /dev/serial/by-id.""" + p1 = patch("os.path.isdir", MagicMock(return_value=False)) + p2 = patch("os.scandir") + with p1 as is_dir_mock, p2 as scan_mock: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 0 + + +def test_get_serial_by_id(): + """Test serial by id conversion.""" + p1 = patch("os.path.isdir", MagicMock(return_value=True)) + p2 = patch("os.scandir") + + def _realpath(path): + if path is sentinel.matched_link: + return sentinel.path + return sentinel.serial_link_path + + p3 = patch("os.path.realpath", side_effect=_realpath) + with p1 as is_dir_mock, p2 as scan_mock, p3: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 1 + + entry1 = MagicMock(spec_set=os.DirEntry) + entry1.is_symlink.return_value = True + entry1.path = sentinel.some_path + + entry2 = MagicMock(spec_set=os.DirEntry) + entry2.is_symlink.return_value = False + entry2.path = sentinel.other_path + + entry3 = MagicMock(spec_set=os.DirEntry) + entry3.is_symlink.return_value = True + entry3.path = sentinel.matched_link + + scan_mock.return_value = [entry1, entry2, entry3] + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.matched_link + assert is_dir_mock.call_count == 2 + assert scan_mock.call_count == 2 diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 05ce26ebc10..b3e5ce224c6 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -3,19 +3,23 @@ from unittest.mock import call import pytest +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State -from homeassistant.setup import async_setup_component -from tests.common import mock_restore_cache +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_cover(hass, rfxtrx): """Test with 1 cover.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("cover.lightwaverf_siemens_0213c7_242") @@ -57,11 +61,14 @@ async def test_state_restore(hass, rfxtrx, state): mock_restore_cache(hass, [State(entity_id, state)]) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1400cd0213c7f20d010f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state @@ -69,20 +76,18 @@ async def test_state_restore(hass, rfxtrx, state): async def test_several_covers(hass, rfxtrx): """Test with 3 covers.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1400cd0213c7f20d010f51": {}, - "0A1400ADF394AB010D0060": {}, - "09190000009ba8010100": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}, + "0A1400ADF394AB010D0060": {"signal_repetitions": 1}, + "09190000009ba8010100": {"signal_repetitions": 1}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("cover.lightwaverf_siemens_0213c7_242") @@ -118,19 +123,17 @@ async def test_discover_covers(hass, rfxtrx_automatic): async def test_duplicate_cover(hass, rfxtrx): """Test with 2 duplicate covers.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1400cd0213c7f20d010f51": {}, - "0b1400cd0213c7f20d010f50": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}, + "0b1400cd0213c7f20d010f50": {"signal_repetitions": 1}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("cover.lightwaverf_siemens_0213c7_242") diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index abe7c3c0441..037b08b7cc6 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,10 +1,13 @@ """The tests for the Rfxtrx component.""" +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback from homeassistant.setup import async_setup_component from tests.async_mock import call +from tests.common import MockConfigEntry +from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_valid_config(hass): @@ -55,21 +58,19 @@ async def test_invalid_config(hass): async def test_fire_event(hass, rfxtrx): """Test fire event.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "/dev/serial/by-id/usb" - + "-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0", - "automatic_add": True, - "devices": { - "0b1100cd0213c7f210010f51": {"fire_event": True}, - "0716000100900970": {"fire_event": True}, - }, - } + entry_data = create_rfx_test_cfg( + device="/dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0", + automatic_add=True, + devices={ + "0b1100cd0213c7f210010f51": {"fire_event": True}, + "0716000100900970": {"fire_event": True}, }, ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -101,16 +102,19 @@ async def test_fire_event(hass, rfxtrx): "type_string": "Byron SX", "id_string": "00:90", "data": "0716000100900970", - "values": {"Sound": 9, "Battery numeric": 0, "Rssi numeric": 7}, + "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9}, }, ] async def test_send(hass, rfxtrx): """Test configuration.""" - assert await async_setup_component( - hass, "rfxtrx", {"rfxtrx": {"device": "/dev/null"}} - ) + entry_data = create_rfx_test_cfg(device="/dev/null", devices={}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.services.async_call( diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index f6f056fa16a..78151c5fa9c 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -4,19 +4,23 @@ from unittest.mock import call import pytest from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State -from homeassistant.setup import async_setup_component -from tests.common import mock_restore_cache +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_light(hass, rfxtrx): """Test with 1 light.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("light.ac_213c7f2_16") @@ -95,11 +99,14 @@ async def test_state_restore(hass, rfxtrx, state, brightness): hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})] ) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210020f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state @@ -108,20 +115,18 @@ async def test_state_restore(hass, rfxtrx, state, brightness): async def test_several_lights(hass, rfxtrx): """Test with 3 lights.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1100cd0213c7f230020f71": {}, - "0b1100100118cdea02020f70": {}, - "0b1100101118cdea02050f70": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0b1100cd0213c7f230020f71": {"signal_repetitions": 1}, + "0b1100100118cdea02020f70": {"signal_repetitions": 1}, + "0b1100101118cdea02050f70": {"signal_repetitions": 1}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -160,18 +165,14 @@ async def test_several_lights(hass, rfxtrx): @pytest.mark.parametrize("repetitions", [1, 3]) async def test_repetitions(hass, rfxtrx, repetitions): """Test signal repetitions.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions} - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f230020f71": {"signal_repetitions": repetitions}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.services.async_call( diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index d0100e4ea14..c1bb3222b79 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -1,19 +1,23 @@ """The tests for the Rfxtrx sensor platform.""" import pytest +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import ATTR_EVENT from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import State -from homeassistant.setup import async_setup_component -from tests.common import mock_restore_cache +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_default_config(hass, rfxtrx): """Test with 0 sensor.""" - await async_setup_component( - hass, "sensor", {"sensor": {"platform": "rfxtrx", "devices": {}}} - ) + entry_data = create_rfx_test_cfg(devices={}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -21,11 +25,12 @@ async def test_default_config(hass, rfxtrx): async def test_one_sensor(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}}, - ) + entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02_temperature") @@ -49,11 +54,12 @@ async def test_state_restore(hass, rfxtrx, state, event): mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})]) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0a520801070100b81b0279": {}}}}, - ) + entry_data = create_rfx_test_cfg(devices={"0a520801070100b81b0279": {}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state @@ -61,11 +67,12 @@ async def test_state_restore(hass, rfxtrx, state, event): async def test_one_sensor_no_datatype(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0a52080705020095220269": {}}}}, - ) + entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() base_id = "sensor.wt260_wt260h_wt440h_wt450_wt450h_05_02" @@ -104,19 +111,17 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): async def test_several_sensors(hass, rfxtrx): """Test with 3 sensors.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0a52080705020095220269": {}, - "0a520802060100ff0e0269": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0a52080705020095220269": {}, + "0a520802060100ff0e0269": {}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -244,19 +249,17 @@ async def test_discover_sensor(hass, rfxtrx_automatic): async def test_update_of_sensors(hass, rfxtrx): """Test with 3 sensors.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0a52080705020095220269": {}, - "0a520802060100ff0e0269": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0a52080705020095220269": {}, + "0a520802060100ff0e0269": {}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() @@ -290,23 +293,21 @@ async def test_update_of_sensors(hass, rfxtrx): async def test_rssi_sensor(hass, rfxtrx): """Test with 1 sensor.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0913000022670e013b70": { - "data_bits": 4, - "command_on": 0xE, - "command_off": 0x7, - }, - "0b1100cd0213c7f230010f71": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013b70": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + }, + "0b1100cd0213c7f230010f71": {}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 1fed6a65562..ee4fd265fc9 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -5,9 +5,9 @@ import pytest from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State -from homeassistant.setup import async_setup_component -from tests.common import mock_restore_cache +from tests.common import MockConfigEntry, mock_restore_cache +from tests.components.rfxtrx.conftest import create_rfx_test_cfg EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" @@ -15,11 +15,14 @@ EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" async def test_one_switch(hass, rfxtrx): """Test with 1 switch.""" - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("switch.ac_213c7f2_16") @@ -55,11 +58,14 @@ async def test_state_restore(hass, rfxtrx, state): mock_restore_cache(hass, [State(entity_id, state)]) - assert await async_setup_component( - hass, - "rfxtrx", - {"rfxtrx": {"device": "abcd", "devices": {"0b1100cd0213c7f210010f51": {}}}}, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(entity_id).state == state @@ -67,20 +73,18 @@ async def test_state_restore(hass, rfxtrx, state): async def test_several_switches(hass, rfxtrx): """Test with 3 switches.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1100cd0213c7f230010f71": {}, - "0b1100100118cdea02010f70": {}, - "0b1100101118cdea02010f70": {}, - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={ + "0b1100cd0213c7f230010f71": {"signal_repetitions": 1}, + "0b1100100118cdea02010f70": {"signal_repetitions": 1}, + "0b1100101118cdea02010f70": {"signal_repetitions": 1}, + } ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("switch.ac_213c7f2_48") @@ -102,18 +106,14 @@ async def test_several_switches(hass, rfxtrx): @pytest.mark.parametrize("repetitions", [1, 3]) async def test_repetitions(hass, rfxtrx, repetitions): """Test signal repetitions.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": { - "0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions} - }, - } - }, + entry_data = create_rfx_test_cfg( + devices={"0b1100cd0213c7f230010f71": {"signal_repetitions": repetitions}} ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() await hass.services.async_call( @@ -156,16 +156,12 @@ async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic): async def test_unknown_event_code(hass, rfxtrx): """Test with 3 switches.""" - assert await async_setup_component( - hass, - "rfxtrx", - { - "rfxtrx": { - "device": "abcd", - "devices": {"1234567890": {}}, - } - }, - ) + entry_data = create_rfx_test_cfg(devices={"1234567890": {"signal_repetitions": 1}}) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() conf_entries = hass.config_entries.async_entries(DOMAIN) From ad8ab183cb5f86d94801881ecadb089e420e5b54 Mon Sep 17 00:00:00 2001 From: Eliot Wong Date: Thu, 1 Oct 2020 03:05:44 -0400 Subject: [PATCH 007/831] Rewrite random unittest tests to pytest style test functions (#40920) --- tests/components/random/test_binary_sensor.py | 61 +++++++++---------- tests/components/random/test_sensor.py | 47 ++++++-------- 2 files changed, 48 insertions(+), 60 deletions(-) diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 2f15243c71e..85a8e32018c 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -1,45 +1,44 @@ """The test for the Random binary sensor platform.""" -import unittest - -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.async_mock import patch -from tests.common import get_test_home_assistant -class TestRandomSensor(unittest.TestCase): +async def test_random_binary_sensor_on(hass): """Test the Random binary sensor.""" + config = {"binary_sensor": {"platform": "random", "name": "test"}} - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + with patch( + "homeassistant.components.random.binary_sensor.getrandbits", + return_value=1, + ): + assert await async_setup_component( + hass, + "binary_sensor", + config, + ) + await hass.async_block_till_done() - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() + state = hass.states.get("binary_sensor.test") - @patch("homeassistant.components.random.binary_sensor.getrandbits", return_value=1) - def test_random_binary_sensor_on(self, mocked): - """Test the Random binary sensor.""" - config = {"binary_sensor": {"platform": "random", "name": "test"}} + assert state.state == "on" - assert setup_component(self.hass, "binary_sensor", config) - self.hass.block_till_done() - state = self.hass.states.get("binary_sensor.test") +async def test_random_binary_sensor_off(hass): + """Test the Random binary sensor.""" + config = {"binary_sensor": {"platform": "random", "name": "test"}} - assert state.state == "on" + with patch( + "homeassistant.components.random.binary_sensor.getrandbits", + return_value=False, + ): + assert await async_setup_component( + hass, + "binary_sensor", + config, + ) + await hass.async_block_till_done() - @patch( - "homeassistant.components.random.binary_sensor.getrandbits", return_value=False - ) - def test_random_binary_sensor_off(self, mocked): - """Test the Random binary sensor.""" - config = {"binary_sensor": {"platform": "random", "name": "test"}} + state = hass.states.get("binary_sensor.test") - assert setup_component(self.hass, "binary_sensor", config) - self.hass.block_till_done() - - state = self.hass.states.get("binary_sensor.test") - - assert state.state == "off" + assert state.state == "off" diff --git a/tests/components/random/test_sensor.py b/tests/components/random/test_sensor.py index 657efc9a0cc..9ac2588ff0a 100644 --- a/tests/components/random/test_sensor.py +++ b/tests/components/random/test_sensor.py @@ -1,37 +1,26 @@ """The test for the random number sensor platform.""" -import unittest - -from homeassistant.setup import setup_component - -from tests.common import get_test_home_assistant +from homeassistant.setup import async_setup_component -class TestRandomSensor(unittest.TestCase): +async def test_random_sensor(hass): """Test the Random number sensor.""" - - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - - def test_random_sensor(self): - """Test the Random number sensor.""" - config = { - "sensor": { - "platform": "random", - "name": "test", - "minimum": 10, - "maximum": 20, - } + config = { + "sensor": { + "platform": "random", + "name": "test", + "minimum": 10, + "maximum": 20, } + } - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + assert await async_setup_component( + hass, + "sensor", + config, + ) + await hass.async_block_till_done() - state = self.hass.states.get("sensor.test") + state = hass.states.get("sensor.test") - assert int(state.state) <= config["sensor"]["maximum"] - assert int(state.state) >= config["sensor"]["minimum"] + assert int(state.state) <= config["sensor"]["maximum"] + assert int(state.state) >= config["sensor"]["minimum"] From 632bf4f7f708b5d33a425539662ed8a1d9b0ff2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 09:06:53 +0200 Subject: [PATCH 008/831] Bump actions/setup-python from v2.1.2 to v2.1.3 (#40921) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.1.2 to v2.1.3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.1.2...c181ffa198a1248f902bc2f7965d2f9a36c2d7f6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3d65df477e7..3f5ce5496c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -73,7 +73,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -118,7 +118,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -163,7 +163,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -230,7 +230,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -278,7 +278,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -326,7 +326,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -371,7 +371,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -419,7 +419,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -475,7 +475,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -555,7 +555,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2.1.3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} From df1e910ac74131590e51cb366fcafc7abd54abe9 Mon Sep 17 00:00:00 2001 From: Antetokounpo <31354775+Antetokounpo@users.noreply.github.com> Date: Thu, 1 Oct 2020 03:14:48 -0400 Subject: [PATCH 009/831] Update weather tests to pytest style (#40917) --- tests/components/weather/test_weather.py | 108 ++++++++++------------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index ccddb35ad0a..c32c4d09523 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -1,6 +1,4 @@ """The tests for the Weather component.""" -import unittest - from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_FORECAST, @@ -17,68 +15,58 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ) -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import get_test_home_assistant + +async def test_attributes(hass): + """Test weather attributes.""" + assert await async_setup_component( + hass, weather.DOMAIN, {"weather": {"platform": "demo"}} + ) + hass.config.units = METRIC_SYSTEM + await hass.async_block_till_done() + + state = hass.states.get("weather.demo_weather_south") + assert state is not None + + assert state.state == "sunny" + + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 + assert data.get(ATTR_WEATHER_HUMIDITY) == 92 + assert data.get(ATTR_WEATHER_PRESSURE) == 1099 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 + assert data.get(ATTR_WEATHER_WIND_BEARING) is None + assert data.get(ATTR_WEATHER_OZONE) is None + assert data.get(ATTR_WEATHER_ATTRIBUTION) == "Powered by Home Assistant" + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy" + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 60 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == "fog" + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) == 0.2 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 + assert ( + data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100 + ) + assert len(data.get(ATTR_FORECAST)) == 7 -class TestWeather(unittest.TestCase): - """Test the Weather component.""" +async def test_temperature_convert(hass): + """Test temperature conversion.""" + assert await async_setup_component( + hass, weather.DOMAIN, {"weather": {"platform": "demo"}} + ) + hass.config.units = METRIC_SYSTEM + await hass.async_block_till_done() - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.units = METRIC_SYSTEM - assert setup_component( - self.hass, weather.DOMAIN, {"weather": {"platform": "demo"}} - ) - self.hass.block_till_done() - self.addCleanup(self.tear_down_cleanup) + state = hass.states.get("weather.demo_weather_north") + assert state is not None - def tear_down_cleanup(self): - """Stop down everything that was started.""" - self.hass.stop() + assert state.state == "rainy" - def test_attributes(self): - """Test weather attributes.""" - state = self.hass.states.get("weather.demo_weather_south") - assert state is not None - - assert state.state == "sunny" - - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 - assert data.get(ATTR_WEATHER_HUMIDITY) == 92 - assert data.get(ATTR_WEATHER_PRESSURE) == 1099 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 - assert data.get(ATTR_WEATHER_WIND_BEARING) is None - assert data.get(ATTR_WEATHER_OZONE) is None - assert data.get(ATTR_WEATHER_ATTRIBUTION) == "Powered by Home Assistant" - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy" - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 - assert ( - data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) - == 60 - ) - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == "fog" - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) == 0.2 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 - assert ( - data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) - == 100 - ) - assert len(data.get(ATTR_FORECAST)) == 7 - - def test_temperature_convert(self): - """Test temperature conversion.""" - state = self.hass.states.get("weather.demo_weather_north") - assert state is not None - - assert state.state == "rainy" - - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24 + data = state.attributes + assert data.get(ATTR_WEATHER_TEMPERATURE) == -24 From 2922d4675f3ff180ae07df5440cec28317afbf24 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Oct 2020 09:23:12 +0200 Subject: [PATCH 010/831] Abort deCONZ config flow if no radio hardware is connected (#40811) --- .../components/deconz/config_flow.py | 3 ++ homeassistant/components/deconz/strings.json | 1 + .../components/deconz/translations/en.json | 1 + tests/components/deconz/test_config_flow.py | 31 +++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 6c2df3ad614..c43c1c95504 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -172,6 +172,9 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") + if self.bridge_id == "0000000000000000": + return self.async_abort(reason="no_hardware_available") + return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) async def async_step_ssdp(self, discovery_info): diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index d3e9e884cb2..8880689934b 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -29,6 +29,7 @@ "already_configured": "Bridge is already configured", "already_in_progress": "Config flow for bridge is already in progress.", "no_bridges": "No deCONZ bridges discovered", + "no_hardware_available": "No radio hardware connected to deCONZ", "not_deconz_bridge": "Not a deCONZ bridge", "one_instance_only": "Component only supports one deCONZ instance", "updated_instance": "Updated deCONZ instance with new host address" diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 868c5771199..f762c61ca88 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Bridge is already configured", "already_in_progress": "Config flow for bridge is already in progress.", "no_bridges": "No deCONZ bridges discovered", + "no_hardware_available": "No radio hardware connected to deCONZ", "not_deconz_bridge": "Not a deCONZ bridge", "one_instance_only": "Component only supports one deCONZ instance", "updated_instance": "Updated deCONZ instance with new host address" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 104edca6cca..8e46308ccd7 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -24,6 +24,8 @@ from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration from tests.async_mock import patch +BAD_BRIDGEID = "0000000000000000" + async def test_flow_discovered_bridges(hass, aioclient_mock): """Test that config flow works for discovered bridges.""" @@ -391,6 +393,35 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): } +async def test_flow_ssdp_discovery_bad_bridge_id_aborts(hass, aioclient_mock): + """Test that config flow aborts if deCONZ signals no radio hardware available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: BAD_BRIDGEID, + }, + context={"source": "ssdp"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + aioclient_mock.post( + "http://1.2.3.4:80/api", + json=[{"success": {"username": API_KEY}}], + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_hardware_available" + + async def test_ssdp_discovery_not_deconz_bridge(hass): """Test a non deconz bridge being discovered over ssdp.""" result = await hass.config_entries.flow.async_init( From d95f83b5fbffcbde54b5b48135a9c9bf4b24d4f0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Oct 2020 09:23:32 +0200 Subject: [PATCH 011/831] Improve logging to identify which deCONZ device is at fault (#40808) --- homeassistant/components/deconz/device_trigger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 0bff3f17f9e..e400afd9b9f 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from . import DOMAIN +from .const import LOGGER from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE CONF_SUBTYPE = "subtype" @@ -429,6 +430,7 @@ async def async_attach_trigger(hass, config, action, automation_info): deconz_event = _get_deconz_event_from_device_id(hass, device.id) if deconz_event is None: + LOGGER.error("No deconz_event tied to device %s found", device.name) raise InvalidDeviceAutomationConfig event_id = deconz_event.serial From 95d228cacec4c90120af071ef27d07a9644f15ae Mon Sep 17 00:00:00 2001 From: Eliot Wong Date: Thu, 1 Oct 2020 03:42:23 -0400 Subject: [PATCH 012/831] Rewrite worldclock unittest tests to pytest style test functions (#40922) --- tests/components/worldclock/test_sensor.py | 73 +++++++++++----------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index e029ce783d3..8ed808b9d04 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -1,49 +1,52 @@ """The test for the World clock sensor platform.""" -import unittest +import pytest -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant + +@pytest.fixture +def time_zone(): + """Fixture for time zone.""" + return dt_util.get_time_zone("America/New_York") -class TestWorldClockSensor(unittest.TestCase): - """Test the World clock sensor.""" +async def test_time(hass, time_zone): + """Test the time at a different location.""" + config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.time_zone = dt_util.get_time_zone("America/New_York") + assert await async_setup_component( + hass, + "sensor", + config, + ) + await hass.async_block_till_done() - def test_time(self): - """Test the time at a different location.""" - config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() - self.addCleanup(self.hass.stop) + state = hass.states.get("sensor.worldclock_sensor") + assert state is not None - state = self.hass.states.get("sensor.worldclock_sensor") - assert state is not None + assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M") - assert state.state == dt_util.now(time_zone=self.time_zone).strftime("%H:%M") - def test_time_format(self): - """Test time_format setting.""" - time_format = "%a, %b %d, %Y %I:%M %p" - config = { - "sensor": { - "platform": "worldclock", - "time_zone": "America/New_York", - "time_format": time_format, - } +async def test_time_format(hass, time_zone): + """Test time_format setting.""" + time_format = "%a, %b %d, %Y %I:%M %p" + config = { + "sensor": { + "platform": "worldclock", + "time_zone": "America/New_York", + "time_format": time_format, } - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() - self.addCleanup(self.hass.stop) + } - state = self.hass.states.get("sensor.worldclock_sensor") - assert state is not None + assert await async_setup_component( + hass, + "sensor", + config, + ) + await hass.async_block_till_done() - assert state.state == dt_util.now(time_zone=self.time_zone).strftime( - time_format - ) + state = hass.states.get("sensor.worldclock_sensor") + assert state is not None + + assert state.state == dt_util.now(time_zone=time_zone).strftime(time_format) From 9116061262a00bce30cba0ad9312b36d924c5ff6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Oct 2020 09:50:06 +0200 Subject: [PATCH 013/831] Add lock support to deCONZ (#40807) Co-authored-by: Paulus Schoutsen --- homeassistant/components/deconz/const.py | 7 ++ homeassistant/components/deconz/light.py | 3 +- homeassistant/components/deconz/lock.py | 59 +++++++++++++++ tests/components/deconz/test_gateway.py | 7 +- tests/components/deconz/test_lock.py | 96 ++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/deconz/lock.py create mode 100644 tests/components/deconz/test_lock.py diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 31ae9eb018c..f60c4c35646 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -23,6 +23,7 @@ SUPPORTED_PLATFORMS = [ "climate", "cover", "light", + "lock", "scene", "sensor", "switch", @@ -38,10 +39,16 @@ ATTR_OFFSET = "offset" ATTR_ON = "on" ATTR_VALVE = "valve" +# Covers DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device", "Window covering controller"] COVER_TYPES = DAMPERS + WINDOW_COVERS +# Locks +LOCKS = ["Door Lock"] +LOCK_TYPES = LOCKS + +# Switches POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 3f11cef31da..544699970f2 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -26,6 +26,7 @@ from .const import ( CONF_GROUP_ID_BASE, COVER_TYPES, DOMAIN as DECONZ_DOMAIN, + LOCK_TYPES, NEW_GROUP, NEW_LIGHT, SWITCH_TYPES, @@ -50,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for light in lights: if ( - light.type not in COVER_TYPES + SWITCH_TYPES + light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_TYPES and light.uniqueid not in gateway.entities[DOMAIN] ): entities.append(DeconzLight(light, gateway)) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py new file mode 100644 index 00000000000..1f4fbe57069 --- /dev/null +++ b/homeassistant/components/deconz/lock.py @@ -0,0 +1,59 @@ +"""Support for deCONZ locks.""" +from homeassistant.components.lock import DOMAIN, LockEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import LOCKS, NEW_LIGHT +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up locks for deCONZ component. + + Locks are based on the same device class as lights in deCONZ. + """ + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_lock(lights): + """Add lock from deCONZ.""" + entities = [] + + for light in lights: + + if light.type in LOCKS and light.uniqueid not in gateway.entities[DOMAIN]: + entities.append(DeconzLock(light, gateway)) + + if entities: + async_add_entities(entities, True) + + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock + ) + ) + + async_add_lock(gateway.api.lights.values()) + + +class DeconzLock(DeconzDevice, LockEntity): + """Representation of a deCONZ lock.""" + + TYPE = DOMAIN + + @property + def is_locked(self): + """Return true if lock is on.""" + return self._device.state + + async def async_lock(self, **kwargs): + """Lock the lock.""" + data = {"on": True} + await self._device.async_set_state(data) + + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + data = {"on": False} + await self._device.async_set_state(data) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 64818401bcb..e4dc0424f83 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -91,9 +91,10 @@ async def test_gateway_setup(hass): assert forward_entry_setup.mock_calls[1][1] == (entry, "climate") assert forward_entry_setup.mock_calls[2][1] == (entry, "cover") assert forward_entry_setup.mock_calls[3][1] == (entry, "light") - assert forward_entry_setup.mock_calls[4][1] == (entry, "scene") - assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor") - assert forward_entry_setup.mock_calls[6][1] == (entry, "switch") + assert forward_entry_setup.mock_calls[4][1] == (entry, "lock") + assert forward_entry_setup.mock_calls[5][1] == (entry, "scene") + assert forward_entry_setup.mock_calls[6][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[7][1] == (entry, "switch") async def test_gateway_retry(hass): diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py new file mode 100644 index 00000000000..554f825d42a --- /dev/null +++ b/tests/components/deconz/test_lock.py @@ -0,0 +1,96 @@ +"""deCONZ lock platform tests.""" +from copy import deepcopy + +from homeassistant.components import deconz +import homeassistant.components.lock as lock +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component + +from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration + +from tests.async_mock import patch + +LOCKS = { + "1": { + "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-08-22T15:29:03Z", + "manufacturername": "Danalock", + "modelid": "V3-BTZB", + "name": "Door lock", + "state": {"alert": "none", "on": False, "reachable": True}, + "swversion": "19042019", + "type": "Door Lock", + "uniqueid": "00:00:00:00:00:00:00:00-00", + } +} + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a gateway.""" + assert ( + await async_setup_component( + hass, lock.DOMAIN, {"lock": {"platform": deconz.DOMAIN}} + ) + is True + ) + assert deconz.DOMAIN not in hass.data + + +async def test_no_locks(hass): + """Test that no lock entities are created.""" + gateway = await setup_deconz_integration(hass) + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_locks(hass): + """Test that all supported lock entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(LOCKS) + gateway = await setup_deconz_integration(hass, get_state_response=data) + assert "lock.door_lock" in gateway.deconz_ids + assert len(hass.states.async_all()) == 1 + + door_lock = hass.states.get("lock.door_lock") + assert door_lock.state == STATE_UNLOCKED + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + door_lock = hass.states.get("lock.door_lock") + assert door_lock.state == STATE_LOCKED + + door_lock_device = gateway.api.lights["1"] + + with patch.object(door_lock_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {"entity_id": "lock.door_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + + with patch.object(door_lock_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {"entity_id": "lock.door_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 From 6dc25ccc7b3b853ae765f2c7c7389f1d7aa537cc Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 1 Oct 2020 09:56:46 +0200 Subject: [PATCH 014/831] Fix Netatmo climate error when no boiler status is set (#40815) --- homeassistant/components/netatmo/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index f24591fe954..30ce38753c6 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -315,7 +315,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): @property def hvac_action(self) -> Optional[str]: """Return the current running hvac operation if supported.""" - if self._model == NA_THERM: + if self._model == NA_THERM and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve if self._room_status and self._room_status.get("heating_power_request", 0) > 0: From be9ff3bd66fce03b9417986914b1da7c4439e3cf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 1 Oct 2020 02:57:45 -0500 Subject: [PATCH 015/831] Accept new Plex websocket callback payloads (#40773) --- homeassistant/components/plex/__init__.py | 52 +++++++++++++++++---- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plex/helpers.py | 3 +- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index fff6dea3bb4..95fb6e4f09f 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -5,7 +5,14 @@ import json import logging import plexapi.exceptions -from plexwebsocket import PlexWebsocket +from plexwebsocket import ( + SIGNAL_CONNECTION_STATE, + SIGNAL_DATA, + STATE_CONNECTED, + STATE_DISCONNECTED, + STATE_STOPPED, + PlexWebsocket, +) import requests.exceptions import voluptuous as vol @@ -14,7 +21,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SOURCE, @@ -95,11 +102,12 @@ async def async_setup_entry(hass, entry): entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data} ) except requests.exceptions.ConnectionError as error: - _LOGGER.error( - "Plex server (%s) could not be reached: [%s]", - server_config[CONF_URL], - error, - ) + if entry.state != ENTRY_STATE_SETUP_RETRY: + _LOGGER.error( + "Plex server (%s) could not be reached: [%s]", + server_config[CONF_URL], + error, + ) raise ConfigEntryNotReady from error except plexapi.exceptions.Unauthorized: hass.async_create_task( @@ -142,13 +150,37 @@ async def async_setup_entry(hass, entry): hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - def update_plex(): - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + def plex_websocket_callback(signal, data, error): + """Handle callbacks from plexwebsocket library.""" + if signal == SIGNAL_CONNECTION_STATE: + + if data == STATE_CONNECTED: + _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER]) + elif data == STATE_DISCONNECTED: + _LOGGER.debug( + "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER] + ) + # Stopped websockets without errors are expected during shutdown and ignored + elif data == STATE_STOPPED and error: + _LOGGER.error( + "Websocket to %s failed, aborting [Error: %s]", + entry.data[CONF_SERVER], + error, + ) + asyncio.run_coroutine_threadsafe( + hass.config_entries.async_reload(entry.entry_id), hass.loop + ) + + elif signal == SIGNAL_DATA: + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) session = async_get_clientsession(hass) verify_ssl = server_config.get(CONF_VERIFY_SSL) websocket = PlexWebsocket( - plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl + plex_server.plex_server, + plex_websocket_callback, + session=session, + verify_ssl=verify_ssl, ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 29b0fe8038e..f5bbc6ac53c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==4.1.1", "plexauth==0.0.5", - "plexwebsocket==0.0.11" + "plexwebsocket==0.0.12" ], "dependencies": ["http"], "after_dependencies": ["sonos"], diff --git a/requirements_all.txt b/requirements_all.txt index 0ad24c0ae88..2b141422e0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,7 +1116,7 @@ plexapi==4.1.1 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.11 +plexwebsocket==0.0.12 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86b1faf8965..bb2c30ccbe3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,7 +533,7 @@ plexapi==4.1.1 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.11 +plexwebsocket==0.0.12 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index 8055ab0d5b1..a20d70fbb7e 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,7 +1,8 @@ """Helper methods for Plex tests.""" +from plexwebsocket import SIGNAL_DATA def trigger_plex_update(mock_websocket): """Call the websocket callback method.""" callback = mock_websocket.call_args[0][1] - callback() + callback(SIGNAL_DATA, None, None) From 7285c7806f699484cff20346a721207656974879 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Oct 2020 03:19:20 -0500 Subject: [PATCH 016/831] Seperate state change tracking from async_track_template_result into async_track_state_change_filtered (#40803) --- homeassistant/helpers/event.py | 334 +++++++++++++++++++++------------ tests/helpers/test_event.py | 138 ++++++++++++++ 2 files changed, 349 insertions(+), 123 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 9af781b7abd..b396ebb1d91 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -61,13 +61,27 @@ TRACK_STATE_REMOVED_DOMAIN_LISTENER = "track_state_removed_domain_listener" TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks" TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener" -_TEMPLATE_ALL_LISTENER = "all" -_TEMPLATE_DOMAINS_LISTENER = "domains" -_TEMPLATE_ENTITIES_LISTENER = "entities" +_ALL_LISTENER = "all" +_DOMAINS_LISTENER = "domains" +_ENTITIES_LISTENER = "entities" _LOGGER = logging.getLogger(__name__) +@dataclass +class TrackStates: + """Class for keeping track of states being tracked. + + all_states: All states on the system are being tracked + entities: Entities to track + domains: Domains to track + """ + + all_states: bool + entities: Set + domains: Set + + @dataclass class TrackTemplate: """Class for keeping track of a template with variables. @@ -452,6 +466,158 @@ def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]: return [mstr.lower() for mstr in instr] +class _TrackStateChangeFiltered: + """Handle removal / refresh of tracker.""" + + def __init__( + self, + hass: HomeAssistant, + track_states: TrackStates, + action: Callable[[Event], Any], + ): + """Handle removal / refresh of tracker init.""" + self.hass = hass + self._action = action + self._listeners: Dict[str, Callable] = {} + self._last_track_states: TrackStates = track_states + + @callback + def async_setup(self) -> None: + """Create listeners to track states.""" + track_states = self._last_track_states + + if ( + not track_states.all_states + and not track_states.domains + and not track_states.entities + ): + return + + if track_states.all_states: + self._setup_all_listener() + return + + self._setup_domains_listener(track_states.domains) + self._setup_entities_listener(track_states.domains, track_states.entities) + + @property + def listeners(self) -> Dict: + """State changes that will cause a re-render.""" + track_states = self._last_track_states + return { + _ALL_LISTENER: track_states.all_states, + _ENTITIES_LISTENER: track_states.entities, + _DOMAINS_LISTENER: track_states.domains, + } + + @callback + def async_update_listeners(self, new_track_states: TrackStates) -> None: + """Update the listeners based on the new TrackStates.""" + last_track_states = self._last_track_states + self._last_track_states = new_track_states + + had_all_listener = last_track_states.all_states + + if new_track_states.all_states: + if had_all_listener: + return + self._cancel_listener(_DOMAINS_LISTENER) + self._cancel_listener(_ENTITIES_LISTENER) + self._setup_all_listener() + return + + if had_all_listener: + self._cancel_listener(_ALL_LISTENER) + + domains_changed = new_track_states.domains != last_track_states.domains + + if had_all_listener or domains_changed: + domains_changed = True + self._cancel_listener(_DOMAINS_LISTENER) + self._setup_domains_listener(new_track_states.domains) + + if ( + had_all_listener + or domains_changed + or new_track_states.entities != last_track_states.entities + ): + self._cancel_listener(_ENTITIES_LISTENER) + self._setup_entities_listener( + new_track_states.domains, new_track_states.entities + ) + + @callback + def async_remove(self) -> None: + """Cancel the listeners.""" + for key in list(self._listeners): + self._listeners.pop(key)() + + @callback + def _cancel_listener(self, listener_name: str) -> None: + if listener_name not in self._listeners: + return + + self._listeners.pop(listener_name)() + + @callback + def _setup_entities_listener(self, domains: Set, entities: Set) -> None: + if domains: + entities = entities.copy() + entities.update(self.hass.states.async_entity_ids(domains)) + + # Entities has changed to none + if not entities: + return + + self._listeners[_ENTITIES_LISTENER] = async_track_state_change_event( + self.hass, entities, self._action + ) + + @callback + def _setup_domains_listener(self, domains: Set) -> None: + if not domains: + return + + self._listeners[_DOMAINS_LISTENER] = async_track_state_added_domain( + self.hass, domains, self._action + ) + + @callback + def _setup_all_listener(self) -> None: + self._listeners[_ALL_LISTENER] = self.hass.bus.async_listen( + EVENT_STATE_CHANGED, self._action + ) + + +@callback +@bind_hass +def async_track_state_change_filtered( + hass: HomeAssistant, + track_states: TrackStates, + action: Callable[[Event], Any], +) -> _TrackStateChangeFiltered: + """Track state changes with a TrackStates filter that can be updated. + + Parameters + ---------- + hass + Home assistant object. + track_states + A TrackStates data class. + action + Callable to call with results. + + Returns + ------- + Object used to update the listeners (async_update_listeners) with a new TrackStates or + cancel the tracking (async_remove). + + """ + tracker = _TrackStateChangeFiltered(hass, track_states, action) + tracker.async_setup() + return tracker + + @callback @bind_hass def async_track_template( @@ -557,12 +723,9 @@ class _TrackTemplateResultInfo: track_template_.template.hass = hass self._track_templates = track_templates - self._listeners: Dict[str, Callable] = {} - self._last_result: Dict[Template, Union[str, TemplateError]] = {} self._info: Dict[Template, RenderInfo] = {} - self._last_domains: Set = set() - self._last_entities: Set = set() + self._track_state_changes: Optional[_TrackStateChangeFiltered] = None def async_setup(self, raise_on_template_error: bool) -> None: """Activation of template tracking.""" @@ -580,7 +743,9 @@ class _TrackTemplateResultInfo: exc_info=self._info[template].exception, ) - self._create_listeners() + self._track_state_changes = async_track_state_change_filtered( + self.hass, _render_infos_to_track_states(self._info.values()), self._refresh + ) _LOGGER.debug( "Template group %s listens for %s", self._track_templates, @@ -590,123 +755,14 @@ class _TrackTemplateResultInfo: @property def listeners(self) -> Dict: """State changes that will cause a re-render.""" - return { - "all": _TEMPLATE_ALL_LISTENER in self._listeners, - "entities": self._last_entities, - "domains": self._last_domains, - } - - @property - def _needs_all_listener(self) -> bool: - for info in self._info.values(): - # Tracking all states - if info.all_states or info.all_states_lifecycle: - return True - - # Previous call had an exception - # so we do not know which states - # to track - if info.exception: - return True - - return False - - @property - def _all_templates_are_static(self) -> bool: - for info in self._info.values(): - if not info.is_static: - return False - - return True - - @callback - def _create_listeners(self) -> None: - if self._all_templates_are_static: - return - - if self._needs_all_listener: - self._setup_all_listener() - return - - self._last_entities, self._last_domains = _entities_domains_from_info( - self._info.values() - ) - self._setup_domains_listener(self._last_domains) - self._setup_entities_listener(self._last_domains, self._last_entities) - - @callback - def _cancel_listener(self, listener_name: str) -> None: - if listener_name not in self._listeners: - return - - self._listeners.pop(listener_name)() - - @callback - def _update_listeners(self) -> None: - had_all_listener = _TEMPLATE_ALL_LISTENER in self._listeners - - if self._needs_all_listener: - if had_all_listener: - return - self._last_domains = set() - self._last_entities = set() - self._cancel_listener(_TEMPLATE_DOMAINS_LISTENER) - self._cancel_listener(_TEMPLATE_ENTITIES_LISTENER) - self._setup_all_listener() - return - - if had_all_listener: - self._cancel_listener(_TEMPLATE_ALL_LISTENER) - - entities, domains = _entities_domains_from_info(self._info.values()) - domains_changed = domains != self._last_domains - - if had_all_listener or domains_changed: - domains_changed = True - self._cancel_listener(_TEMPLATE_DOMAINS_LISTENER) - self._setup_domains_listener(domains) - - if had_all_listener or domains_changed or entities != self._last_entities: - self._cancel_listener(_TEMPLATE_ENTITIES_LISTENER) - self._setup_entities_listener(domains, entities) - - self._last_domains = domains - self._last_entities = entities - - @callback - def _setup_entities_listener(self, domains: Set, entities: Set) -> None: - if domains: - entities = entities.copy() - entities.update(self.hass.states.async_entity_ids(domains)) - - # Entities has changed to none - if not entities: - return - - self._listeners[_TEMPLATE_ENTITIES_LISTENER] = async_track_state_change_event( - self.hass, entities, self._refresh - ) - - @callback - def _setup_domains_listener(self, domains: Set) -> None: - if not domains: - return - - self._listeners[_TEMPLATE_DOMAINS_LISTENER] = async_track_state_added_domain( - self.hass, domains, self._refresh - ) - - @callback - def _setup_all_listener(self) -> None: - self._listeners[_TEMPLATE_ALL_LISTENER] = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self._refresh - ) + assert self._track_state_changes + return self._track_state_changes.listeners @callback def async_remove(self) -> None: """Cancel the listener.""" - for key in list(self._listeners): - self._listeners.pop(key)() + assert self._track_state_changes + self._track_state_changes.async_remove() @callback def async_refresh(self) -> None: @@ -765,7 +821,10 @@ class _TrackTemplateResultInfo: updates.append(TrackTemplateResult(template, last_result, result)) if info_changed: - self._update_listeners() + assert self._track_state_changes + self._track_state_changes.async_update_listeners( + _render_infos_to_track_states(self._info.values()), + ) _LOGGER.debug( "Template group %s listens for %s", self._track_templates, @@ -1229,7 +1288,10 @@ def process_state_match( return lambda state: state in parameter_set -def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set, Set]: +@callback +def _entities_domains_from_render_infos( + render_infos: Iterable[RenderInfo], +) -> Tuple[Set, Set]: """Combine from multiple RenderInfo.""" entities = set() domains = set() @@ -1242,3 +1304,29 @@ def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set if render_info.domains_lifecycle: domains.update(render_info.domains_lifecycle) return entities, domains + + +@callback +def _render_infos_needs_all_listener(render_infos: Iterable[RenderInfo]) -> bool: + """Determine if an all listener is needed from RenderInfo.""" + for render_info in render_infos: + # Tracking all states + if render_info.all_states or render_info.all_states_lifecycle: + return True + + # Previous call had an exception + # so we do not know which states + # to track + if render_info.exception: + return True + + return False + + +@callback +def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackStates: + """Create a TrackStates dataclass from the latest RenderInfo.""" + if _render_infos_needs_all_listener(render_infos): + return TrackStates(True, set(), set()) + + return TrackStates(False, *_entities_domains_from_render_infos(render_infos)) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 8bdf9cb891c..887917fa74c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -14,6 +14,7 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.event import ( + TrackStates, TrackTemplate, TrackTemplateResult, async_call_later, @@ -23,6 +24,7 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_state_change, async_track_state_change_event, + async_track_state_change_filtered, async_track_state_removed_domain, async_track_sunrise, async_track_sunset, @@ -255,6 +257,142 @@ async def test_track_state_change(hass): assert len(wildercard_runs) == 6 +async def test_async_track_state_change_filtered(hass): + """Test async_track_state_change_filtered.""" + single_entity_id_tracker = [] + multiple_entity_id_tracker = [] + + @ha.callback + def single_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + single_entity_id_tracker.append((old_state, new_state)) + + @ha.callback + def multiple_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + multiple_entity_id_tracker.append((old_state, new_state)) + + @ha.callback + def callback_that_throws(event): + raise ValueError + + track_single = async_track_state_change_filtered( + hass, TrackStates(False, {"light.bowl"}, None), single_run_callback + ) + assert track_single.listeners == { + "all": False, + "domains": None, + "entities": {"light.bowl"}, + } + + track_multi = async_track_state_change_filtered( + hass, TrackStates(False, {"light.bowl"}, {"switch"}), multiple_run_callback + ) + assert track_multi.listeners == { + "all": False, + "domains": {"switch"}, + "entities": {"light.bowl"}, + } + + track_throws = async_track_state_change_filtered( + hass, TrackStates(False, {"light.bowl"}, {"switch"}), callback_that_throws + ) + assert track_throws.listeners == { + "all": False, + "domains": {"switch"}, + "entities": {"light.bowl"}, + } + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert single_entity_id_tracker[-1][0] is None + assert single_entity_id_tracker[-1][1] is not None + assert len(multiple_entity_id_tracker) == 1 + assert multiple_entity_id_tracker[-1][0] is None + assert multiple_entity_id_tracker[-1][1] is not None + + # Set same state should not trigger a state change/listener + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 1 + + # State change off -> on + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 2 + assert len(multiple_entity_id_tracker) == 2 + + # State change off -> off + hass.states.async_set("light.Bowl", "off", {"some_attr": 1}) + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 3 + assert len(multiple_entity_id_tracker) == 3 + + # State change off -> on + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 4 + assert len(multiple_entity_id_tracker) == 4 + + hass.states.async_remove("light.bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert single_entity_id_tracker[-1][0] is not None + assert single_entity_id_tracker[-1][1] is None + assert len(multiple_entity_id_tracker) == 5 + assert multiple_entity_id_tracker[-1][0] is not None + assert multiple_entity_id_tracker[-1][1] is None + + # Set state for different entity id + hass.states.async_set("switch.kitchen", "on") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert len(multiple_entity_id_tracker) == 6 + + track_single.async_remove() + # Ensure unsubing the listener works + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 5 + assert len(multiple_entity_id_tracker) == 7 + + assert track_multi.listeners == { + "all": False, + "domains": {"switch"}, + "entities": {"light.bowl"}, + } + track_multi.async_update_listeners(TrackStates(False, {"light.bowl"}, None)) + assert track_multi.listeners == { + "all": False, + "domains": None, + "entities": {"light.bowl"}, + } + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(multiple_entity_id_tracker) == 8 + hass.states.async_set("switch.kitchen", "off") + await hass.async_block_till_done() + assert len(multiple_entity_id_tracker) == 8 + + track_multi.async_update_listeners(TrackStates(True, None, None)) + hass.states.async_set("switch.kitchen", "off") + await hass.async_block_till_done() + assert len(multiple_entity_id_tracker) == 8 + hass.states.async_set("switch.any", "off") + await hass.async_block_till_done() + assert len(multiple_entity_id_tracker) == 9 + + track_multi.async_remove() + track_throws.async_remove() + + async def test_async_track_state_change_event(hass): """Test async_track_state_change_event.""" single_entity_id_tracker = [] From d93141c1a9338d3573d4fe54f6566b8feb4fa95f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 1 Oct 2020 03:26:26 -0500 Subject: [PATCH 017/831] Use DataUpdateCoordinator for canary (#40691) Co-authored-by: Paulus Schoutsen --- homeassistant/components/canary/__init__.py | 87 +++---------------- .../components/canary/alarm_control_panel.py | 58 +++++++------ homeassistant/components/canary/camera.py | 39 +++++---- homeassistant/components/canary/const.py | 2 +- .../components/canary/coordinator.py | 59 +++++++++++++ homeassistant/components/canary/sensor.py | 86 ++++++++++-------- tests/components/canary/conftest.py | 9 +- .../canary/test_alarm_control_panel.py | 6 +- tests/components/canary/test_sensor.py | 9 +- 9 files changed, 193 insertions(+), 162 deletions(-) create mode 100644 homeassistant/components/canary/coordinator.py diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 06f4134e24e..64f2d00735e 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -13,16 +13,16 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import Throttle from .const import ( CONF_FFMPEG_ARGUMENTS, - DATA_CANARY, + DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT, DOMAIN, ) +from .coordinator import CanaryDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -89,17 +89,21 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.config_entries.async_update_entry(entry, options=options) try: - canary_data = await hass.async_add_executor_job( - _get_canary_data_instance, entry - ) + canary_api = await hass.async_add_executor_job(_get_canary_api_instance, entry) except (ConnectTimeout, HTTPError) as error: _LOGGER.error("Unable to connect to Canary service: %s", str(error)) raise ConfigEntryNotReady from error + coordinator = CanaryDataUpdateCoordinator(hass, api=canary_api) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { - DATA_CANARY: canary_data, + DATA_COORDINATOR: coordinator, DATA_UNDO_UPDATE_LISTENER: undo_listener, } @@ -134,77 +138,12 @@ async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> await hass.config_entries.async_reload(entry.entry_id) -class CanaryData: - """Manages the data retrieved from Canary API.""" - - def __init__(self, api: Api): - """Init the Canary data object.""" - self._api = api - self._locations_by_id = {} - self._readings_by_device_id = {} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Get the latest data from py-canary with a throttle.""" - self._update(**kwargs) - - def _update(self, **kwargs): - """Get the latest data from py-canary.""" - for location in self._api.get_locations(): - location_id = location.location_id - - self._locations_by_id[location_id] = location - - for device in location.devices: - if device.is_online: - self._readings_by_device_id[ - device.device_id - ] = self._api.get_latest_readings(device.device_id) - - @property - def locations(self): - """Return a list of locations.""" - return self._locations_by_id.values() - - def get_location(self, location_id): - """Return a location based on location_id.""" - return self._locations_by_id.get(location_id, []) - - def get_readings(self, device_id): - """Return a list of readings based on device_id.""" - return self._readings_by_device_id.get(device_id, []) - - def get_reading(self, device_id, sensor_type): - """Return reading for device_id and sensor type.""" - readings = self._readings_by_device_id.get(device_id, []) - return next( - ( - reading.value - for reading in readings - if reading.sensor_type == sensor_type - ), - None, - ) - - def set_location_mode(self, location_id, mode_name, is_private=False): - """Set location mode.""" - self._api.set_location_mode(location_id, mode_name, is_private) - self.update(no_throttle=True) - - def get_live_stream_session(self, device): - """Return live stream session.""" - return self._api.get_live_stream_session(device) - - -def _get_canary_data_instance(entry: ConfigEntry) -> CanaryData: - """Initialize a new instance of CanaryData.""" +def _get_canary_api_instance(entry: ConfigEntry) -> Api: + """Initialize a new instance of CanaryApi.""" canary = Api( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), ) - canary_data = CanaryData(canary) - canary_data.update() - - return canary_data + return canary diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 8d2b01fd5da..957b659eb79 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -19,9 +19,10 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CanaryData -from .const import DATA_CANARY, DOMAIN +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import CanaryDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,25 +33,35 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Canary alarm control panels based on a config entry.""" - data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] - alarms = [CanaryAlarm(data, location.location_id) for location in data.locations] + coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + alarms = [ + CanaryAlarm(coordinator, location) + for location_id, location in coordinator.data["locations"].items() + ] async_add_entities(alarms, True) -class CanaryAlarm(AlarmControlPanelEntity): +class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Representation of a Canary alarm control panel.""" - def __init__(self, data, location_id): + def __init__(self, coordinator, location): """Initialize a Canary security camera.""" - self._data = data - self._location_id = location_id + super().__init__(coordinator) + self._location_id = location.location_id + self._location_name = location.name + + @property + def location(self): + """Return information about the location.""" + return self.coordinator.data["locations"][self._location_id] @property def name(self): """Return the name of the alarm.""" - location = self._data.get_location(self._location_id) - return location.name + return self._location_name @property def unique_id(self): @@ -60,18 +71,17 @@ class CanaryAlarm(AlarmControlPanelEntity): @property def state(self): """Return the state of the device.""" - location = self._data.get_location(self._location_id) - - if location.is_private: + if self.location.is_private: return STATE_ALARM_DISARMED - mode = location.mode + mode = self.location.mode if mode.name == LOCATION_MODE_AWAY: return STATE_ALARM_ARMED_AWAY if mode.name == LOCATION_MODE_HOME: return STATE_ALARM_ARMED_HOME if mode.name == LOCATION_MODE_NIGHT: return STATE_ALARM_ARMED_NIGHT + return None @property @@ -82,26 +92,24 @@ class CanaryAlarm(AlarmControlPanelEntity): @property def device_state_attributes(self): """Return the state attributes.""" - location = self._data.get_location(self._location_id) - return {"private": location.is_private} + return {"private": self.location.is_private} def alarm_disarm(self, code=None): """Send disarm command.""" - location = self._data.get_location(self._location_id) - self._data.set_location_mode(self._location_id, location.mode.name, True) + self.coordinator.canary.set_location_mode( + self._location_id, self.location.mode.name, True + ) def alarm_arm_home(self, code=None): """Send arm home command.""" - self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) + self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME) def alarm_arm_away(self, code=None): """Send arm away command.""" - self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) + self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY) def alarm_arm_night(self, code=None): """Send arm night command.""" - self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) - - def update(self): - """Get the latest state of the sensor.""" - self._data.update() + self.coordinator.canary.set_location_mode( + self._location_id, LOCATION_MODE_NIGHT + ) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 1cc7a535344..4d0a4a0d169 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -15,17 +15,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle -from . import CanaryData from .const import ( CONF_FFMPEG_ARGUMENTS, - DATA_CANARY, + DATA_COORDINATOR, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT, DOMAIN, MANUFACTURER, ) +from .coordinator import CanaryDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -49,21 +50,22 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Canary sensors based on a config entry.""" - data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] - + coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] ffmpeg_arguments = entry.options.get( CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS ) cameras = [] - for location in data.locations: + for location_id, location in coordinator.data["locations"].items(): for device in location.devices: if device.is_online: cameras.append( CanaryCamera( hass, - data, - location, + coordinator, + location_id, device, DEFAULT_TIMEOUT, ffmpeg_arguments, @@ -73,17 +75,15 @@ async def async_setup_entry( async_add_entities(cameras, True) -class CanaryCamera(Camera): +class CanaryCamera(CoordinatorEntity, Camera): """An implementation of a Canary security camera.""" - def __init__(self, hass, data, location, device, timeout, ffmpeg_args): + def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args): """Initialize a Canary security camera.""" - super().__init__() - + super().__init__(coordinator) self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = ffmpeg_args - self._data = data - self._location = location + self._location_id = location_id self._device = device self._device_id = device.device_id self._device_name = device.name @@ -91,6 +91,11 @@ class CanaryCamera(Camera): self._timeout = timeout self._live_stream_session = None + @property + def location(self): + """Return information about the location.""" + return self.coordinator.data["locations"][self._location_id] + @property def name(self): """Return the name of this device.""" @@ -114,12 +119,12 @@ class CanaryCamera(Camera): @property def is_recording(self): """Return true if the device is recording.""" - return self._location.is_recording + return self.location.is_recording @property def motion_detection_enabled(self): """Return the camera motion detection status.""" - return not self._location.is_recording + return not self.location.is_recording async def async_camera_image(self): """Return a still image response from the camera.""" @@ -159,4 +164,6 @@ class CanaryCamera(Camera): @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self): """Renew live stream session.""" - self._live_stream_session = self._data.get_live_stream_session(self._device) + self._live_stream_session = self.coordinator.canary.get_live_stream_session( + self._device + ) diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py index e6e3dbb73c9..8219a485ef9 100644 --- a/homeassistant/components/canary/const.py +++ b/homeassistant/components/canary/const.py @@ -8,7 +8,7 @@ MANUFACTURER = "Canary Connect, Inc" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" # Data -DATA_CANARY = "canary" +DATA_COORDINATOR = "coordinator" DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" # Defaults diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py new file mode 100644 index 00000000000..650bc3d70ea --- /dev/null +++ b/homeassistant/components/canary/coordinator.py @@ -0,0 +1,59 @@ +"""Provides the Canary DataUpdateCoordinator.""" +from datetime import timedelta +import logging + +from async_timeout import timeout +from canary.api import Api +from requests import ConnectTimeout, HTTPError + +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class CanaryDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Canary data.""" + + def __init__(self, hass: HomeAssistantType, *, api: Api): + """Initialize global Canary data updater.""" + self.canary = api + update_interval = timedelta(seconds=30) + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=update_interval, + ) + + def _update_data(self) -> dict: + """Fetch data from Canary via sync functions.""" + locations_by_id = {} + readings_by_device_id = {} + + for location in self.canary.get_locations(): + location_id = location.location_id + locations_by_id[location_id] = location + + for device in location.devices: + if device.is_online: + readings_by_device_id[ + device.device_id + ] = self.canary.get_latest_readings(device.device_id) + + return { + "locations": locations_by_id, + "readings": readings_by_device_id, + } + + async def _async_update_data(self) -> dict: + """Fetch data from Canary.""" + + try: + async with timeout(15): + return await self.hass.async_add_executor_job(self._update_data) + except (ConnectTimeout, HTTPError) as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index acf44457cbf..6ec9f3a87ff 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -14,9 +14,10 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CanaryData -from .const import DATA_CANARY, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .coordinator import CanaryDataUpdateCoordinator SENSOR_VALUE_PRECISION = 2 ATTR_AIR_QUALITY = "air_quality" @@ -49,37 +50,71 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up Canary sensors based on a config entry.""" - data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] + coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] sensors = [] - for location in data.locations: + for location in coordinator.data["locations"].values(): for device in location.devices: if device.is_online: device_type = device.device_type for sensor_type in SENSOR_TYPES: if device_type.get("name") in sensor_type[4]: sensors.append( - CanarySensor(data, sensor_type, location, device) + CanarySensor(coordinator, sensor_type, location, device) ) async_add_entities(sensors, True) -class CanarySensor(Entity): +class CanarySensor(CoordinatorEntity, Entity): """Representation of a Canary sensor.""" - def __init__(self, data, sensor_type, location, device): + def __init__(self, coordinator, sensor_type, location, device): """Initialize the sensor.""" - self._data = data + super().__init__(coordinator) self._sensor_type = sensor_type self._device_id = device.device_id self._device_name = device.name self._device_type_name = device.device_type["name"] - self._sensor_value = None sensor_type_name = sensor_type[0].replace("_", " ").title() self._name = f"{location.name} {device.name} {sensor_type_name}" + canary_sensor_type = None + if self._sensor_type[0] == "air_quality": + canary_sensor_type = SensorType.AIR_QUALITY + elif self._sensor_type[0] == "temperature": + canary_sensor_type = SensorType.TEMPERATURE + elif self._sensor_type[0] == "humidity": + canary_sensor_type = SensorType.HUMIDITY + elif self._sensor_type[0] == "wifi": + canary_sensor_type = SensorType.WIFI + elif self._sensor_type[0] == "battery": + canary_sensor_type = SensorType.BATTERY + + self._canary_type = canary_sensor_type + + @property + def reading(self): + """Return the device sensor reading.""" + readings = self.coordinator.data["readings"][self._device_id] + + value = next( + ( + reading.value + for reading in readings + if reading.sensor_type == self._canary_type + ), + None, + ) + + if value is not None: + return round(float(value), SENSOR_VALUE_PRECISION) + + return None + @property def name(self): """Return the name of the Canary sensor.""" @@ -88,7 +123,7 @@ class CanarySensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._sensor_value + return self.reading @property def unique_id(self): @@ -123,36 +158,17 @@ class CanarySensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self._sensor_type[0] == "air_quality" and self._sensor_value is not None: + reading = self.reading + + if self._sensor_type[0] == "air_quality" and reading is not None: air_quality = None - if self._sensor_value <= 0.4: + if reading <= 0.4: air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL - elif self._sensor_value <= 0.59: + elif reading <= 0.59: air_quality = STATE_AIR_QUALITY_ABNORMAL - elif self._sensor_value <= 1.0: + elif reading <= 1.0: air_quality = STATE_AIR_QUALITY_NORMAL return {ATTR_AIR_QUALITY: air_quality} return None - - def update(self): - """Get the latest state of the sensor.""" - self._data.update() - - canary_sensor_type = None - if self._sensor_type[0] == "air_quality": - canary_sensor_type = SensorType.AIR_QUALITY - elif self._sensor_type[0] == "temperature": - canary_sensor_type = SensorType.TEMPERATURE - elif self._sensor_type[0] == "humidity": - canary_sensor_type = SensorType.HUMIDITY - elif self._sensor_type[0] == "wifi": - canary_sensor_type = SensorType.WIFI - elif self._sensor_type[0] == "battery": - canary_sensor_type = SensorType.BATTERY - - value = self._data.get_reading(self._device_id, canary_sensor_type) - - if value is not None: - self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION) diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 0127865f6a1..01527a193c0 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -5,17 +5,12 @@ from pytest import fixture from tests.async_mock import MagicMock, patch -def mock_canary_update(self, **kwargs): - """Get the latest data from py-canary.""" - self._update(**kwargs) - - @fixture def canary(hass): """Mock the CanaryApi for easier testing.""" with patch.object(Api, "login", return_value=True), patch( - "homeassistant.components.canary.CanaryData.update", mock_canary_update - ), patch("homeassistant.components.canary.Api") as mock_canary: + "homeassistant.components.canary.Api" + ) as mock_canary: instance = mock_canary.return_value = Api( "test-username", "test-password", diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index f1b8fc3396e..930fd9613e0 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -137,7 +137,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None: service_data={"entity_id": entity_id}, blocking=True, ) - instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY, False) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY) # test arm home await hass.services.async_call( @@ -146,7 +146,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None: service_data={"entity_id": entity_id}, blocking=True, ) - instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME, False) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME) # test arm night await hass.services.async_call( @@ -155,7 +155,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None: service_data={"entity_id": entity_id}, blocking=True, ) - instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT, False) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT) # test disarm await hass.services.async_call( diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 13b82c9a996..c17db7b88c9 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the Canary sensor platform.""" +from datetime import timedelta + from homeassistant.components.canary.const import DOMAIN, MANUFACTURER from homeassistant.components.canary.sensor import ( ATTR_AIR_QUALITY, @@ -16,11 +18,12 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from . import mock_device, mock_location, mock_reading from tests.async_mock import patch -from tests.common import mock_device_registry, mock_registry +from tests.common import async_fire_time_changed, mock_device_registry, mock_registry async def test_sensors_pro(hass, canary) -> None: @@ -124,6 +127,8 @@ async def test_sensors_attributes_pro(hass, canary) -> None: mock_reading("air_quality", "0.4"), ] + future = utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() @@ -137,6 +142,8 @@ async def test_sensors_attributes_pro(hass, canary) -> None: mock_reading("air_quality", "1.0"), ] + future += timedelta(seconds=30) + async_fire_time_changed(hass, future) await hass.helpers.entity_component.async_update_entity(entity_id) await hass.async_block_till_done() From 1eb086e86d3f98720c8e8913abeae7d42798a47f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Oct 2020 11:05:00 +0200 Subject: [PATCH 018/831] Revert using own cast app for media (#40937) --- homeassistant/components/cast/media_player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index f62a73860f3..788da18e8bd 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -39,7 +39,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.const import ( - CAST_APP_ID_HOMEASSISTANT, CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, @@ -292,7 +291,6 @@ class CastDevice(MediaPlayerEntity): ), ChromeCastZeroconf.get_zeroconf(), ) - chromecast.media_controller.app_id = CAST_APP_ID_HOMEASSISTANT self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: From 22ba6e06fd546e8511662acfb2741989a3c71ca1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 1 Oct 2020 11:22:30 +0200 Subject: [PATCH 019/831] Use direct service calls in demo climate tests instead of climate common (#40934) --- tests/components/demo/test_climate.py | 201 +++++++++++++++++++------- 1 file changed, 152 insertions(+), 49 deletions(-) diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 91e14247834..aa6ff39cb0e 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -10,6 +10,7 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, @@ -26,13 +27,25 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, + SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HUMIDITY, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.components.climate import common - ENTITY_CLIMATE = "climate.hvac" ENTITY_ECOBEE = "climate.ecobee" ENTITY_HEATPUMP = "climate.heatpump" @@ -82,9 +95,14 @@ async def test_set_only_target_temp_bad_attr(hass): assert state.attributes.get(ATTR_TEMPERATURE) == 21 with pytest.raises(vol.Invalid): - await common.async_set_temperature(hass, None, ENTITY_CLIMATE) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_TEMPERATURE: None}, + blocking=True, + ) - await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_TEMPERATURE) == 21 @@ -93,8 +111,12 @@ async def test_set_only_target_temp(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_TEMPERATURE) == 21 - await common.async_set_temperature(hass, 30, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_TEMPERATURE: 30}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_TEMPERATURE) == 30.0 @@ -105,8 +127,12 @@ async def test_set_only_target_temp_with_convert(hass): state = hass.states.get(ENTITY_HEATPUMP) assert state.attributes.get(ATTR_TEMPERATURE) == 20 - await common.async_set_temperature(hass, 21, ENTITY_HEATPUMP) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_HEATPUMP, ATTR_TEMPERATURE: 21}, + blocking=True, + ) state = hass.states.get(ENTITY_HEATPUMP) assert state.attributes.get(ATTR_TEMPERATURE) == 21.0 @@ -119,10 +145,16 @@ async def test_set_target_temp_range(hass): assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 21.0 assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24.0 - await common.async_set_temperature( - hass, target_temp_high=25, target_temp_low=20, entity_id=ENTITY_ECOBEE + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ECOBEE, + ATTR_TARGET_TEMP_LOW: 20, + ATTR_TARGET_TEMP_HIGH: 25, + }, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(ENTITY_ECOBEE) assert state.attributes.get(ATTR_TEMPERATURE) is None @@ -138,14 +170,16 @@ async def test_set_target_temp_range_bad_attr(hass): assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24.0 with pytest.raises(vol.Invalid): - await common.async_set_temperature( - hass, - temperature=None, - entity_id=ENTITY_ECOBEE, - target_temp_low=None, - target_temp_high=None, + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ECOBEE, + ATTR_TARGET_TEMP_LOW: None, + ATTR_TARGET_TEMP_HIGH: None, + }, + blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(ENTITY_ECOBEE) assert state.attributes.get(ATTR_TEMPERATURE) is None @@ -159,8 +193,12 @@ async def test_set_target_humidity_bad_attr(hass): assert state.attributes.get(ATTR_HUMIDITY) == 67 with pytest.raises(vol.Invalid): - await common.async_set_humidity(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HUMIDITY: None}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HUMIDITY) == 67 @@ -171,8 +209,12 @@ async def test_set_target_humidity(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HUMIDITY) == 67 - await common.async_set_humidity(hass, 64, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HUMIDITY: 64}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HUMIDITY) == 64.0 @@ -184,8 +226,12 @@ async def test_set_fan_mode_bad_attr(hass): assert state.attributes.get(ATTR_FAN_MODE) == "On High" with pytest.raises(vol.Invalid): - await common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: None}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_FAN_MODE) == "On High" @@ -196,8 +242,12 @@ async def test_set_fan_mode(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_FAN_MODE) == "On High" - await common.async_set_fan_mode(hass, "On Low", ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: "On Low"}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_FAN_MODE) == "On Low" @@ -209,8 +259,12 @@ async def test_set_swing_mode_bad_attr(hass): assert state.attributes.get(ATTR_SWING_MODE) == "Off" with pytest.raises(vol.Invalid): - await common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: None}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_SWING_MODE) == "Off" @@ -221,8 +275,12 @@ async def test_set_swing(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_SWING_MODE) == "Off" - await common.async_set_swing_mode(hass, "Auto", ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "Auto"}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_SWING_MODE) == "Auto" @@ -238,8 +296,12 @@ async def test_set_hvac_bad_attr_and_state(hass): assert state.state == HVAC_MODE_COOL with pytest.raises(vol.Invalid): - await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: None}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL @@ -251,8 +313,12 @@ async def test_set_hvac(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_COOL - await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_HEAT @@ -260,8 +326,12 @@ async def test_set_hvac(hass): async def test_set_hold_mode_away(hass): """Test setting the hold mode away.""" - await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_ECOBEE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ECOBEE, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) state = hass.states.get(ENTITY_ECOBEE) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY @@ -269,8 +339,12 @@ async def test_set_hold_mode_away(hass): async def test_set_hold_mode_eco(hass): """Test setting the hold mode eco.""" - await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_ECOBEE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ECOBEE, ATTR_PRESET_MODE: PRESET_ECO}, + blocking=True, + ) state = hass.states.get(ENTITY_ECOBEE) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO @@ -282,16 +356,25 @@ async def test_set_aux_heat_bad_attr(hass): assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF with pytest.raises(vol.Invalid): - await common.async_set_aux_heat(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: None}, + blocking=True, + ) + state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF async def test_set_aux_heat_on(hass): """Test setting the axillary heater on/true.""" - await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: True}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_AUX_HEAT) == STATE_ON @@ -299,8 +382,12 @@ async def test_set_aux_heat_on(hass): async def test_set_aux_heat_off(hass): """Test setting the auxiliary heater off/false.""" - await common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: False}, + blocking=True, + ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF @@ -308,21 +395,37 @@ async def test_set_aux_heat_off(hass): async def test_turn_on(hass): """Test turn on device.""" - await common.async_set_hvac_mode(hass, HVAC_MODE_OFF, ENTITY_CLIMATE) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_OFF - await common.async_turn_on(hass, ENTITY_CLIMATE) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True + ) state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_HEAT async def test_turn_off(hass): """Test turn on device.""" - await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_HEAT - await common.async_turn_off(hass, ENTITY_CLIMATE) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_CLIMATE}, blocking=True + ) state = hass.states.get(ENTITY_CLIMATE) assert state.state == HVAC_MODE_OFF From 0a700f7272abb948317066a47c4232d3d03f911e Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 1 Oct 2020 11:31:43 +0200 Subject: [PATCH 020/831] Common strings in Freebox config flow (#40938) --- homeassistant/components/freebox/config_flow.py | 2 +- homeassistant/components/freebox/strings.json | 2 +- tests/components/freebox/test_config_flow.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 0589dfb2ef1..d776c34c4f9 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -92,7 +92,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except HttpRequestError: _LOGGER.error("Error connecting to the Freebox router at %s", self._host) - errors["base"] = "connection_failed" + errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index 2257e7bd908..cb48e5322de 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -15,7 +15,7 @@ }, "error": { "register_failed": "Failed to register, please try again", - "connection_failed": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index a4c8a950299..addb1762df0 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -139,7 +139,7 @@ async def test_on_link_failed(hass): ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "connection_failed"} + assert result["errors"] == {"base": "cannot_connect"} with patch( "homeassistant.components.freebox.router.Freepybox.open", From 78ebd1add9aae136c1c6fec110b8bd5fe37ff0e3 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 1 Oct 2020 11:36:26 +0200 Subject: [PATCH 021/831] Use of reference strings in meteo france config flow (#40939) --- homeassistant/components/meteo_france/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json index 611d1ca054c..4deb17d01e6 100644 --- a/homeassistant/components/meteo_france/strings.json +++ b/homeassistant/components/meteo_france/strings.json @@ -21,7 +21,7 @@ }, "abort": { "already_configured": "City already configured", - "unknown": "Unknown error: please retry later" + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "options": { @@ -33,4 +33,4 @@ } } } -} \ No newline at end of file +} From 04f87eedf584dbb11f73f18771761b41c8e6395b Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 1 Oct 2020 12:59:35 +0300 Subject: [PATCH 022/831] Add context to event trigger (#40932) --- .../homeassistant/triggers/event.py | 37 ++++++---- .../homeassistant/triggers/test_event.py | 73 ++++++++++++++++--- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 800d6bb5f77..4498702bac4 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -11,6 +11,7 @@ from homeassistant.helpers import config_validation as cv CONF_EVENT_TYPE = "event_type" CONF_EVENT_DATA = "event_data" +CONF_EVENT_CONTEXT = "context" _LOGGER = logging.getLogger(__name__) @@ -19,36 +20,42 @@ TRIGGER_SCHEMA = vol.Schema( vol.Required(CONF_PLATFORM): "event", vol.Required(CONF_EVENT_TYPE): cv.string, vol.Optional(CONF_EVENT_DATA): dict, + vol.Optional(CONF_EVENT_CONTEXT): dict, } ) +def _populate_schema(config, config_parameter): + if config_parameter not in config: + return None + + return vol.Schema( + {vol.Required(key): value for key, value in config[config_parameter].items()}, + extra=vol.ALLOW_EXTRA, + ) + + async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="event" ): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) - event_data_schema = None - if config.get(CONF_EVENT_DATA): - event_data_schema = vol.Schema( - { - vol.Required(key): value - for key, value in config.get(CONF_EVENT_DATA).items() - }, - extra=vol.ALLOW_EXTRA, - ) + event_data_schema = _populate_schema(config, CONF_EVENT_DATA) + event_context_schema = _populate_schema(config, CONF_EVENT_CONTEXT) @callback def handle_event(event): """Listen for events and calls the action when data matches.""" - if event_data_schema: - # Check that the event data matches the configured + try: + # Check that the event data and context match the configured # schema if one was provided - try: + if event_data_schema: event_data_schema(event.data) - except vol.Invalid: - # If event data doesn't match requested schema, skip event - return + if event_context_schema: + event_context_schema(event.context.as_dict()) + except vol.Invalid: + # If event doesn't match, skip event + return hass.async_run_job( action, diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 0e4c2674b1b..aa0acae254c 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -15,6 +15,12 @@ def calls(hass): return async_mock_service(hass, "test", "automation") +@pytest.fixture +def context_with_user(): + """Track calls to a mock service.""" + return Context(user_id="test_user_id") + + @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" @@ -53,8 +59,8 @@ async def test_if_fires_on_event(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_event_extra_data(hass, calls): - """Test the firing of events still matches with event data.""" +async def test_if_fires_on_event_extra_data(hass, calls, context_with_user): + """Test the firing of events still matches with event data and context.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -65,8 +71,9 @@ async def test_if_fires_on_event_extra_data(hass, calls): } }, ) - - hass.bus.async_fire("test_event", {"extra_key": "extra_data"}) + hass.bus.async_fire( + "test_event", {"extra_key": "extra_data"}, context=context_with_user + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -82,8 +89,8 @@ async def test_if_fires_on_event_extra_data(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_event_with_data(hass, calls): - """Test the firing of events with data.""" +async def test_if_fires_on_event_with_data_and_context(hass, calls, context_with_user): + """Test the firing of events with data and context.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -96,6 +103,7 @@ async def test_if_fires_on_event_with_data(hass, calls): "some_attr": "some_value", "second_attr": "second_value", }, + "context": {"user_id": context_with_user.user_id}, }, "action": {"service": "test.automation"}, } @@ -105,17 +113,31 @@ async def test_if_fires_on_event_with_data(hass, calls): hass.bus.async_fire( "test_event", {"some_attr": "some_value", "another": "value", "second_attr": "second_value"}, + context=context_with_user, ) await hass.async_block_till_done() assert len(calls) == 1 - hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + hass.bus.async_fire( + "test_event", + {"some_attr": "some_value", "another": "value"}, + context=context_with_user, + ) await hass.async_block_till_done() assert len(calls) == 1 # No new call + hass.bus.async_fire( + "test_event", + {"some_attr": "some_value", "another": "value", "second_attr": "second_value"}, + ) + await hass.async_block_till_done() + assert len(calls) == 1 -async def test_if_fires_on_event_with_empty_data_config(hass, calls): - """Test the firing of events with empty data config. + +async def test_if_fires_on_event_with_empty_data_and_context_config( + hass, calls, context_with_user +): + """Test the firing of events with empty data and context config. The frontend automation editor can produce configurations with an empty dict for event_data instead of no key. @@ -129,13 +151,18 @@ async def test_if_fires_on_event_with_empty_data_config(hass, calls): "platform": "event", "event_type": "test_event", "event_data": {}, + "context": {}, }, "action": {"service": "test.automation"}, } }, ) - hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + hass.bus.async_fire( + "test_event", + {"some_attr": "some_value", "another": "value"}, + context=context_with_user, + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -165,7 +192,7 @@ async def test_if_fires_on_event_with_nested_data(hass, calls): async def test_if_not_fires_if_event_data_not_matches(hass, calls): - """Test firing of event if no match.""" + """Test firing of event if no data match.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -184,3 +211,27 @@ async def test_if_not_fires_if_event_data_not_matches(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_other_value"}) await hass.async_block_till_done() assert len(calls) == 0 + + +async def test_if_not_fires_if_event_context_not_matches( + hass, calls, context_with_user +): + """Test firing of event if no context match.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "test_event", + "context": {"user_id": "some_user"}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire("test_event", {}, context=context_with_user) + await hass.async_block_till_done() + assert len(calls) == 0 From b4b056b75bc3ccd8dd18003e5b19b1284c887afe Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 1 Oct 2020 12:07:18 +0200 Subject: [PATCH 023/831] Handle Shelly channel names (if available) for emeters devices (#40820) --- homeassistant/components/shelly/entity.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 96c35eecbf1..237deec4da1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -23,7 +23,12 @@ def temperature_unit(block_info: dict) -> str: def shelly_naming(self, block, entity_type: str): """Naming for switch and sensors.""" + entity_name = self.wrapper.name + if not block: + return f"{entity_name} {self.description.name}" + channels = 0 + mode = "relays" if "num_outputs" in self.wrapper.device.shelly: channels = self.wrapper.device.shelly["num_outputs"] if ( @@ -31,12 +36,21 @@ def shelly_naming(self, block, entity_type: str): and self.wrapper.device.settings["mode"] == "roller" ): channels = 1 - - entity_name = self.wrapper.name + if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly: + channels = self.wrapper.device.shelly["num_emeters"] + mode = "emeters" if channels > 1 and block.type != "device": - entity_name = self.wrapper.device.settings["relays"][int(block.channel)]["name"] + # Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release + if "name" in self.wrapper.device.settings[mode][int(block.channel)]: + entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"] + else: + entity_name = None if not entity_name: - entity_name = f"{self.wrapper.name} channel {int(block.channel)+1}" + if self.wrapper.model == "SHEM-3": + base = ord("A") + else: + base = ord("1") + entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}" if entity_type == "switch": return entity_name From d1c04750cd629eee447234e98d175b8d3196782d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Oct 2020 12:08:03 +0200 Subject: [PATCH 024/831] Add voltage, power factor and energy returned sensors to Shelly integration (#40681) --- homeassistant/components/shelly/sensor.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 14c1b645118..e82f167ca60 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, POWER_WATT, + VOLT, ) from .entity import ( @@ -53,6 +54,18 @@ SENSORS = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, ), + ("emeter", "voltage"): BlockAttributeDescription( + name="Voltage", + unit=VOLT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_VOLTAGE, + ), + ("emeter", "powerFactor"): BlockAttributeDescription( + name="Power Factor", + unit=PERCENTAGE, + value=lambda value: round(value * 100, 1), + device_class=sensor.DEVICE_CLASS_POWER_FACTOR, + ), ("relay", "power"): BlockAttributeDescription( name="Power", unit=POWER_WATT, @@ -77,6 +90,12 @@ SENSORS = { value=lambda value: round(value / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, ), + ("emeter", "energyReturned"): BlockAttributeDescription( + name="Energy Returned", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), ("light", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, From a3b62cea6ab0a8cee3fd0ec78901461dc6f35add Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 1 Oct 2020 12:27:47 +0200 Subject: [PATCH 025/831] Use common strings in roomba config flow (#40948) --- homeassistant/components/roomba/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index d22a6e0509c..cbe7c06ae36 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -14,7 +14,7 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "options": { @@ -27,4 +27,4 @@ } } } -} \ No newline at end of file +} From 3195ce5d56ff9bdffb9febfdbbd6a86e9021a1dc Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 1 Oct 2020 12:28:32 +0200 Subject: [PATCH 026/831] Use of reference strings for Airly config flow (#40946) --- homeassistant/components/airly/config_flow.py | 2 +- homeassistant/components/airly/strings.json | 4 ++-- tests/components/airly/test_config_flow.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 84bad2d3719..8b3b1949ec3 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -39,7 +39,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() api_key_valid = await self._test_api_key(websession, user_input["api_key"]) if not api_key_valid: - self._errors["base"] = "auth" + self._errors["base"] = "invalid_api_key" else: location_valid = await self._test_location( websession, diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 8bf7782e910..41b58615ea8 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -14,10 +14,10 @@ }, "error": { "wrong_location": "No Airly measuring stations in this area.", - "auth": "API key is not correct." + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { "already_configured": "Airly integration for these coordinates is already configured." } } -} \ No newline at end of file +} diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 243a92258eb..d7d45bbd7e3 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -48,7 +48,7 @@ async def test_invalid_api_key(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["errors"] == {"base": "auth"} + assert result["errors"] == {"base": "invalid_api_key"} async def test_invalid_location(hass): From 75a6dacabaded67e46fde4e9855b0874d8e50b68 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Oct 2020 12:54:10 +0200 Subject: [PATCH 027/831] Use translation references for gios config flow (#40952) --- homeassistant/components/gios/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json index 2187bcbc998..3eb2bb99ee2 100644 --- a/homeassistant/components/gios/strings.json +++ b/homeassistant/components/gios/strings.json @@ -13,10 +13,10 @@ "error": { "wrong_station_id": "ID of the measuring station is not correct.", "invalid_sensors_data": "Invalid sensors' data for this measuring station.", - "cannot_connect": "Cannot connect to the GIO\u015a server." + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "GIO\u015a integration for this measuring station is already configured." + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } From 1b864aeccc1af2215048ef210f83be638a684607 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 1 Oct 2020 12:56:55 +0200 Subject: [PATCH 028/831] Use common strings in adguard config flow (#40942) --- homeassistant/components/adguard/config_flow.py | 4 ++-- homeassistant/components/adguard/strings.json | 6 ++++-- tests/components/adguard/test_config_flow.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index a0ace623862..c799c4a7135 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -80,7 +80,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): try: await adguard.version() except AdGuardHomeConnectionError: - errors["base"] = "connection_error" + errors["base"] = "cannot_connect" return await self._show_setup_form(errors) return self.async_create_entry( @@ -152,7 +152,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): try: await adguard.version() except AdGuardHomeConnectionError: - errors["base"] = "connection_error" + errors["base"] = "cannot_connect" return await self._show_hassio_form(errors) return self.async_create_entry( diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index f010f9e2ade..2658f7344d4 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -17,10 +17,12 @@ "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?" } }, - "error": { "connection_error": "Failed to connect." }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { "existing_instance_updated": "Updated existing configuration.", - "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } } diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index e773768ebe6..36263335dac 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -53,7 +53,7 @@ async def test_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "connection_error"} + assert result["errors"] == {"base": "cannot_connect"} async def test_full_flow_implementation(hass, aioclient_mock): @@ -235,4 +235,4 @@ async def test_hassio_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" - assert result["errors"] == {"base": "connection_error"} + assert result["errors"] == {"base": "cannot_connect"} From 0d523d7116b07ff7817990ee39608d2044e1edd3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Oct 2020 13:02:53 +0200 Subject: [PATCH 029/831] Use translation references for Brother config flow (#40953) --- homeassistant/components/brother/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 264992a7eae..4faa177a379 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -19,12 +19,12 @@ }, "error": { "wrong_host": "Invalid hostname or IP address.", - "connection_error": "Connection error.", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "snmp_error": "SNMP server turned off or printer not supported." }, "abort": { "unsupported_model": "This printer model is not supported.", - "already_configured": "This printer is already configured." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } \ No newline at end of file From fb3de7e3e686550ac26a7a7c97e9ad49e44fedb5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Oct 2020 13:06:22 +0200 Subject: [PATCH 030/831] Test reloading webhook trigger (#40950) --- tests/components/webhook/test_trigger.py | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index fcd024652e8..30601ef6bec 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -4,6 +4,8 @@ import pytest from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(autouse=True) async def setup_http(hass): @@ -109,3 +111,60 @@ async def test_webhook_query(hass, aiohttp_client): assert len(events) == 1 assert events[0].data["hello"] == "yo world" + + +async def test_webhook_reload(hass, aiohttp_client): + """Test reloading a webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen("test_success", store_event) + + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + } + }, + ) + + client = await aiohttp_client(hass.http.app) + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + + assert len(events) == 1 + assert events[0].data["hello"] == "yo world" + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + "automation": { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success", + "event_data_template": {"hello": "yo2 {{ trigger.data.hello }}"}, + }, + } + }, + ): + await hass.services.async_call( + "automation", + "reload", + blocking=True, + ) + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + + assert len(events) == 2 + assert events[1].data["hello"] == "yo2 world" From 670404f43a8d3509fb6a7bbfdd887ec9d7200916 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 1 Oct 2020 13:16:07 +0200 Subject: [PATCH 031/831] Use translation references for BraviaTV config flow (#40955) --- homeassistant/components/braviatv/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index c066f91d395..3a7a5b45a0e 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -18,11 +18,11 @@ }, "error": { "invalid_host": "Invalid hostname or IP address.", - "cannot_connect": "Failed to connect, invalid host or PIN code.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unsupported_model": "Your TV model is not supported." }, "abort": { - "already_configured": "This TV is already configured.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." } }, From 1d138f07737895aaf2cdda920f6e93115779de49 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Oct 2020 13:38:59 +0200 Subject: [PATCH 032/831] Use common string in TPLink config flow (#40958) --- homeassistant/components/tplink/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index cbb89653607..a10c44b9252 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -6,8 +6,8 @@ } }, "abort": { - "single_instance_allowed": "Only a single configuration is necessary.", - "no_devices_found": "No TP-Link devices found on the network." + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } } From 46e8655fcf57e71e37ebfb20a1e10d024b4a5005 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 1 Oct 2020 14:51:05 +0200 Subject: [PATCH 033/831] Use reference strings for Nut configflow (#40966) --- homeassistant/components/nut/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 2203a501ff2..1b71280b6a9 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -25,11 +25,11 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { @@ -43,4 +43,4 @@ } } } -} \ No newline at end of file +} From bd1de3cd7c16ed0c449c6d514fb61d6e91d2de01 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 1 Oct 2020 14:53:33 +0200 Subject: [PATCH 034/831] Use common strings in spotify config flow (#40957) --- homeassistant/components/spotify/strings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 5a7e56013cc..76cad9bdf80 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -1,14 +1,15 @@ { "config": { "step": { - "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, "reauth_confirm": { "title": "Re-authenticate with Spotify", "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" } }, "abort": { - "already_setup": "You can only configure one Spotify account.", "authorize_url_timeout": "Timeout generating authorize url.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", From 20f8bcc908cdcb75e945f5e24277649966aa992c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 1 Oct 2020 14:55:45 +0200 Subject: [PATCH 035/831] Use common strings in mqtt config flow (#40956) --- homeassistant/components/mqtt/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index d2d18af6e60..8c3db8e5b61 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -20,10 +20,10 @@ } }, "abort": { - "single_instance_allowed": "Only a single configuration of MQTT is allowed." + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "error": { - "cannot_connect": "Unable to connect to the broker." + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "device_automation": { @@ -77,7 +77,7 @@ } }, "error": { - "cannot_connect": "Unable to connect to the broker.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "bad_birth": "Invalid birth topic.", "bad_will": "Invalid will topic." } From f90f6904e0cef106a1b4eccc5b9adb4ebc454911 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Oct 2020 15:05:50 +0200 Subject: [PATCH 036/831] Upgrade surepy to 0.2.6 (#40964) --- homeassistant/components/surepetcare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 659a6091299..2fbe4fe245f 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,5 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.2.5"] + "requirements": ["surepy==0.2.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b141422e0b..168078293d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2108,7 +2108,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.5 +surepy==0.2.6 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb2c30ccbe3..1258e49322a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ stringcase==1.2.0 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.5 +surepy==0.2.6 # homeassistant.components.tellduslive tellduslive==0.10.11 From e0d14603f1bfaa30e39b517fe86c5057cb1bf8af Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 1 Oct 2020 15:33:44 +0200 Subject: [PATCH 037/831] Replace strings in atag component (#40935) --- homeassistant/components/atag/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index 85e22c10c1b..22d70921420 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -6,14 +6,14 @@ "title": "Connect to the device", "data": { "host": "[%key:common::config_flow::data::host%]", - "email": "Email (Optional)", + "email": "[%key:common::config_flow::data::email%]", "port": "[%key:common::config_flow::data::port%]" } } }, "error": { "unauthorized": "Pairing denied, check device for auth request", - "connection_error": "Failed to connect, please try again" + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_configured": "This device has already been added to HomeAssistant" From 480066ba636d1add42dad7fe0f2a722204be87f0 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 1 Oct 2020 15:36:57 +0200 Subject: [PATCH 038/831] Add longitude and latitude common strings (#40963) --- homeassistant/components/accuweather/strings.json | 4 ++-- homeassistant/components/airly/strings.json | 4 ++-- homeassistant/components/airvisual/strings.json | 6 +++--- homeassistant/components/flunearyou/strings.json | 5 ++++- homeassistant/components/ipma/strings.json | 4 ++-- homeassistant/components/met/strings.json | 4 ++-- homeassistant/components/metoffice/strings.json | 6 +++--- homeassistant/components/nws/strings.json | 6 +++--- homeassistant/components/openuv/strings.json | 4 ++-- homeassistant/components/openweathermap/strings.json | 4 ++-- homeassistant/components/smhi/strings.json | 4 ++-- homeassistant/strings.json | 4 +++- 12 files changed, 30 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 89228cd0692..102635c2d40 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -7,8 +7,8 @@ "data": { "name": "Name of the integration", "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 41b58615ea8..9d335232286 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -7,8 +7,8 @@ "data": { "name": "Name of the integration", "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index c7e2bc34701..cb0e01a92e5 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -6,8 +6,8 @@ "description": "Use the AirVisual cloud API to monitor a geographical location.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } }, "node_pro": { @@ -47,4 +47,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json index 2a7e59989b0..7394fbcb680 100644 --- a/homeassistant/components/flunearyou/strings.json +++ b/homeassistant/components/flunearyou/strings.json @@ -4,7 +4,10 @@ "user": { "title": "Configure Flu Near You", "description": "Monitor user-based and CDC repots for a pair of coordinates.", - "data": { "latitude": "Latitude", "longitude": "Longitude" } + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } } }, "error": { "general_error": "There was an unknown error." }, diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index 5b325938411..31174f64c3f 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -6,8 +6,8 @@ "description": "Instituto Portugu\u00eas do Mar e Atmosfera", "data": { "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", "mode": "Mode" } } diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index 814df01b49e..81427f267ca 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -6,8 +6,8 @@ "description": "Meteorologisk institutt", "data": { "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", "elevation": "Elevation" } } diff --git a/homeassistant/components/metoffice/strings.json b/homeassistant/components/metoffice/strings.json index 74d8b16542a..77c3f5d65eb 100644 --- a/homeassistant/components/metoffice/strings.json +++ b/homeassistant/components/metoffice/strings.json @@ -6,8 +6,8 @@ "title": "Connect to the UK Met Office", "data": { "api_key": "Met Office DataPoint API key", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, @@ -19,4 +19,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/nws/strings.json b/homeassistant/components/nws/strings.json index 83e8a3a694b..91a3b9c5254 100644 --- a/homeassistant/components/nws/strings.json +++ b/homeassistant/components/nws/strings.json @@ -6,8 +6,8 @@ "title": "Connect to the National Weather Service", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "Latitude", - "longitude": "Longitude", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", "station": "METAR station code" } } @@ -20,4 +20,4 @@ "already_configured": "Device is already configured" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 0777b139cf9..039a7a9d689 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -6,8 +6,8 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "elevation": "Elevation", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index e068bb91964..79462f6912b 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -12,8 +12,8 @@ "data": { "api_key": "OpenWeatherMap API key", "language": "Language", - "latitude": "Latitude", - "longitude": "Longitude", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", "mode": "Mode", "name": "Name of the integration" }, diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json index 245260b0fce..759b13f3c04 100644 --- a/homeassistant/components/smhi/strings.json +++ b/homeassistant/components/smhi/strings.json @@ -5,8 +5,8 @@ "title": "Location in Sweden", "data": { "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude" + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" } } }, diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 05bc2e3c247..8f025ad5ca3 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -34,7 +34,9 @@ "url": "URL", "usb_path": "USB Device Path", "access_token": "Access Token", - "api_key": "API Key" + "api_key": "API Key", + "longitude": "Longitude", + "latitude": "Latitude" }, "create_entry": { "authenticated": "Successfully authenticated" From 7554c8d6c5fa68c2a8af92d20f93c48be6143cd0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 1 Oct 2020 16:14:29 +0200 Subject: [PATCH 039/831] Add missing unit for signal strength (#40436) * Added missing unit for signal strength * Added one more unit * Replaced string with variable * Fixed wrong import * Fix import * Replaced string with variable * Fixed wrong import * Apply suggestions from code review * Black * Again a fix :-( * iSort * iSort after merge Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/blink/sensor.py | 7 +++- homeassistant/components/canary/sensor.py | 9 ++++- .../components/netgear_lte/sensor_types.py | 10 +++-- homeassistant/components/rfxtrx/__init__.py | 3 +- homeassistant/components/ring/sensor.py | 4 +- homeassistant/components/sms/sensor.py | 4 +- .../components/wirelesstag/__init__.py | 3 +- homeassistant/components/wled/const.py | 1 - homeassistant/components/wled/sensor.py | 5 ++- homeassistant/const.py | 4 ++ tests/components/canary/test_sensor.py | 3 +- tests/components/huawei_lte/test_sensor.py | 10 ++++- tests/components/rfxtrx/test_sensor.py | 37 +++++++++++++++---- tests/components/wled/test_sensor.py | 7 +++- .../custom_components/test/sensor.py | 4 +- 15 files changed, 84 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 35cc2d0d5a5..3c3adf6d990 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity @@ -14,7 +15,11 @@ _LOGGER = logging.getLogger(__name__) SENSORS = { TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE], - TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", DEVICE_CLASS_SIGNAL_STRENGTH], + TYPE_WIFI_STRENGTH: [ + "Wifi Signal", + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + DEVICE_CLASS_SIGNAL_STRENGTH, + ], } diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 6ec9f3a87ff..99dcdf48fce 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity @@ -35,7 +36,13 @@ SENSOR_TYPES = [ ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]], ["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]], ["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]], - ["wifi", "dBm", None, DEVICE_CLASS_SIGNAL_STRENGTH, [CANARY_FLEX]], + [ + "wifi", + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + None, + DEVICE_CLASS_SIGNAL_STRENGTH, + [CANARY_FLEX], + ], ["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]], ] diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py index e354c84e715..644fe35c8c3 100644 --- a/homeassistant/components/netgear_lte/sensor_types.py +++ b/homeassistant/components/netgear_lte/sensor_types.py @@ -1,7 +1,11 @@ """Define possible sensor types.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY -from homeassistant.const import DATA_MEBIBYTES, PERCENTAGE +from homeassistant.const import ( + DATA_MEBIBYTES, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) SENSOR_SMS = "sms" SENSOR_SMS_TOTAL = "sms_total" @@ -12,8 +16,8 @@ SENSOR_UNITS = { SENSOR_SMS_TOTAL: "messages", SENSOR_USAGE: DATA_MEBIBYTES, "radio_quality": PERCENTAGE, - "rx_level": "dBm", - "tx_level": "dBm", + "rx_level": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + "tx_level": SIGNAL_STRENGTH_DECIBELS_MILLIWATT, "upstream": None, "connection_text": None, "connection_type": None, diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c12a6380f20..ccdf79772aa 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, TIME_HOURS, @@ -86,7 +87,7 @@ DATA_TYPES = OrderedDict( ("Voltage", VOLT), ("Current", ELECTRICAL_CURRENT_AMPERE), ("Battery numeric", PERCENTAGE), - ("Rssi numeric", "dBm"), + ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT), ] ) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 24a5cd3b6fb..f59c3b2e61d 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,7 +1,7 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -256,7 +256,7 @@ SENSOR_TYPES = { "wifi_signal_strength": [ "WiFi Signal Strength", ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - "dBm", + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, "wifi", None, "signal_strength", diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index 64d2bf9bd98..eaad395eaa6 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -3,7 +3,7 @@ import logging import gammu # pylint: disable=import-error, no-member -from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH +from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS from homeassistant.helpers.entity import Entity from .const import DOMAIN, SMS_GATEWAY @@ -50,7 +50,7 @@ class GSMSignalSensor(Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return "dB" + return SIGNAL_STRENGTH_DECIBELS @property def device_class(self): diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 702479c4112..ba45844597b 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLT, ) import homeassistant.helpers.config_validation as cv @@ -281,7 +282,7 @@ class WirelessTagBaseSensor(Entity): return { ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100), ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}{VOLT}", - ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm", + ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}{SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}{PERCENTAGE}", } diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 6006952a580..f50e17c01cc 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -26,7 +26,6 @@ ATTR_UDP_PORT = "udp_port" # Units of measurement CURRENT_MA = "mA" -SIGNAL_DBM = "dBm" # Services SERVICE_EFFECT = "effect" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 63a253e6efc..06445d03ba9 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -10,13 +10,14 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TIMESTAMP, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity -from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN, SIGNAL_DBM +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -192,7 +193,7 @@ class WLEDWifiRSSISensor(WLEDSensor): icon="mdi:wifi", key="wifi_rssi", name=f"{coordinator.data.info.name} Wi-Fi RSSI", - unit_of_measurement=SIGNAL_DBM, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) @property diff --git a/homeassistant/const.py b/homeassistant/const.py index f845bc2bd0f..6099a28a7c8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -472,6 +472,10 @@ SPEED_METERS_PER_SECOND = f"{LENGTH_METERS}/{TIME_SECONDS}" SPEED_KILOMETERS_PER_HOUR = f"{LENGTH_KILOMETERS}/{TIME_HOURS}" SPEED_MILES_PER_HOUR = "mph" +# Signal_strength units +SIGNAL_STRENGTH_DECIBELS = "dB" +SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm" + # Data units DATA_BITS = "bit" DATA_KILOBITS = "kbit" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index c17db7b88c9..d32741d3705 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.setup import async_setup_component @@ -187,7 +188,7 @@ async def test_sensors_flex(hass, canary) -> None: "home_dining_room_wifi": ( "20_wifi", "-57.0", - "dBm", + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, DEVICE_CLASS_SIGNAL_STRENGTH, None, ), diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py index f9834349750..92bb8cbac5e 100644 --- a/tests/components/huawei_lte/test_sensor.py +++ b/tests/components/huawei_lte/test_sensor.py @@ -3,11 +3,19 @@ import pytest from homeassistant.components.huawei_lte import sensor +from homeassistant.const import ( + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) @pytest.mark.parametrize( ("value", "expected"), - (("-71 dBm", (-71, "dBm")), ("15dB", (15, "dB")), (">=-51dBm", (-51, "dBm"))), + ( + ("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), + ("15dB", (15, SIGNAL_STRENGTH_DECIBELS)), + (">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), + ), ) def test_format_default(value, expected): """Test that default formatter copes with expected values.""" diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index c1bb3222b79..77f9960de49 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -3,7 +3,12 @@ import pytest from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import ATTR_EVENT -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache @@ -100,7 +105,10 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) state = hass.states.get(f"{base_id}_battery_numeric") assert state @@ -174,7 +182,10 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) state = hass.states.get(f"{base_id}_temperature") assert state @@ -203,7 +214,10 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) state = hass.states.get(f"{base_id}_temperature") assert state @@ -232,7 +246,10 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) state = hass.states.get(f"{base_id}_temperature") assert state @@ -315,13 +332,19 @@ async def test_rssi_sensor(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 39cde51dbfd..d5c1c738d2f 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.wled.const import ( ATTR_MAX_POWER, CURRENT_MA, DOMAIN, - SIGNAL_DBM, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -20,6 +19,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -138,7 +138,10 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_DBM + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) assert state.state == "-62" entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi") diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index d9ed47844af..d467a93fd62 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -4,7 +4,7 @@ Provide a mock sensor platform. Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor -from homeassistant.const import PERCENTAGE, PRESSURE_HPA +from homeassistant.const import PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS from tests.common import MockEntity @@ -15,7 +15,7 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_BATTERY: PERCENTAGE, # % of battery that is left sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) - sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS, # signal strength (dB/dBm) sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) sensor.DEVICE_CLASS_PRESSURE: PRESSURE_HPA, # pressure (hPa/mbar) From 6627ffff39276d32d241542af3cf3dbffcc28d3c Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 1 Oct 2020 10:15:24 -0400 Subject: [PATCH 040/831] Clean up goalzero (#40817) * cleanup goalzero code * more cleanup * mroe cleanup * log defined exception to error * return None if not configured * return False if not configured --- homeassistant/components/goalzero/__init__.py | 34 ++----------------- .../components/goalzero/binary_sensor.py | 4 +-- .../components/goalzero/config_flow.py | 22 ++++++------ .../components/goalzero/strings.json | 2 +- .../components/goalzero/translations/en.json | 2 +- tests/components/goalzero/test_config_flow.py | 5 --- 6 files changed, 19 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 892ee46982d..ff60a9ac043 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -3,13 +3,11 @@ import asyncio import logging from goalzero import Yeti, exceptions -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -17,33 +15,10 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DEFAULT_NAME, - DOMAIN, - MIN_TIME_BETWEEN_UPDATES, -) +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN, MIN_TIME_BETWEEN_UPDATES _LOGGER = logging.getLogger(__name__) -GOALZERO_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.matches_regex( - r"\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2 \ - [0-4][0-9]|[01]?[0-9][0-9]?)\Z" - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - }, - ) -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [GOALZERO_SCHEMA]))}, - extra=vol.ALLOW_EXTRA, -) - PLATFORMS = ["binary_sensor"] @@ -61,8 +36,6 @@ async def async_setup_entry(hass, entry): name = entry.data[CONF_NAME] host = entry.data[CONF_HOST] - _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - session = async_get_clientsession(hass) api = Yeti(host, hass.loop, session) try: @@ -76,7 +49,6 @@ async def async_setup_entry(hass, entry): try: await api.get_state() except exceptions.ConnectError as err: - _LOGGER.warning("Failed to update data from Yeti") raise UpdateFailed(f"Failed to communicating with API: {err}") from err coordinator = DataUpdateCoordinator( @@ -117,10 +89,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class YetiEntity(CoordinatorEntity): """Representation of a Goal Zero Yeti entity.""" - def __init__(self, _api, coordinator, name, sensor_name, server_unique_id): + def __init__(self, api, coordinator, name, server_unique_id): """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) - self.api = _api + self.api = api self._name = name self._server_unique_id = server_unique_id self._device_class = None diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 25b370c459f..a2af8a18546 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -30,14 +30,13 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): def __init__(self, api, coordinator, name, sensor_name, server_unique_id): """Initialize a Goal Zero Yeti sensor.""" - super().__init__(api, coordinator, name, sensor_name, server_unique_id) + super().__init__(api, coordinator, name, server_unique_id) self._condition = sensor_name variable_info = BINARY_SENSOR_DICT[sensor_name] self._condition_name = variable_info[0] self._icon = variable_info[2] - self.api = api self._device_class = variable_info[1] @property @@ -55,6 +54,7 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): """Return if the service is on.""" if self.api.data: return self.api.data[self._condition] == 1 + return False @property def icon(self): diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 31c1a51efeb..35b6953865c 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -34,19 +34,20 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await self._async_try_connect(host) + except exceptions.ConnectError: + errors["base"] = "cannot_connect" + _LOGGER.error("Error connecting to device at %s", host) + except exceptions.InvalidHost: + errors["base"] = "invalid_host" + _LOGGER.error("Invalid host at %s", host) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: return self.async_create_entry( title=name, data={CONF_HOST: host, CONF_NAME: name}, ) - except exceptions.ConnectError: - errors["base"] = "cannot_connect" - _LOGGER.exception("Error connecting to device at %s", host) - except exceptions.InvalidHost: - errors["base"] = "invalid_host" - _LOGGER.exception("Invalid data received from device at %s", host) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" user_input = user_input or {} return self.async_show_form( @@ -67,7 +68,8 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_endpoint_existed(self, endpoint): for entry in self._async_current_entries(): if endpoint == entry.data.get(CONF_HOST): - return endpoint + return True + return False async def _async_try_connect(self, host): session = async_get_clientsession(self.hass) diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index e7a134c01ec..5b8bed63e6a 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -12,7 +12,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_host": "This is not the Yeti you are looking for", + "invalid_host": "Invalid host provided", "unknown": "Unknown Error" }, "abort": { diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 98cfa4f6f33..4aba53955e7 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_host": "This is not the Yeti you are looking for", + "invalid_host": "Invalid host provided", "unknown": "Unknown Error" }, "step": { diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 5a367c452c6..906a84d7882 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -46,11 +46,6 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - _flow_next(hass, result["flow_id"]) - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_CONFIG_FLOW, From 2a05fe7270709adce60cdcf9d854c8a21cd36e24 Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 1 Oct 2020 16:31:34 +0200 Subject: [PATCH 041/831] Replace IP Address in strings.json (#40968) --- homeassistant/components/denonavr/strings.json | 2 +- homeassistant/components/gogogate2/strings.json | 2 +- homeassistant/components/guardian/strings.json | 2 +- homeassistant/components/powerwall/strings.json | 2 +- homeassistant/components/ps4/strings.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index 25648d974ab..2ebdd2afb3f 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -6,7 +6,7 @@ "title": "Denon AVR Network Receivers", "description": "Connect to your receiver, if the IP address is not set, auto-discovery is used", "data": { - "host": "IP address" + "host": "[%key:common::config_flow::data::ip%]" } }, "confirm": { diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json index 47a53d0d320..f5385ff5d54 100644 --- a/homeassistant/components/gogogate2/strings.json +++ b/homeassistant/components/gogogate2/strings.json @@ -12,7 +12,7 @@ "title": "Setup GogoGate2 or iSmartGate", "description": "Provide requisite information below.", "data": { - "ip_address": "IP Address", + "ip_address": "[%key:common::config_flow::data::ip%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 3f87d3260f4..baa1bf6aa7d 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -5,7 +5,7 @@ "user": { "description": "Configure a local Elexa Guardian device.", "data": { - "ip_address": "IP Address", + "ip_address": "[%key:common::config_flow::data::ip%]", "port": "Port" } }, diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index ce7e6a1965f..2ec174a4b18 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Connect to the powerwall", - "data": { "ip_address": "IP Address" } + "data": { "ip_address": "[%key:common::config_flow::data::ip%]" } } }, "error": { diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index c3a864565cf..a44dfb0c9a9 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -20,7 +20,7 @@ "region": "Region", "name": "Name", "code": "PIN", - "ip_address": "IP Address" + "ip_address": "[%key:common::config_flow::data::ip%]" } } }, From b56ec71998f9b2bddda182179d8af0c114d1a131 Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 1 Oct 2020 17:08:19 +0200 Subject: [PATCH 042/831] Replace strings in hvv_departures with references (#40980) --- homeassistant/components/hvv_departures/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hvv_departures/strings.json b/homeassistant/components/hvv_departures/strings.json index dfd6484f7d8..c29ad6cc694 100644 --- a/homeassistant/components/hvv_departures/strings.json +++ b/homeassistant/components/hvv_departures/strings.json @@ -5,9 +5,9 @@ "user": { "title": "Connect to the HVV API", "data": { - "host": "Host", - "username": "Username", - "password": "Password" + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "station": { @@ -24,12 +24,12 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "no_results": "No results. Try with a different station/address" }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { From 639c864a769fdb57897504c8e5d4429ab669ee1c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 1 Oct 2020 18:00:26 +0200 Subject: [PATCH 043/831] Add test coverage for modbus switch (coil part) (#40696) * Prepare test code for complex devices. push entity_id to conftest, to make it common for all devices. Add device to base_setup. * Add test coverage for modbus switch (coil part). * Update .coveragerc * Update .coveragerc Co-authored-by: Chris Talkington --- tests/components/modbus/conftest.py | 43 +- .../modbus/test_modbus_binary_sensor.py | 32 +- tests/components/modbus/test_modbus_sensor.py | 386 +++++++++--------- tests/components/modbus/test_modbus_switch.py | 59 +++ 4 files changed, 289 insertions(+), 231 deletions(-) create mode 100644 tests/components/modbus/test_modbus_switch.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 66f4d542525..36495acc9c5 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,5 +1,4 @@ """The tests for the Modbus sensor component.""" -from datetime import timedelta import logging from unittest import mock @@ -40,20 +39,17 @@ class ReadResult: self.bits = register_words -async def run_base_test( +async def setup_base_test( sensor_name, hass, use_mock_hub, data_array, - register_type, entity_domain, - register_words, - expected, + scan_interval, ): - """Run test for given config.""" + """Run setup device for given config.""" # Full sensor configuration - scan_interval = 5 config = { entity_domain: { CONF_PLATFORM: "modbus", @@ -62,6 +58,28 @@ async def run_base_test( } } + # Initialize sensor + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, entity_domain, config) + await hass.async_block_till_done() + + entity_id = f"{entity_domain}.{sensor_name}" + device = hass.states.get(entity_id) + return entity_id, now, device + + +async def run_base_read_test( + entity_id, + hass, + use_mock_hub, + register_type, + register_words, + expected, + now, +): + """Run test for given config.""" + # Setup inputs for the sensor read_result = ReadResult(register_words) if register_type == CALL_TYPE_COIL: @@ -73,14 +91,11 @@ async def run_base_test( else: # CALL_TYPE_REGISTER_HOLDING use_mock_hub.read_holding_registers.return_value = read_result - # Initialize sensor - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, entity_domain, config) - await hass.async_block_till_done() - # Trigger update call with time_changed event - now += timedelta(seconds=scan_interval + 1) with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + + # Check state + state = hass.states.get(entity_id).state + assert state == expected diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py index ff64ad8723a..63513872e79 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +from datetime import timedelta import logging from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN @@ -11,7 +12,7 @@ from homeassistant.components.modbus.const import ( ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON -from .conftest import run_base_test +from .conftest import run_base_read_test, setup_base_test _LOGGER = logging.getLogger(__name__) @@ -19,28 +20,29 @@ _LOGGER = logging.getLogger(__name__) async def run_sensor_test(hass, use_mock_hub, register_config, value, expected): """Run test for given config.""" sensor_name = "modbus_test_binary_sensor" - entity_domain = SENSOR_DOMAIN - data_array = { - CONF_INPUTS: [ - dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **register_config) - ] - } - await run_base_test( + scan_interval = 5 + entity_id, now, device = await setup_base_test( sensor_name, hass, use_mock_hub, - data_array, + { + CONF_INPUTS: [ + dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **register_config) + ] + }, + SENSOR_DOMAIN, + scan_interval, + ) + await run_base_read_test( + entity_id, + hass, + use_mock_hub, register_config.get(CONF_INPUT_TYPE), - entity_domain, value, expected, + now + timedelta(seconds=scan_interval + 1), ) - # Check state - entity_id = f"{entity_domain}.{sensor_name}" - state = hass.states.get(entity_id).state - assert state == expected - async def test_coil_true(hass, mock_hub): """Test conversion of single word register.""" diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 5ade2f197dd..59d845ef053 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +from datetime import timedelta import logging from homeassistant.components.modbus.const import ( @@ -21,7 +22,7 @@ from homeassistant.components.modbus.const import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME -from .conftest import run_base_test +from .conftest import run_base_read_test, setup_base_test _LOGGER = logging.getLogger(__name__) @@ -31,363 +32,344 @@ async def run_sensor_test( ): """Run test for sensor.""" sensor_name = "modbus_test_sensor" - entity_domain = SENSOR_DOMAIN - data_array = { - CONF_REGISTERS: [ - dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) - ] - } - - await run_base_test( + scan_interval = 5 + entity_id, now, device = await setup_base_test( sensor_name, hass, use_mock_hub, - data_array, + { + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ] + }, + SENSOR_DOMAIN, + scan_interval, + ) + await run_base_read_test( + entity_id, + hass, + use_mock_hub, register_config.get(CONF_REGISTER_TYPE), - entity_domain, register_words, expected, + now + timedelta(seconds=scan_interval + 1), ) - # Check state - entity_id = f"{entity_domain}.{sensor_name}" - state = hass.states.get(entity_id).state - assert state == expected - async def test_simple_word_register(hass, mock_hub): """Test conversion of single word register.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0], - expected="0", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0], + "0", ) async def test_optional_conf_keys(hass, mock_hub): """Test handling of optional configuration keys.""" - register_config = {} await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x8000], - expected="-32768", + {}, + [0x8000], + "-32768", ) async def test_offset(hass, mock_hub): """Test offset calculation.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 1, - CONF_OFFSET: 13, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[7], - expected="20", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + }, + [7], + "20", ) async def test_scale_and_offset(hass, mock_hub): """Test handling of scale and offset.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 3, - CONF_OFFSET: 13, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[7], - expected="34", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + }, + [7], + "34", ) async def test_ints_can_have_precision(hass, mock_hub): """Test precision can be specified event if using integer values only.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 3, - CONF_OFFSET: 13, - CONF_PRECISION: 4, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[7], - expected="34.0000", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 4, + }, + [7], + "34.0000", ) async def test_floats_get_rounded_correctly(hass, mock_hub): """Test that floating point values get rounded correctly.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 1.5, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[1], - expected="2", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1.5, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [1], + "2", ) async def test_parameters_as_strings(hass, mock_hub): """Test that scale, offset and precision can be given as strings.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: "1.5", - CONF_OFFSET: "5", - CONF_PRECISION: "1", - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[9], - expected="18.5", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: "1.5", + CONF_OFFSET: "5", + CONF_PRECISION: "1", + }, + [9], + "18.5", ) async def test_floating_point_scale(hass, mock_hub): """Test use of floating point scale.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 2.4, - CONF_OFFSET: 0, - CONF_PRECISION: 2, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[1], - expected="2.40", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 2.4, + CONF_OFFSET: 0, + CONF_PRECISION: 2, + }, + [1], + "2.40", ) async def test_floating_point_offset(hass, mock_hub): """Test use of floating point scale.""" - register_config = { - CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 1, - CONF_OFFSET: -10.3, - CONF_PRECISION: 1, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[2], - expected="-8.3", + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: -10.3, + CONF_PRECISION: 1, + }, + [2], + "-8.3", ) async def test_signed_two_word_register(hass, mock_hub): """Test reading of signed register with two words.""" - register_config = { - CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF], - expected="-1985229329", + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x89AB, 0xCDEF], + "-1985229329", ) async def test_unsigned_two_word_register(hass, mock_hub): """Test reading of unsigned register with two words.""" - register_config = { - CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF], - expected=str(0x89ABCDEF), + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x89AB, 0xCDEF], + str(0x89ABCDEF), ) async def test_reversed(hass, mock_hub): """Test handling of reversed register words.""" - register_config = { - CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_REVERSE_ORDER: True, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF], - expected=str(0xCDEF89AB), + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_REVERSE_ORDER: True, + }, + [0x89AB, 0xCDEF], + str(0xCDEF89AB), ) async def test_four_word_register(hass, mock_hub): """Test reading of 64-bit register.""" - register_config = { - CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], - expected="9920249030613615975", + { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x89AB, 0xCDEF, 0x0123, 0x4567], + "9920249030613615975", ) async def test_four_word_register_precision_is_intact_with_int_params(hass, mock_hub): """Test that precision is not lost when doing integer arithmetic for 64-bit register.""" - register_config = { - CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 2, - CONF_OFFSET: 3, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], - expected="163971058432973793", + { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2, + CONF_OFFSET: 3, + CONF_PRECISION: 0, + }, + [0x0123, 0x4567, 0x89AB, 0xCDEF], + "163971058432973793", ) async def test_four_word_register_precision_is_lost_with_float_params(hass, mock_hub): """Test that precision is affected when floating point conversion is done.""" - register_config = { - CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 2.0, - CONF_OFFSET: 3.0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], - expected="163971058432973792", + { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2.0, + CONF_OFFSET: 3.0, + CONF_PRECISION: 0, + }, + [0x0123, 0x4567, 0x89AB, 0xCDEF], + "163971058432973792", ) async def test_two_word_input_register(hass, mock_hub): """Test reaging of input register.""" - register_config = { - CONF_COUNT: 2, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF], - expected=str(0x89ABCDEF), + { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x89AB, 0xCDEF], + str(0x89ABCDEF), ) async def test_two_word_holding_register(hass, mock_hub): """Test reaging of holding register.""" - register_config = { - CONF_COUNT: 2, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x89AB, 0xCDEF], - expected=str(0x89ABCDEF), + { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x89AB, 0xCDEF], + str(0x89ABCDEF), ) async def test_float_data_type(hass, mock_hub): """Test floating point register data type.""" - register_config = { - CONF_COUNT: 2, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_FLOAT, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 5, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[16286, 1617], - expected="1.23457", + { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_FLOAT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 5, + }, + [16286, 1617], + "1.23457", ) async def test_string_data_type(hass, mock_hub): """Test byte string register data type.""" - register_config = { - CONF_COUNT: 8, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_STRING, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, - } await run_sensor_test( hass, mock_hub, - register_config, - register_words=[0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335], - expected="07-05-2020 14:35", + { + CONF_COUNT: 8, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335], + "07-05-2020 14:35", ) diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py new file mode 100644 index 00000000000..ac1d8bd6963 --- /dev/null +++ b/tests/components/modbus/test_modbus_switch.py @@ -0,0 +1,59 @@ +"""The tests for the Modbus switch component.""" +from datetime import timedelta +import logging + +from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_COILS +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_NAME, CONF_SLAVE + +from .conftest import run_base_read_test, setup_base_test + +_LOGGER = logging.getLogger(__name__) + + +async def run_sensor_test(hass, use_mock_hub, value, expected): + """Run test for given config.""" + switch_name = "modbus_test_switch" + scan_interval = 5 + entity_id, now, device = await setup_base_test( + switch_name, + hass, + use_mock_hub, + { + CONF_COILS: [ + {CONF_NAME: switch_name, CALL_TYPE_COIL: 1234, CONF_SLAVE: 1}, + ] + }, + SWITCH_DOMAIN, + scan_interval, + ) + + await run_base_read_test( + entity_id, + hass, + use_mock_hub, + CALL_TYPE_COIL, + value, + expected, + now + timedelta(seconds=scan_interval + 1), + ) + + +async def test_read_coil_false(hass, mock_hub): + """Test reading of switch coil.""" + await run_sensor_test( + hass, + mock_hub, + [0x00], + expected="off", + ) + + +async def test_read_coil_true(hass, mock_hub): + """Test reading of switch coil.""" + await run_sensor_test( + hass, + mock_hub, + [0xFF], + expected="on", + ) From 40ea30da96244fa5266ac4fcd09c2f3cc89f72b9 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Thu, 1 Oct 2020 12:19:34 -0400 Subject: [PATCH 044/831] Fix Rachio switch state when paused (#40984) --- homeassistant/components/rachio/switch.py | 7 ++++++- homeassistant/components/rachio/webhooks.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 32ec6267941..b5dc71b585c 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -56,6 +56,7 @@ from .webhooks import ( SUBTYPE_SLEEP_MODE_OFF, SUBTYPE_SLEEP_MODE_ON, SUBTYPE_ZONE_COMPLETED, + SUBTYPE_ZONE_PAUSED, SUBTYPE_ZONE_STARTED, SUBTYPE_ZONE_STOPPED, ) @@ -392,7 +393,11 @@ class RachioZone(RachioSwitch): if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED: self._state = True - elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, SUBTYPE_ZONE_COMPLETED]: + elif args[0][KEY_SUBTYPE] in [ + SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED, + SUBTYPE_ZONE_PAUSED, + ]: self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 5daf7852725..c175117efcb 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -58,6 +58,7 @@ SUBTYPE_ZONE_STOPPED = "ZONE_STOPPED" SUBTYPE_ZONE_COMPLETED = "ZONE_COMPLETED" SUBTYPE_ZONE_CYCLING = "ZONE_CYCLING" SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED" +SUBTYPE_ZONE_PAUSED = "ZONE_PAUSED" # Webhook callbacks LISTEN_EVENT_TYPES = [ From c2ed743237b3a4adf4cc2aaeeff4f4285a9c9462 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 1 Oct 2020 19:06:20 +0200 Subject: [PATCH 045/831] Improve deCONZ platforms (#40986) * Use platform domain imports with the list of supported platforms * Remove legacy async_setup_platform from platforms --- .../components/deconz/binary_sensor.py | 4 --- homeassistant/components/deconz/climate.py | 4 --- homeassistant/components/deconz/const.py | 25 +++++++++++++------ homeassistant/components/deconz/cover.py | 4 --- homeassistant/components/deconz/light.py | 4 --- homeassistant/components/deconz/scene.py | 4 --- homeassistant/components/deconz/sensor.py | 4 --- homeassistant/components/deconz/switch.py | 4 --- 8 files changed, 17 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index f7a9c1a5217..5dc11d9b580 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -33,10 +33,6 @@ DEVICE_CLASS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 10ea7173f8a..7cfb716b586 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -19,10 +19,6 @@ from .gateway import get_gateway_from_config_entry SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index f60c4c35646..d965b6485f8 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,6 +1,15 @@ """Constants for the deCONZ component.""" import logging +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN + LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" @@ -19,14 +28,14 @@ CONF_ALLOW_NEW_DEVICES = "allow_new_devices" CONF_MASTER_GATEWAY = "master" SUPPORTED_PLATFORMS = [ - "binary_sensor", - "climate", - "cover", - "light", - "lock", - "scene", - "sensor", - "switch", + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + LIGHT_DOMAIN, + LOCK_DOMAIN, + SCENE_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, ] NEW_GROUP = "groups" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 7bcd821a344..d1cd48c47cc 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -17,10 +17,6 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up covers for deCONZ component. diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 544699970f2..cf7007109b3 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -35,10 +35,6 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index fdeb1d43acc..2ce3da6c234 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,10 +9,6 @@ from .const import NEW_SCENE from .gateway import get_gateway_from_config_entry -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up scenes for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 08e81d2dd3b..32dc0ee7ea3 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -68,10 +68,6 @@ UNIT_OF_MEASUREMENT = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index dacae4d4a56..af543348a4d 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -8,10 +8,6 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up deCONZ platforms.""" - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for deCONZ component. From cf785b86db8ef61549f14ba51a291abfd36c6b89 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 1 Oct 2020 12:55:11 -0500 Subject: [PATCH 046/831] Minor fixes for Plex media browser (#39878) --- homeassistant/components/plex/media_browser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 3c91362deb9..56e6f68a968 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -103,7 +103,7 @@ def browse_media( children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] except KeyError as err: raise BrowseError( - f"Media not found: {media_content_type} / {media_content_id}" + f"Unknown type received: {library_or_section.TYPE}" ) from err else: raise BrowseError( @@ -218,9 +218,9 @@ def server_payload(plex_server): media_content_type="server", can_play=False, can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, ) server_info.children = [] - server_info.children_media_class = MEDIA_CLASS_DIRECTORY server_info.children.append(special_library_payload(server_info, "On Deck")) server_info.children.append(special_library_payload(server_info, "Recently Added")) for library in plex_server.library.sections(): From 6b509fd9dbaa96f5921c9e020956b5478866ea19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Oct 2020 12:57:52 -0500 Subject: [PATCH 047/831] Prevent sqlalchemy from refetching the old_state_id as it will never change (#40982) Disable expire_on_commit for the event writer. Since we never expect the old_state_id to change in the database, it was never worth the expense of refetching the id after the commit. --- homeassistant/components/recorder/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3bf95b4303a..02a07bab628 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -573,7 +573,9 @@ class Recorder(threading.Thread): sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection) Base.metadata.create_all(self.engine) - self.get_session = scoped_session(sessionmaker(bind=self.engine)) + self.get_session = scoped_session( + sessionmaker(bind=self.engine, expire_on_commit=False) + ) def _close_connection(self): """Close the connection.""" From 4b8f91823c49004b0a764cabad42abe76319c2ae Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 1 Oct 2020 20:12:44 +0200 Subject: [PATCH 048/831] Use reference strings in elkm1 strings.json (#40996) --- homeassistant/components/elkm1/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index 223e2ca3fff..bf0da956d44 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -15,9 +15,9 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "An ElkM1 with this prefix is already configured", From 17c6838b7fc64a76bbdb6ec918f7b536d6e31118 Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 1 Oct 2020 20:13:50 +0200 Subject: [PATCH 049/831] Replace references in august strings.json (#40993) --- homeassistant/components/august/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 254e8146984..024b633e456 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -1,12 +1,12 @@ { "config": { "error": { - "unknown": "Unexpected error", - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication" + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "Account is already configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "Re-authentication was successful" }, "step": { From c7ebfdb403bebb4470e6c07f919f978718c92e51 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 1 Oct 2020 20:44:13 +0200 Subject: [PATCH 050/831] Use single_instance_allowed for webhook config flows (#40965) --- homeassistant/components/dialogflow/strings.json | 2 +- homeassistant/components/geofency/strings.json | 2 +- homeassistant/components/gpslogger/strings.json | 2 +- homeassistant/components/ifttt/strings.json | 2 +- homeassistant/components/islamic_prayer_times/config_flow.py | 2 +- homeassistant/components/islamic_prayer_times/strings.json | 2 +- homeassistant/components/locative/strings.json | 2 +- homeassistant/components/mailgun/strings.json | 2 +- homeassistant/components/owntracks/config_flow.py | 4 ++-- homeassistant/components/owntracks/strings.json | 4 +++- homeassistant/components/ozw/config_flow.py | 2 +- homeassistant/components/ozw/strings.json | 2 +- homeassistant/components/plaato/strings.json | 2 +- homeassistant/components/speedtestdotnet/config_flow.py | 2 +- homeassistant/components/speedtestdotnet/strings.json | 2 +- homeassistant/components/traccar/strings.json | 2 +- homeassistant/components/twilio/strings.json | 2 +- homeassistant/helpers/config_entry_flow.py | 2 +- tests/components/islamic_prayer_times/test_config_flow.py | 2 +- tests/components/owntracks/test_config_flow.py | 4 ++-- tests/components/ozw/test_config_flow.py | 2 +- tests/components/speedtestdotnet/test_config_flow.py | 2 +- tests/helpers/test_config_entry_flow.py | 2 +- 23 files changed, 27 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/dialogflow/strings.json b/homeassistant/components/dialogflow/strings.json index d1a691dc92b..f17491a7528 100644 --- a/homeassistant/components/dialogflow/strings.json +++ b/homeassistant/components/dialogflow/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages." }, "create_entry": { diff --git a/homeassistant/components/geofency/strings.json b/homeassistant/components/geofency/strings.json index 1c6a72f27c8..a7b6649fb6e 100644 --- a/homeassistant/components/geofency/strings.json +++ b/homeassistant/components/geofency/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." }, "create_entry": { diff --git a/homeassistant/components/gpslogger/strings.json b/homeassistant/components/gpslogger/strings.json index f3d4344cd49..b757e5c46cb 100644 --- a/homeassistant/components/gpslogger/strings.json +++ b/homeassistant/components/gpslogger/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from GPSLogger." }, "create_entry": { diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json index b637e0de13d..9002cf30756 100644 --- a/homeassistant/components/ifttt/strings.json +++ b/homeassistant/components/ifttt/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages." }, "create_entry": { diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index d45997af76f..065af0bd611 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -23,7 +23,7 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") if user_input is None: return self.async_show_form(step_id="user") diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index b57c338ecea..73998913f41 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -8,7 +8,7 @@ } }, "abort": { - "one_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "options": { diff --git a/homeassistant/components/locative/strings.json b/homeassistant/components/locative/strings.json index 53a0c160e99..3a5821f40b1 100644 --- a/homeassistant/components/locative/strings.json +++ b/homeassistant/components/locative/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency." }, "create_entry": { diff --git a/homeassistant/components/mailgun/strings.json b/homeassistant/components/mailgun/strings.json index 29ea3c0b952..a948c6165e7 100644 --- a/homeassistant/components/mailgun/strings.json +++ b/homeassistant/components/mailgun/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages." }, "create_entry": { diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 0aba24217cc..f0838b510ec 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -19,7 +19,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a user initiated set up flow to create OwnTracks webhook.""" if self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") if user_input is None: return self.async_show_form(step_id="user") @@ -52,7 +52,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Import a config flow from configuration.""" if self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() secret = secrets.token_hex(16) return self.async_create_entry( diff --git a/homeassistant/components/owntracks/strings.json b/homeassistant/components/owntracks/strings.json index 12aba21be72..ddb700cc642 100644 --- a/homeassistant/components/owntracks/strings.json +++ b/homeassistant/components/owntracks/strings.json @@ -6,7 +6,9 @@ "description": "Are you sure you want to set up OwnTracks?" } }, - "abort": { "one_instance_allowed": "Only a single instance is necessary." }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, "create_entry": { "default": "\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information." } diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 8822490132c..3153324322e 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -15,7 +15,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" if self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") if "mqtt" not in self.hass.config.components: return self.async_abort(reason="mqtt_required") if user_input is not None: diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json index dd2aad7e4ce..88f8911db0d 100644 --- a/homeassistant/components/ozw/strings.json +++ b/homeassistant/components/ozw/strings.json @@ -6,7 +6,7 @@ } }, "abort": { - "one_instance_allowed": "The integration only supports one Z-Wave instance", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "mqtt_required": "The MQTT integration is not set up" } } diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index f78943ca941..e03d2508037 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Plaato Airlock." }, "create_entry": { diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 57076c2a90b..2bc462afdb0 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -36,7 +36,7 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") if user_input is None: return self.async_show_form(step_id="user") diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json index f638c25a549..c4b92b16ff3 100644 --- a/homeassistant/components/speedtestdotnet/strings.json +++ b/homeassistant/components/speedtestdotnet/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "wrong_server_id": "Server id is not valid" } }, diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json index 8574f4f34f1..aef269defcb 100644 --- a/homeassistant/components/traccar/strings.json +++ b/homeassistant/components/traccar/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar." }, "create_entry": { diff --git a/homeassistant/components/twilio/strings.json b/homeassistant/components/twilio/strings.json index 96e0249df9a..0480fdae7c8 100644 --- a/homeassistant/components/twilio/strings.json +++ b/homeassistant/components/twilio/strings.json @@ -7,7 +7,7 @@ } }, "abort": { - "one_instance_allowed": "Only a single instance is necessary.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Twilio messages." }, "create_entry": { diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 2b967286b95..f957d884d8d 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -136,7 +136,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow): ) -> Dict[str, Any]: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): - return self.async_abort(reason="one_instance_allowed") + return self.async_abort(reason="single_instance_allowed") if user_input is None: return self.async_show_form(step_id="user") diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 4ffd6e4f25d..3b966c9e861 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -83,4 +83,4 @@ async def test_integration_already_configured(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 396870c2975..2290e3e17a4 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -111,12 +111,12 @@ async def test_abort_if_already_setup(hass): # Should fail, already setup (import) result = await flow.async_step_import({}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" # Should fail, already setup (flow) result = await flow.async_step_user({}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" async def test_user_not_supports_encryption(hass, not_supports_encryption): diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 3446bbfc7de..1af6787a952 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -51,4 +51,4 @@ async def test_one_instance_allowed(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "abort" - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 8e7edc2d986..d421113a2d7 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -135,4 +135,4 @@ async def test_integration_already_configured(hass): speedtestdotnet.DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index adbcca63990..2bb993f1197 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -238,7 +238,7 @@ async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "single_instance_allowed" async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf): From b3464c508730d43f14bfd07b24ea5853d5424283 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 1 Oct 2020 14:38:57 -0500 Subject: [PATCH 051/831] Remove unnecessary instance attribute in Plex reauth config flow (#41000) * Avoid unnecessary instance attribute * Don't need to enrich existing entry data --- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plex/config_flow.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 95fb6e4f09f..3682f362700 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass, entry): hass.config_entries.flow.async_init( PLEX_DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, - data={**entry.data, "config_entry_id": entry.entry_id}, + data=entry.data, ) ) _LOGGER.error( diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index b2bf856402e..bdbdc9c6cc9 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -96,7 +96,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.token = None self.client_id = None self._manual = False - self._entry_id = None async def async_step_user( self, user_input=None, errors=None @@ -230,12 +229,11 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): PLEX_SERVER_CONFIG: entry_config, } - await self.async_set_unique_id(server_id) + entry = await self.async_set_unique_id(server_id) if ( self.context[CONF_SOURCE] # pylint: disable=no-member == config_entries.SOURCE_REAUTH ): - entry = self.hass.config_entries.async_get_entry(self._entry_id) self.hass.config_entries.async_update_entry(entry, data=data) _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name) await self.hass.config_entries.async_reload(entry.entry_id) @@ -329,7 +327,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, data): """Handle a reauthorization flow request.""" self.current_login = dict(data) - self._entry_id = self.current_login.pop("config_entry_id") return await self.async_step_user() From b45215f1d29c5971bbc8d229bddbb710480b23a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Oct 2020 14:39:44 -0500 Subject: [PATCH 052/831] Implement template rate_limit directive (#40667) --- homeassistant/const.py | 4 + homeassistant/core.py | 2 +- homeassistant/helpers/event.py | 24 +- homeassistant/helpers/ratelimit.py | 97 +++++++ homeassistant/helpers/template.py | 44 ++- tests/components/template/test_cover.py | 1 + tests/components/template/test_sensor.py | 16 +- tests/helpers/test_event.py | 341 ++++++++++++++++++++++- tests/helpers/test_ratelimit.py | 108 +++++++ tests/helpers/test_template.py | 50 +++- 10 files changed, 669 insertions(+), 18 deletions(-) create mode 100644 homeassistant/helpers/ratelimit.py create mode 100644 tests/helpers/test_ratelimit.py diff --git a/homeassistant/const.py b/homeassistant/const.py index 6099a28a7c8..9fac02a60b6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -627,3 +627,7 @@ CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"] # The ID of the Home Assistant Cast App CAST_APP_ID_HOMEASSISTANT = "B12CE3CA" + +# The tracker error allow when converting +# loop time to human readable time +MAX_TIME_TRACKING_ERROR = 0.001 diff --git a/homeassistant/core.py b/homeassistant/core.py index eb584b22b49..82fbe1be2b6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -538,7 +538,7 @@ class Event: event_type: str, data: Optional[Dict[str, Any]] = None, origin: EventOrigin = EventOrigin.local, - time_fired: Optional[int] = None, + time_fired: Optional[datetime.datetime] = None, context: Optional[Context] = None, ) -> None: """Initialize a new event.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b396ebb1d91..52a43fca3ff 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -27,6 +27,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, + MAX_TIME_TRACKING_ERROR, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) @@ -40,6 +41,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.ratelimit import KeyedRateLimit from homeassistant.helpers.sun import get_astral_event_next from homeassistant.helpers.template import RenderInfo, Template, result_as_boolean from homeassistant.helpers.typing import TemplateVarsType @@ -47,8 +49,6 @@ from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe -MAX_TIME_TRACKING_ERROR = 0.001 - TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" @@ -88,10 +88,12 @@ class TrackTemplate: The template is template to calculate. The variables are variables to pass to the template. + The rate_limit is a rate limit on how often the template is re-rendered. """ template: Template variables: TemplateVarsType + rate_limit: Optional[timedelta] = None @dataclass @@ -724,6 +726,8 @@ class _TrackTemplateResultInfo: self._track_templates = track_templates self._last_result: Dict[Template, Union[str, TemplateError]] = {} + + self._rate_limit = KeyedRateLimit(hass) self._info: Dict[Template, RenderInfo] = {} self._track_state_changes: Optional[_TrackStateChangeFiltered] = None @@ -763,6 +767,7 @@ class _TrackTemplateResultInfo: """Cancel the listener.""" assert self._track_state_changes self._track_state_changes.async_remove() + self._rate_limit.async_remove() @callback def async_refresh(self) -> None: @@ -784,11 +789,23 @@ class _TrackTemplateResultInfo: def _refresh(self, event: Optional[Event]) -> None: updates = [] info_changed = False + now = dt_util.utcnow() for track_template_ in self._track_templates: template = track_template_.template if event: - if not self._event_triggers_template(template, event): + if not self._rate_limit.async_has_timer( + template + ) and not self._event_triggers_template(template, event): + continue + + if self._rate_limit.async_schedule_action( + template, + self._info[template].rate_limit or track_template_.rate_limit, + now, + self._refresh, + event, + ): continue _LOGGER.debug( @@ -797,6 +814,7 @@ class _TrackTemplateResultInfo: event, ) + self._rate_limit.async_triggered(template, now) self._info[template] = template.async_render_to_info( track_template_.variables ) diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py new file mode 100644 index 00000000000..422ebdf2eee --- /dev/null +++ b/homeassistant/helpers/ratelimit.py @@ -0,0 +1,97 @@ +"""Ratelimit helper.""" +import asyncio +from datetime import datetime, timedelta +import logging +from typing import Any, Callable, Dict, Hashable, Optional + +from homeassistant.const import MAX_TIME_TRACKING_ERROR +from homeassistant.core import HomeAssistant, callback +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +class KeyedRateLimit: + """Class to track rate limits.""" + + def __init__( + self, + hass: HomeAssistant, + ): + """Initialize ratelimit tracker.""" + self.hass = hass + self._last_triggered: Dict[Hashable, datetime] = {} + self._rate_limit_timers: Dict[Hashable, asyncio.TimerHandle] = {} + + @callback + def async_has_timer(self, key: Hashable) -> bool: + """Check if a rate limit timer is running.""" + return key in self._rate_limit_timers + + @callback + def async_triggered(self, key: Hashable, now: Optional[datetime] = None) -> None: + """Call when the action we are tracking was triggered.""" + self.async_cancel_timer(key) + self._last_triggered[key] = now or dt_util.utcnow() + + @callback + def async_cancel_timer(self, key: Hashable) -> None: + """Cancel a rate limit time that will call the action.""" + if not self.async_has_timer(key): + return + + self._rate_limit_timers.pop(key).cancel() + + @callback + def async_remove(self) -> None: + """Remove all timers.""" + for timer in self._rate_limit_timers.values(): + timer.cancel() + self._rate_limit_timers.clear() + + @callback + def async_schedule_action( + self, + key: Hashable, + rate_limit: Optional[timedelta], + now: datetime, + action: Callable, + *args: Any, + ) -> Optional[datetime]: + """Check rate limits and schedule an action if we hit the limit. + + If the rate limit is hit: + Schedules the action for when the rate limit expires + if there are no pending timers. The action must + be called in async. + + Returns the time the rate limit will expire + + If the rate limit is not hit: + + Return None + """ + if rate_limit is None or key not in self._last_triggered: + return None + + next_call_time = self._last_triggered[key] + rate_limit + + if next_call_time <= now: + self.async_cancel_timer(key) + return None + + _LOGGER.debug( + "Reached rate limit of %s for %s and deferred action until %s", + rate_limit, + key, + next_call_time, + ) + + if key not in self._rate_limit_timers: + self._rate_limit_timers[key] = self.hass.loop.call_later( + (next_call_time - now).total_seconds() + MAX_TIME_TRACKING_ERROR, + action, + *args, + ) + + return next_call_time diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6261f7b2257..b877e0b0e12 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -72,6 +72,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = { "name", } +DEFAULT_RATE_LIMIT = timedelta(seconds=1) + @bind_hass def attach(hass: HomeAssistantType, obj: Any) -> None: @@ -198,10 +200,11 @@ class RenderInfo: self.domains = set() self.domains_lifecycle = set() self.entities = set() + self.rate_limit = None def __repr__(self) -> str: """Representation of RenderInfo.""" - return f"" + return f"" def _filter_domains_and_entities(self, entity_id: str) -> bool: """Template should re-render if the entity state changes when we match specific domains or entities.""" @@ -221,16 +224,24 @@ class RenderInfo: def _freeze_static(self) -> None: self.is_static = True - self.entities = frozenset(self.entities) - self.domains = frozenset(self.domains) - self.domains_lifecycle = frozenset(self.domains_lifecycle) + self._freeze_sets() self.all_states = False - def _freeze(self) -> None: + def _freeze_sets(self) -> None: self.entities = frozenset(self.entities) self.domains = frozenset(self.domains) self.domains_lifecycle = frozenset(self.domains_lifecycle) + def _freeze(self) -> None: + self._freeze_sets() + + if self.rate_limit is None and ( + self.domains or self.domains_lifecycle or self.all_states or self.exception + ): + # If the template accesses all states or an entire + # domain, and no rate limit is set, we use the default. + self.rate_limit = DEFAULT_RATE_LIMIT + if self.exception: return @@ -478,6 +489,26 @@ class Template: return 'Template("' + self.template + '")' +class RateLimit: + """Class to control update rate limits.""" + + def __init__(self, hass: HomeAssistantType): + """Initialize rate limit.""" + self._hass = hass + + def __call__(self, *args: Any, **kwargs: Any) -> str: + """Handle a call to the class.""" + render_info = self._hass.data.get(_RENDER_INFO) + if render_info is not None: + render_info.rate_limit = timedelta(*args, **kwargs) + + return "" + + def __repr__(self) -> str: + """Representation of a RateLimit.""" + return "